mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-30 13:02:03 -04:00
* refactor: simplify patchedDependencies lockfile format to map selectors to hashes
Remove the `path` field from patchedDependencies in the lockfile, changing the
format from `Record<string, { path: string, hash: string }>` to
`Record<string, string>` (selector → hash). The path was never consumed from
the lockfile — patch file paths come from user config, not the lockfile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate old patchedDependencies format when reading lockfile
When reading a lockfile with the old `{ path, hash }` format for
patchedDependencies, extract just the hash string. This ensures
backwards compatibility with existing lockfiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: carry patchFilePath through patch groups for runtime patch application
The previous commit removed `path` from the lockfile format but also
accidentally dropped it from the runtime PatchInfo type. This broke
patch application since `applyPatchToDir` needs the file path.
- Add optional `patchFilePath` to `PatchInfo` for runtime use
- Build patch groups with resolved file paths in install
- Fix `build-modules` to use `patchFilePath` instead of `file.path`
- Fix `calcPatchHashes` call site in `checkDepsStatus` (extra arg)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update remaining references to old PatchFile type
- Update getPatchInfo tests to use { hash, key } instead of { file, key }
- Fix createDeployFiles to handle patchedDependencies as hash strings
- Fix configurationalDependencies test assertion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: throw when patch exists but patchFilePath is missing
Also guard against undefined patchedDependencies entry when
ignorePackageManifest is true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: don't join lockfileDir with already-absolute patch file paths
opts.patchedDependencies values are already absolute paths, so
path.join(opts.lockfileDir, absolutePath) created invalid doubled
paths like /project/home/runner/work/pnpm/...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use path.resolve for patch file paths and address Copilot review
- Use path.resolve instead of path.join to correctly handle both
relative and absolute patch file paths
- Use PnpmError instead of plain Error for missing patch file path
- Only copy patchedDependencies to deploy output when manifest
provides the patch file paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass rootProjectManifest in deploy patchedDependencies test
The test was missing rootProjectManifest, so createDeployFiles could
not find the manifest's patchedDependencies to propagate to the
deploy output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import path from 'path'
|
|
import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
|
|
import {
|
|
progressLogger,
|
|
removalLogger,
|
|
statsLogger,
|
|
} from '@pnpm/core-loggers'
|
|
import type {
|
|
DepHierarchy,
|
|
DependenciesGraph,
|
|
} from '@pnpm/deps.graph-builder'
|
|
import { linkBins } from '@pnpm/link-bins'
|
|
import { logger } from '@pnpm/logger'
|
|
import type {
|
|
PackageFilesResponse,
|
|
StoreController,
|
|
} from '@pnpm/store-controller-types'
|
|
import pLimit from 'p-limit'
|
|
import { difference, isEmpty } from 'ramda'
|
|
import rimraf from '@zkochan/rimraf'
|
|
import type { AllowBuild } from '@pnpm/types'
|
|
|
|
const limitLinking = pLimit(16)
|
|
|
|
export async function linkHoistedModules (
|
|
storeController: StoreController,
|
|
graph: DependenciesGraph,
|
|
prevGraph: DependenciesGraph,
|
|
hierarchy: DepHierarchy,
|
|
opts: {
|
|
allowBuild?: AllowBuild
|
|
depsStateCache: DepsStateCache
|
|
disableRelinkLocalDirDeps?: boolean
|
|
force: boolean
|
|
ignoreScripts: boolean
|
|
lockfileDir: string
|
|
preferSymlinkedExecutables?: boolean
|
|
sideEffectsCacheRead: boolean
|
|
}
|
|
): Promise<void> {
|
|
// TODO: remove nested node modules first
|
|
const dirsToRemove = difference(
|
|
Object.keys(prevGraph),
|
|
Object.keys(graph)
|
|
)
|
|
statsLogger.debug({
|
|
prefix: opts.lockfileDir,
|
|
removed: dirsToRemove.length,
|
|
})
|
|
// We should avoid removing unnecessary directories while simultaneously adding new ones.
|
|
// Doing so can sometimes lead to a race condition when linking commands to `node_modules/.bin`.
|
|
await Promise.all(dirsToRemove.map((dir) => tryRemoveDir(dir)))
|
|
await Promise.all(
|
|
Object.entries(hierarchy)
|
|
.map(([parentDir, depsHierarchy]) => {
|
|
function warn (message: string) {
|
|
logger.info({
|
|
message,
|
|
prefix: parentDir,
|
|
})
|
|
}
|
|
return linkAllPkgsInOrder(storeController, graph, depsHierarchy, parentDir, {
|
|
...opts,
|
|
warn,
|
|
})
|
|
})
|
|
)
|
|
}
|
|
|
|
async function tryRemoveDir (dir: string): Promise<void> {
|
|
removalLogger.debug(dir)
|
|
try {
|
|
await rimraf(dir)
|
|
} catch (err: any) { // eslint-disable-line
|
|
/* Just ignoring for now. Not even logging.
|
|
logger.warn({
|
|
error: err,
|
|
message: `Failed to remove "${pathToRemove}"`,
|
|
prefix: lockfileDir,
|
|
})
|
|
*/
|
|
}
|
|
}
|
|
|
|
async function linkAllPkgsInOrder (
|
|
storeController: StoreController,
|
|
graph: DependenciesGraph,
|
|
hierarchy: DepHierarchy,
|
|
parentDir: string,
|
|
opts: {
|
|
allowBuild?: AllowBuild
|
|
depsStateCache: DepsStateCache
|
|
disableRelinkLocalDirDeps?: boolean
|
|
force: boolean
|
|
ignoreScripts: boolean
|
|
lockfileDir: string
|
|
preferSymlinkedExecutables?: boolean
|
|
sideEffectsCacheRead: boolean
|
|
warn: (message: string) => void
|
|
}
|
|
): Promise<void> {
|
|
const _calcDepState = calcDepState.bind(null, graph, opts.depsStateCache)
|
|
await Promise.all(
|
|
Object.entries(hierarchy).map(async ([dir, deps]) => {
|
|
const depNode = graph[dir]
|
|
if (depNode.fetching) {
|
|
let filesResponse!: PackageFilesResponse
|
|
try {
|
|
filesResponse = (await depNode.fetching()).files
|
|
} catch (err: any) { // eslint-disable-line
|
|
if (depNode.optional) return
|
|
throw err
|
|
}
|
|
|
|
depNode.requiresBuild = filesResponse.requiresBuild
|
|
let sideEffectsCacheKey: string | undefined
|
|
if (opts.sideEffectsCacheRead && filesResponse.sideEffectsMaps && !isEmpty(filesResponse.sideEffectsMaps)) {
|
|
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
|
|
sideEffectsCacheKey = _calcDepState(dir, {
|
|
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
|
|
patchFileHash: depNode.patch?.hash,
|
|
})
|
|
}
|
|
}
|
|
// Limiting the concurrency here fixes an out of memory error.
|
|
// It is not clear why it helps as importing is also limited inside fs.indexed-pkg-importer.
|
|
// The out of memory error was reproduced on the teambit/bit repository with the "rootComponents" feature turned on
|
|
await limitLinking(async () => {
|
|
const { importMethod, isBuilt } = await storeController.importPackage(depNode.dir, {
|
|
filesResponse,
|
|
force: true,
|
|
disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
|
|
keepModulesDir: true,
|
|
requiresBuild: depNode.patch != null || depNode.requiresBuild,
|
|
sideEffectsCacheKey,
|
|
})
|
|
if (importMethod) {
|
|
progressLogger.debug({
|
|
method: importMethod,
|
|
requester: opts.lockfileDir,
|
|
status: 'imported',
|
|
to: depNode.dir,
|
|
})
|
|
}
|
|
depNode.isBuilt = isBuilt
|
|
})
|
|
}
|
|
return linkAllPkgsInOrder(storeController, graph, deps, dir, opts)
|
|
})
|
|
)
|
|
const modulesDir = path.join(parentDir, 'node_modules')
|
|
const binsDir = path.join(modulesDir, '.bin')
|
|
await linkBins(modulesDir, binsDir, {
|
|
allowExoticManifests: true,
|
|
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
|
|
warn: opts.warn,
|
|
})
|
|
}
|