mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-12 02:57:44 -04:00
* chore: update all dependencies to latest versions Update all outdated dependencies across the monorepo catalog and fix breaking changes from major version bumps. Notable updates: - ESLint 9 → 10 (fix custom rule API, disable new no-useless-assignment) - @stylistic/eslint-plugin 4 → 5 (auto-fixed indent changes) - @cyclonedx/cyclonedx-library 9 → 10 (adapt to removed SPDX API) - esbuild 0.25 → 0.27 - TypeScript 5.9.2 → 5.9.3 - Various @types packages, test utilities, and build tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update unified/remark/mdast imports for v11/v4 API changes Update imports in get-release-text for the new ESM named exports: - mdast-util-to-string: default → { toString } - unified: default → { unified } Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve typecheck errors from dependency updates - isexe v4: use named import { sync } instead of default export - remark-parse/remark-stringify v11: add vfile as packageExtension dependency so TypeScript can resolve type declarations - get-release-text: remove unused @ts-expect-error directives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert runtime dependency major version bumps Revert major version bumps for runtime dependencies that are bundled into pnpm to fix test failures where pnpm add silently fails: - bin-links: keep ^5.0.0 (was ^6.0.0) - cli-truncate: keep ^4.0.0 (was ^5.2.0) - delay: keep ^6.0.0 (was ^7.0.0) - filenamify: keep ^6.0.0 (was ^7.0.1) - find-up: keep ^7.0.0 (was ^8.0.0) - isexe: keep 2.0.0 (was 4.0.0) - normalize-newline: keep 4.1.0 (was 5.0.0) - p-queue: keep ^8.1.0 (was ^9.1.0) - ps-list: keep ^8.1.1 (was ^9.0.0) - string-length: keep ^6.0.0 (was ^7.0.1) - symlink-dir: keep ^7.0.0 (was ^9.0.0) - terminal-link: keep ^4.0.0 (was ^5.0.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore runtime dependency major version bumps Re-apply all runtime dependency major version bumps that were previously reverted. All packages maintain their default exports except isexe v4 which needs named imports. Updated runtime deps: - bin-links: ^5.0.0 → ^6.0.0 - cli-truncate: ^4.0.0 → ^5.2.0 - delay: ^6.0.0 → ^7.0.0 - filenamify: ^6.0.0 → ^7.0.1 - find-up: ^7.0.0 → ^8.0.0 - isexe: 2.0.0 → 4.0.0 (fix: use named import { sync }) - normalize-newline: 4.1.0 → 5.0.0 - p-queue: ^8.1.0 → ^9.1.0 - ps-list: ^8.1.1 → ^9.0.0 - string-length: ^6.0.0 → ^7.0.1 - symlink-dir: ^7.0.0 → ^9.0.0 - terminal-link: ^4.0.0 → ^5.0.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert tempy to 3.0.0 to fix bundle hang tempy 3.2.0 pulls in temp-dir 3.0.0 which uses async fs.realpath() inside its module init. When bundled by esbuild into the __esm lazy init pattern, this causes a deadlock during module initialization, making the pnpm binary hang silently on startup. Keeping tempy at 3.0.0 which uses temp-dir 2.x (sync fs.realpathSync). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add comment explaining why tempy cannot be upgraded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert nock to 13.3.4 for node-fetch compatibility nock 14 changed its HTTP interception mechanism in a way that doesn't properly intercept node-fetch requests, causing audit tests to hang waiting for responses that are never intercepted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add comment explaining why nock cannot be upgraded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update symlink-dir imports for v10 ESM named exports symlink-dir v10 removed the default export and switched to named exports: { symlinkDir, symlinkDirSync }. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert @typescript/native-preview to working version Newer tsgo dev builds (>= 20260318) have a regression where @types/node cannot be resolved, breaking all node built-in types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: vulnerabilities * fix: align comment indentation in runLifecycleHook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pin msgpackr to 1.11.8 for TypeScript 5.9 compatibility msgpackr 1.11.9 has broken type definitions that use Iterable/Iterator without required type arguments, causing compile errors with TS 5.9. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
445 lines
14 KiB
TypeScript
445 lines
14 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { linkBinsOfPkgsByAliases, type WarnFunction } from '@pnpm/bins.linker'
|
|
import { createMatcher } from '@pnpm/config.matcher'
|
|
import { WANTED_LOCKFILE } from '@pnpm/constants'
|
|
import { linkLogger } from '@pnpm/core-loggers'
|
|
import { logger } from '@pnpm/logger'
|
|
import type { DependenciesField, DepPath, HoistedDependencies, ProjectId } from '@pnpm/types'
|
|
import { lexCompare } from '@pnpm/util.lex-comparator'
|
|
import { isSubdir } from 'is-subdir'
|
|
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<T extends string> extends GetHoistedDependenciesOpts<T> {
|
|
extraNodePath?: string[]
|
|
preferSymlinkedExecutables?: boolean
|
|
virtualStoreDir: string
|
|
virtualStoreDirMaxLength: number
|
|
}
|
|
|
|
export async function hoist<T extends string> (opts: HoistOpts<T>): Promise<HoistedDependencies | null> {
|
|
const result = getHoistedDependencies(opts)
|
|
if (!result) return null
|
|
const { hoistedDependencies, hoistedAliasesWithBins, hoistedDependenciesByNodeId } = result
|
|
|
|
await symlinkHoistedDependencies(hoistedDependenciesByNodeId, {
|
|
graph: opts.graph,
|
|
directDepsByImporterId: opts.directDepsByImporterId,
|
|
privateHoistedModulesDir: opts.privateHoistedModulesDir,
|
|
publicHoistedModulesDir: opts.publicHoistedModulesDir,
|
|
virtualStoreDir: opts.virtualStoreDir,
|
|
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
|
hoistedWorkspacePackages: opts.hoistedWorkspacePackages,
|
|
})
|
|
|
|
// Here we only link the bins of the privately hoisted modules.
|
|
// The bins of the publicly hoisted modules will be linked together with
|
|
// the bins of the project's direct dependencies.
|
|
// This is possible because the publicly hoisted modules
|
|
// are in the same directory as the regular dependencies.
|
|
await linkAllBins(opts.privateHoistedModulesDir, {
|
|
extraNodePaths: opts.extraNodePath,
|
|
hoistedAliasesWithBins,
|
|
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
|
|
})
|
|
|
|
return hoistedDependencies
|
|
}
|
|
|
|
export interface GetHoistedDependenciesOpts<T extends string> {
|
|
graph: DependenciesGraph<T>
|
|
skipped: Set<DepPath>
|
|
directDepsByImporterId: DirectDependenciesByImporterId<T>
|
|
importerIds?: ProjectId[]
|
|
privateHoistPattern: string[]
|
|
privateHoistedModulesDir: string
|
|
publicHoistPattern: string[]
|
|
publicHoistedModulesDir: string
|
|
hoistedWorkspacePackages?: Record<ProjectId, HoistedWorkspaceProject>
|
|
}
|
|
|
|
export interface HoistedWorkspaceProject {
|
|
name: string
|
|
dir: string
|
|
}
|
|
|
|
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.
|
|
// That is why we can't just simply use the lockfile walker to include links to local workspace packages too.
|
|
// We have to explicitly include all the workspace packages.
|
|
const hoistedWorkspaceDeps: Record<string, ProjectId> = Object.fromEntries(
|
|
Object.entries(opts.hoistedWorkspacePackages ?? {})
|
|
.map(([id, { name }]) => [name, id as ProjectId])
|
|
)
|
|
const deps: Array<Dependency<T>> = [
|
|
{
|
|
children: {
|
|
...hoistedWorkspaceDeps,
|
|
...directDeps
|
|
.reduce((acc, { alias, nodeId }) => {
|
|
if (!acc[alias]) {
|
|
acc[alias] = nodeId
|
|
}
|
|
return acc
|
|
}, {} as Record<string, T>),
|
|
},
|
|
nodeId: '' as T,
|
|
depth: -1,
|
|
},
|
|
...getDependencies(0, step),
|
|
]
|
|
|
|
const getAliasHoistType = createGetAliasHoistType(opts.publicHoistPattern, opts.privateHoistPattern)
|
|
|
|
return hoistGraph(deps, opts.directDepsByImporterId['.' as ProjectId] ?? new Map(), {
|
|
getAliasHoistType,
|
|
graph: opts.graph,
|
|
skipped: opts.skipped,
|
|
})
|
|
}
|
|
|
|
type GetAliasHoistType = (alias: string) => 'private' | 'public' | false
|
|
|
|
function createGetAliasHoistType (
|
|
publicHoistPattern: string[],
|
|
privateHoistPattern: string[]
|
|
): GetAliasHoistType {
|
|
const publicMatcher = createMatcher(publicHoistPattern)
|
|
const privateMatcher = createMatcher(privateHoistPattern)
|
|
return (alias: string) => {
|
|
if (publicMatcher(alias)) return 'public'
|
|
if (privateMatcher(alias)) return 'private'
|
|
return false
|
|
}
|
|
}
|
|
|
|
interface LinkAllBinsOptions {
|
|
extraNodePaths?: string[]
|
|
hoistedAliasesWithBins: string[]
|
|
preferSymlinkedExecutables?: boolean
|
|
}
|
|
|
|
async function linkAllBins (modulesDir: string, opts: LinkAllBinsOptions): Promise<void> {
|
|
const bin = path.join(modulesDir, '.bin')
|
|
const warn: WarnFunction = (message, code) => {
|
|
if (code === 'BINARIES_CONFLICT') return
|
|
logger.info({ message, prefix: path.join(modulesDir, '../..') })
|
|
}
|
|
try {
|
|
await linkBinsOfPkgsByAliases(opts.hoistedAliasesWithBins, bin, {
|
|
allowExoticManifests: true,
|
|
extraNodePaths: opts.extraNodePaths,
|
|
modulesDir,
|
|
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
|
|
warn,
|
|
})
|
|
} catch (err: any) { // eslint-disable-line
|
|
// Some packages generate their commands with lifecycle hooks.
|
|
// At this stage, such commands are not generated yet.
|
|
// For now, we don't hoist such generated commands.
|
|
// Related issue: https://github.com/pnpm/pnpm/issues/2071
|
|
}
|
|
}
|
|
|
|
function getDependencies<T extends string> (
|
|
depth: number,
|
|
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: node.children,
|
|
nodeId,
|
|
depth,
|
|
})
|
|
|
|
nextSteps.push(next())
|
|
}
|
|
|
|
for (const depPath of step.missing) {
|
|
// It might make sense to fail if the depPath is not in the skipped list from .modules.yaml
|
|
// However, the skipped list currently contains package IDs, not dep paths.
|
|
logger.debug({ message: `No entry for "${depPath}" in ${WANTED_LOCKFILE}` })
|
|
}
|
|
|
|
return [
|
|
...deps,
|
|
...(nextSteps.flatMap(getDependencies.bind(null, depth + 1)) as Array<Dependency<T>>),
|
|
]
|
|
}
|
|
|
|
export interface Dependency<T extends string> {
|
|
children: Record<string, T | ProjectId>
|
|
nodeId: T
|
|
depth: number
|
|
}
|
|
|
|
interface HoistGraphResult<T extends string> {
|
|
hoistedDependencies: HoistedDependencies
|
|
hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T>
|
|
hoistedAliasesWithBins: 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
|
|
graph: DependenciesGraph<T>
|
|
skipped: Set<DepPath>
|
|
}
|
|
): 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.nodeId, b.nodeId) : depthDiff
|
|
})
|
|
// build the alias map and the id map
|
|
.forEach((depNode) => {
|
|
for (const [childAlias, childNodeId] of Object.entries<T | ProjectId>(depNode.children)) {
|
|
const hoist = opts.getAliasHoistType(childAlias)
|
|
if (!hoist) continue
|
|
const childAliasNormalized = childAlias.toLowerCase()
|
|
// if this alias has already been taken, skip it
|
|
if (hoistedAliases.has(childAliasNormalized)) {
|
|
continue
|
|
}
|
|
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[node.depPath]) {
|
|
hoistedDependencies[node.depPath] = {}
|
|
}
|
|
hoistedDependencies[node.depPath][childAlias] = hoist
|
|
}
|
|
})
|
|
|
|
return {
|
|
hoistedDependencies,
|
|
hoistedDependenciesByNodeId,
|
|
hoistedAliasesWithBins: Array.from(hoistedAliasesWithBins),
|
|
}
|
|
}
|
|
|
|
async function symlinkHoistedDependencies<T extends string> (
|
|
hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T>,
|
|
opts: {
|
|
graph: DependenciesGraph<T>
|
|
directDepsByImporterId: DirectDependenciesByImporterId<T>
|
|
privateHoistedModulesDir: string
|
|
publicHoistedModulesDir: string
|
|
virtualStoreDir: string
|
|
virtualStoreDirMaxLength: number
|
|
hoistedWorkspacePackages?: Record<string, HoistedWorkspaceProject>
|
|
}
|
|
): Promise<void> {
|
|
const symlink = symlinkHoistedDependency.bind(null, opts)
|
|
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
|
|
}
|
|
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 (
|
|
opts: { virtualStoreDir: string },
|
|
depLocation: string,
|
|
dest: string
|
|
): Promise<void> {
|
|
try {
|
|
await symlinkDir(depLocation, dest, { overwrite: false })
|
|
linkLogger.debug({ target: dest, link: depLocation })
|
|
return
|
|
} catch (err: any) { // eslint-disable-line
|
|
if (err.code !== 'EEXIST' && err.code !== 'EISDIR') throw err
|
|
}
|
|
let existingSymlink!: string
|
|
try {
|
|
existingSymlink = await resolveLinkTarget(dest)
|
|
} catch {
|
|
hoistLogger.debug({
|
|
skipped: dest,
|
|
reason: 'a directory is present at the target location',
|
|
})
|
|
return
|
|
}
|
|
if (!isSubdir(opts.virtualStoreDir, existingSymlink)) {
|
|
hoistLogger.debug({
|
|
skipped: dest,
|
|
existingSymlink,
|
|
reason: 'an external symlink is present at the target location',
|
|
})
|
|
return
|
|
}
|
|
await fs.promises.unlink(dest)
|
|
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>
|
|
}
|