mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-15 02:18:31 -05:00
feat: enable injected local packages to work with global virtual store (#10366)
* feat: enable injected local packages to work with global virtual store by leveraging `pkgLocationsByDepPath` for `file:` dependencies. * fix: populate `pkgLocationsByDepPath` directly for directory dependencies in the graph builder * refactor: store directory dependencies as a Map instead of an object * refactor: improve file: dependency target directory resolution by prioritizing `directoryDepsByDepPath` and providing a lockfile fallback. * refactor: remove `pkgLocationsByDepPath` from hoisted dependency graph generation parameters * test: fix * test: fix * refactor: simplify directory lookup for injected workspace packages by directly using the dependency graph * refactor: move extendProjectsWithTargetDirs to headless module and update imports * refactor: make `directoryDepsByDepPath` required in `LockfileToDepGraphOptions` and remove its nullish coalescing in headless * refactor: directory dependency tracking by renaming `directoryDepsByDepPath` to `injectionTargetsByDepPath` and extracting related logic, and remove an unused export. * docs: add changesets * fix: implemented CR suggestions
This commit is contained in:
5
.changeset/brave-bears-worry.md
Normal file
5
.changeset/brave-bears-worry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/headless": minor
|
||||
---
|
||||
|
||||
Export extendProjectsWithTargetDirs.
|
||||
9
.changeset/fix-injected-deps-global-vstore.md
Normal file
9
.changeset/fix-injected-deps-global-vstore.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/deps.graph-builder": patch
|
||||
"@pnpm/headless": patch
|
||||
"@pnpm/core": patch
|
||||
---
|
||||
|
||||
Fixed injected local packages to work correctly with the global virtual store [#10366](https://github.com/pnpm/pnpm/pull/10366).
|
||||
|
||||
When using `nodeLinker: 'isolated'` with `enableGlobalVirtualStore: true`, injected workspace packages now use the correct hash-based paths from the global virtual store instead of project-relative paths.
|
||||
5
.changeset/spotty-gifts-chew.md
Normal file
5
.changeset/spotty-gifts-chew.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/lockfile.utils": major
|
||||
---
|
||||
|
||||
Remove extendProjectsWithTargetDirs.
|
||||
17
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
17
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
@@ -99,7 +99,7 @@ export interface LockfileToDepGraphResult {
|
||||
hoistedLocations?: Record<string, string[]>
|
||||
symlinkedDirectDependenciesByImporterId?: DirectDependenciesByImporterId
|
||||
prevGraph?: DependenciesGraph
|
||||
pkgLocationsByDepPath?: Record<string, string[]>
|
||||
injectionTargetsByDepPath: Map<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +119,7 @@ export async function lockfileToDepGraph (
|
||||
const {
|
||||
graph,
|
||||
locationByDepPath,
|
||||
injectionTargetsByDepPath,
|
||||
} = await buildGraphFromPackages(lockfile, currentLockfile, opts)
|
||||
|
||||
const _getChildrenPaths = getChildrenPaths.bind(null, {
|
||||
@@ -156,7 +157,7 @@ export async function lockfileToDepGraph (
|
||||
directDependenciesByImporterId[importerId] = _getChildrenPaths(rootDeps, null, importerId)
|
||||
}
|
||||
|
||||
return { graph, directDependenciesByImporterId }
|
||||
return { graph, directDependenciesByImporterId, injectionTargetsByDepPath }
|
||||
}
|
||||
|
||||
async function buildGraphFromPackages (
|
||||
@@ -166,10 +167,13 @@ async function buildGraphFromPackages (
|
||||
): Promise<{
|
||||
graph: DependenciesGraph
|
||||
locationByDepPath: Record<string, string>
|
||||
injectionTargetsByDepPath: Map<string, string[]>
|
||||
}> {
|
||||
const currentPackages = currentLockfile?.packages ?? {}
|
||||
const graph: DependenciesGraph = {}
|
||||
const locationByDepPath: Record<string, string> = {}
|
||||
// Only populated for directory deps (injected workspace packages)
|
||||
const injectionTargetsByDepPath = new Map<string, string[]>()
|
||||
|
||||
const _getPatchInfo = getPatchInfo.bind(null, opts.patchedDependencies)
|
||||
const promises: Array<Promise<void>> = []
|
||||
@@ -201,7 +205,8 @@ async function buildGraphFromPackages (
|
||||
return
|
||||
}
|
||||
|
||||
const depIsPresent = !('directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null) &&
|
||||
const isDirectoryDep = 'directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null
|
||||
const depIsPresent = !isDirectoryDep &&
|
||||
currentPackages[depPath] &&
|
||||
equals(currentPackages[depPath].dependencies, pkgSnapshot.dependencies)
|
||||
|
||||
@@ -210,6 +215,10 @@ async function buildGraphFromPackages (
|
||||
const modules = path.join(opts.virtualStoreDir, dirNameInVirtualStore, 'node_modules')
|
||||
const dir = path.join(modules, pkgName)
|
||||
locationByDepPath[depPath] = dir
|
||||
// Track directory deps for injected workspace packages
|
||||
if (isDirectoryDep) {
|
||||
injectionTargetsByDepPath.set(depPath, [dir])
|
||||
}
|
||||
|
||||
let dirExists: boolean | undefined
|
||||
if (
|
||||
@@ -273,7 +282,7 @@ async function buildGraphFromPackages (
|
||||
})())
|
||||
}
|
||||
await Promise.all(promises)
|
||||
return { graph, locationByDepPath }
|
||||
return { graph, locationByDepPath, injectionTargetsByDepPath }
|
||||
}
|
||||
|
||||
interface GetChildrenPathsContext {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import path from 'path'
|
||||
import { type LockfileObject, type TarballResolution } from '@pnpm/lockfile.types'
|
||||
import { depPathToFilename } from '@pnpm/dependency-path'
|
||||
import { type ProjectId, type DepPath } from '@pnpm/types'
|
||||
import { packageIdFromSnapshot } from './packageIdFromSnapshot.js'
|
||||
import { nameVerFromPkgSnapshot } from './nameVerFromPkgSnapshot.js'
|
||||
|
||||
type GetLocalLocations = (depPath: DepPath, pkgName: string) => string[]
|
||||
|
||||
export function extendProjectsWithTargetDirs<T> (
|
||||
projects: Array<T & { id: ProjectId }>,
|
||||
lockfile: LockfileObject,
|
||||
ctx: {
|
||||
virtualStoreDir: string
|
||||
pkgLocationsByDepPath?: Record<DepPath, string[]>
|
||||
virtualStoreDirMaxLength: number
|
||||
}
|
||||
): Array<T & { id: ProjectId, stages: string[], targetDirs: string[] }> {
|
||||
const getLocalLocations: GetLocalLocations = ctx.pkgLocationsByDepPath != null
|
||||
? (depPath: DepPath) => ctx.pkgLocationsByDepPath![depPath]
|
||||
: (depPath: DepPath, pkgName: string) => [path.join(ctx.virtualStoreDir, depPathToFilename(depPath, ctx.virtualStoreDirMaxLength), 'node_modules', pkgName)]
|
||||
const projectsById: Record<ProjectId, T & { id: ProjectId, targetDirs: string[], stages?: string[] }> =
|
||||
Object.fromEntries(projects.map((project) => [project.id, { ...project, targetDirs: [] as string[] }]))
|
||||
for (const [depPath, pkg] of Object.entries(lockfile.packages ?? {})) {
|
||||
if ((pkg.resolution as TarballResolution)?.type !== 'directory') continue
|
||||
const pkgId = packageIdFromSnapshot(depPath as DepPath, pkg)
|
||||
const { name: pkgName } = nameVerFromPkgSnapshot(depPath, pkg)
|
||||
const importerId = pkgId.replace(/^file:/, '') as ProjectId
|
||||
if (projectsById[importerId] == null) continue
|
||||
const localLocations = getLocalLocations(depPath as DepPath, pkgName)
|
||||
if (!localLocations) continue
|
||||
projectsById[importerId].targetDirs.push(...localLocations)
|
||||
projectsById[importerId].stages = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublishOnly']
|
||||
}
|
||||
return Object.values(projectsById) as Array<T & { id: ProjectId, stages: string[], targetDirs: string[] }>
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { refToRelative } from '@pnpm/dependency-path'
|
||||
|
||||
export { extendProjectsWithTargetDirs } from './extendProjectsWithTargetDirs.js'
|
||||
export { nameVerFromPkgSnapshot } from './nameVerFromPkgSnapshot.js'
|
||||
export { packageIdFromSnapshot } from './packageIdFromSnapshot.js'
|
||||
export { packageIsIndependent } from './packageIsIndependent.js'
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@pnpm/lockfile.settings-checker'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { getContext, type PnpmContext } from '@pnpm/get-context'
|
||||
import { headlessInstall, type InstallationResultStats } from '@pnpm/headless'
|
||||
import { extendProjectsWithTargetDirs, headlessInstall, type InstallationResultStats } from '@pnpm/headless'
|
||||
import {
|
||||
makeNodeRequireOption,
|
||||
runLifecycleHook,
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
type CatalogSnapshots,
|
||||
} from '@pnpm/lockfile.fs'
|
||||
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
|
||||
import { extendProjectsWithTargetDirs } from '@pnpm/lockfile.utils'
|
||||
import { allProjectsAreUpToDate, satisfiesPackageManifest } from '@pnpm/lockfile.verification'
|
||||
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
|
||||
import { logger, globalInfo, streamParser } from '@pnpm/logger'
|
||||
@@ -1471,10 +1470,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
}
|
||||
}))
|
||||
|
||||
const projectsWithTargetDirs = extendProjectsWithTargetDirs(projects, newLockfile, {
|
||||
virtualStoreDir: ctx.virtualStoreDir,
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
})
|
||||
const projectsWithTargetDirs = getProjectsWithTargetDirs(projects, newLockfile, dependenciesGraph)
|
||||
const currentLockfileDir = path.join(ctx.rootModulesDir, '.pnpm')
|
||||
await Promise.all([
|
||||
opts.useLockfile && opts.saveLockfile
|
||||
@@ -1732,3 +1728,28 @@ export class IgnoredBuildsError extends PnpmError {
|
||||
function dedupePackageNamesFromIgnoredBuilds (ignoredBuilds: IgnoredBuilds): string[] {
|
||||
return Array.from(new Set(Array.from(ignoredBuilds ?? []).map(dp.removeSuffix))).sort(lexCompare)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build injectionTargetsByDepPath from the dependenciesGraph for injected workspace packages
|
||||
* and extend projects with their target directories.
|
||||
* The dependenciesGraph already has the correct `dir` values after `extendGraph` is applied
|
||||
* (which uses the correct hash-based paths when global virtual store is enabled).
|
||||
*/
|
||||
function getProjectsWithTargetDirs<T extends { id: ProjectId }> (
|
||||
projects: T[],
|
||||
lockfile: LockfileObject,
|
||||
dependenciesGraph: DependenciesGraph
|
||||
): Array<T & { id: ProjectId, stages: string[], targetDirs: string[] }> {
|
||||
const injectionTargetsByDepPath = new Map<string, string[]>()
|
||||
if (lockfile.packages) {
|
||||
for (const [depPath, { resolution }] of Object.entries(lockfile.packages)) {
|
||||
if (resolution?.type === 'directory') {
|
||||
const graphNode = dependenciesGraph[depPath as DepPath]
|
||||
if (graphNode?.dir) {
|
||||
injectionTargetsByDepPath.set(depPath, [graphNode.dir])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return extendProjectsWithTargetDirs(projects, injectionTargetsByDepPath)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { install } from '@pnpm/core'
|
||||
import { assertProject } from '@pnpm/assert-project'
|
||||
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
|
||||
import { install, type MutatedProject, mutateModules, type ProjectOptions } from '@pnpm/core'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import { sync as rimraf } from '@zkochan/rimraf'
|
||||
import { testDefaults } from '../utils/index.js'
|
||||
|
||||
@@ -71,3 +73,81 @@ test('modules are correctly updated when using a global virtual store', async ()
|
||||
expect(fs.existsSync(path.join(globalVirtualStoreDir, '@pnpm.e2e/peer-c/2.0.0', files[0], 'node_modules/@pnpm.e2e/peer-c/package.json'))).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('injected local packages work with global virtual store', async () => {
|
||||
const project1Manifest = {
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
}
|
||||
const project2Manifest = {
|
||||
name: 'project-2',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'project-1': 'workspace:1.0.0',
|
||||
},
|
||||
dependenciesMeta: {
|
||||
'project-1': {
|
||||
injected: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
preparePackages([
|
||||
{
|
||||
location: 'project-1',
|
||||
package: project1Manifest,
|
||||
},
|
||||
{
|
||||
location: 'project-2',
|
||||
package: project2Manifest,
|
||||
},
|
||||
])
|
||||
fs.writeFileSync('project-1/foo.js', '', 'utf8')
|
||||
|
||||
const globalVirtualStoreDir = path.resolve('links')
|
||||
const importers: MutatedProject[] = [
|
||||
{
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('project-1') as ProjectRootDir,
|
||||
},
|
||||
{
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('project-2') as ProjectRootDir,
|
||||
},
|
||||
]
|
||||
const allProjects: ProjectOptions[] = [
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: project1Manifest,
|
||||
rootDir: path.resolve('project-1') as ProjectRootDir,
|
||||
},
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: project2Manifest,
|
||||
rootDir: path.resolve('project-2') as ProjectRootDir,
|
||||
},
|
||||
]
|
||||
|
||||
await mutateModules(importers, testDefaults({
|
||||
autoInstallPeers: false,
|
||||
allProjects,
|
||||
enableGlobalVirtualStore: true,
|
||||
dedupeInjectedDeps: false,
|
||||
virtualStoreDir: globalVirtualStoreDir,
|
||||
}))
|
||||
|
||||
// Verify project-2 has project-1 installed
|
||||
expect(fs.existsSync(path.resolve('project-2/node_modules/project-1'))).toBeTruthy()
|
||||
|
||||
// Verify the modules manifest has injectedDeps pointing to global virtual store
|
||||
const rootModules = assertProject(process.cwd())
|
||||
const modulesState = rootModules.readModulesManifest()
|
||||
expect(modulesState?.injectedDeps?.['project-1']).toBeDefined()
|
||||
expect(modulesState?.injectedDeps?.['project-1'].length).toBeGreaterThan(0)
|
||||
// Injected deps should be in the global virtual store (links directory)
|
||||
const injectedDepLocation = modulesState?.injectedDeps?.['project-1'][0]
|
||||
expect(injectedDepLocation).toContain('links')
|
||||
expect(fs.existsSync(path.join(injectedDepLocation!, 'foo.js'))).toBeTruthy()
|
||||
})
|
||||
|
||||
26
pkg-manager/headless/src/extendProjectsWithTargetDirs.ts
Normal file
26
pkg-manager/headless/src/extendProjectsWithTargetDirs.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { parse as parseDepPath } from '@pnpm/dependency-path'
|
||||
import { type ProjectId } from '@pnpm/types'
|
||||
|
||||
export function extendProjectsWithTargetDirs<T> (
|
||||
projects: Array<T & { id: ProjectId }>,
|
||||
injectionTargetsByDepPath: Map<string, string[]>
|
||||
): Array<T & { id: ProjectId, stages: string[], targetDirs: string[] }> {
|
||||
const projectsById: Record<ProjectId, T & { id: ProjectId, targetDirs: string[], stages?: string[] }> =
|
||||
Object.fromEntries(projects.map((project) => [project.id, { ...project, targetDirs: [] as string[] }]))
|
||||
|
||||
for (const [depPath, locations] of injectionTargetsByDepPath) {
|
||||
const parsed = parseDepPath(depPath)
|
||||
if (!parsed.name || !parsed.nonSemverVersion?.startsWith('file:')) continue
|
||||
const importerId = parsed.nonSemverVersion.replace(/^file:/, '') as ProjectId
|
||||
if (projectsById[importerId] == null) continue
|
||||
// Dedupe: only add locations that aren't already tracked
|
||||
for (const location of locations) {
|
||||
if (!projectsById[importerId].targetDirs.includes(location)) {
|
||||
projectsById[importerId].targetDirs.push(location)
|
||||
}
|
||||
}
|
||||
projectsById[importerId].stages = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublishOnly']
|
||||
}
|
||||
|
||||
return Object.values(projectsById) as Array<T & { id: ProjectId, stages: string[], targetDirs: string[] }>
|
||||
}
|
||||
@@ -34,9 +34,9 @@ import {
|
||||
} from '@pnpm/lockfile.fs'
|
||||
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
|
||||
import {
|
||||
extendProjectsWithTargetDirs,
|
||||
nameVerFromPkgSnapshot,
|
||||
} from '@pnpm/lockfile.utils'
|
||||
import { extendProjectsWithTargetDirs } from './extendProjectsWithTargetDirs.js'
|
||||
import {
|
||||
type LogBase,
|
||||
logger,
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
} from '@pnpm/deps.graph-builder'
|
||||
import { lockfileToHoistedDepGraph } from './lockfileToHoistedDepGraph.js'
|
||||
import { linkDirectDeps, type LinkedDirectDep } from '@pnpm/pkg-manager.direct-dep-linker'
|
||||
export { extendProjectsWithTargetDirs } from './extendProjectsWithTargetDirs.js'
|
||||
|
||||
export type { HoistingLimits }
|
||||
|
||||
@@ -344,7 +345,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
|
||||
graph,
|
||||
hierarchy,
|
||||
hoistedLocations,
|
||||
pkgLocationsByDepPath,
|
||||
injectionTargetsByDepPath,
|
||||
prevGraph,
|
||||
symlinkedDirectDependenciesByImporterId,
|
||||
} = await (
|
||||
@@ -574,11 +575,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
|
||||
}
|
||||
}
|
||||
|
||||
const projectsToBeBuilt = extendProjectsWithTargetDirs(selectedProjects, wantedLockfile, {
|
||||
pkgLocationsByDepPath,
|
||||
virtualStoreDir,
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
})
|
||||
const projectsToBeBuilt = extendProjectsWithTargetDirs(selectedProjects, injectionTargetsByDepPath)
|
||||
|
||||
if (opts.enableModulesDir !== false) {
|
||||
const rootProjectDeps = !opts.dedupeDirectDeps ? {} : (directDependenciesByImporterId['.'] ?? {})
|
||||
|
||||
@@ -89,7 +89,8 @@ async function _lockfileToHoistedDepGraph (
|
||||
...opts,
|
||||
lockfile,
|
||||
graph,
|
||||
pkgLocationsByDepPath: {},
|
||||
pkgLocationsByDepPath: {} as Record<string, string[]>,
|
||||
injectionTargetsByDepPath: new Map<string, string[]>(),
|
||||
hoistedLocations: {} as Record<string, string[]>,
|
||||
}
|
||||
const hierarchy = {
|
||||
@@ -120,9 +121,9 @@ async function _lockfileToHoistedDepGraph (
|
||||
directDependenciesByImporterId,
|
||||
graph,
|
||||
hierarchy,
|
||||
pkgLocationsByDepPath: fetchDepsOpts.pkgLocationsByDepPath,
|
||||
symlinkedDirectDependenciesByImporterId,
|
||||
hoistedLocations: fetchDepsOpts.hoistedLocations,
|
||||
injectionTargetsByDepPath: fetchDepsOpts.injectionTargetsByDepPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +160,7 @@ async function fetchDeps (
|
||||
graph: DependenciesGraph
|
||||
lockfile: LockfileObject
|
||||
pkgLocationsByDepPath: Record<string, string[]>
|
||||
injectionTargetsByDepPath: Map<string, string[]>
|
||||
hoistedLocations: Record<string, string[]>
|
||||
} & LockfileToHoistedDepGraphOptions,
|
||||
modules: string,
|
||||
@@ -261,6 +263,15 @@ async function fetchDeps (
|
||||
opts.pkgLocationsByDepPath[depPath] = []
|
||||
}
|
||||
opts.pkgLocationsByDepPath[depPath].push(dir)
|
||||
// Track directory deps for injected workspace packages
|
||||
if ('directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null) {
|
||||
const locations = opts.injectionTargetsByDepPath.get(depPath)
|
||||
if (locations) {
|
||||
locations.push(dir)
|
||||
} else {
|
||||
opts.injectionTargetsByDepPath.set(depPath, [dir])
|
||||
}
|
||||
}
|
||||
depHierarchy[dir] = await fetchDeps(opts, path.join(dir, 'node_modules'), dep.dependencies)
|
||||
if (!opts.hoistedLocations[depPath]) {
|
||||
opts.hoistedLocations[depPath] = []
|
||||
|
||||
Reference in New Issue
Block a user