Hoists dependencies in a node_modules created by pnpm
npx @tessl/cli install tessl/npm-pnpm--hoist@1002.0.0@pnpm/hoist provides sophisticated dependency hoisting algorithms for pnpm-managed node_modules directories. It implements logic to move dependencies up the directory tree hierarchy to reduce duplication and improve module resolution performance, supporting both public and private hoisting patterns with configurable matching rules.
pnpm add @pnpm/hoistimport { hoist, getHoistedDependencies, graphWalker } from "@pnpm/hoist";
import type {
HoistOpts,
DependenciesGraph,
DirectDependenciesByImporterId,
HoistedWorkspaceProject
} from "@pnpm/hoist";For CommonJS:
const { hoist, getHoistedDependencies, graphWalker } = require("@pnpm/hoist");import { hoist } from "@pnpm/hoist";
import type { HoistOpts, DependenciesGraph } from "@pnpm/hoist";
import type { ProjectId } from "@pnpm/types";
// Basic hoisting operation
const graph: DependenciesGraph<string> = {
// ... dependency graph data
};
const hoistedDeps = await hoist({
graph,
directDepsByImporterId: {
'.': new Map([['lodash', 'lodash@4.17.21']]),
},
importerIds: ['.' as ProjectId],
privateHoistPattern: ['*'],
privateHoistedModulesDir: '/path/to/node_modules/.pnpm',
publicHoistPattern: ['lodash'],
publicHoistedModulesDir: '/path/to/node_modules',
virtualStoreDir: '/path/to/.pnpm',
virtualStoreDirMaxLength: 120,
skipped: new Set(),
});@pnpm/hoist is built around several key components:
Performs complete dependency hoisting with symlink creation and binary linking.
/**
* Performs dependency hoisting with symlink creation and binary linking
* @param opts - Configuration options for hoisting operation
* @returns Promise resolving to hoisted dependencies data or null if no hoisting needed
*/
async function hoist<T extends string>(opts: HoistOpts<T>): Promise<HoistedDependencies | null>;
interface HoistOpts<T extends string> extends GetHoistedDependenciesOpts<T> {
/** Additional Node.js paths for binary resolution */
extraNodePath?: string[];
/** Whether to prefer symlinked executables over copied ones */
preferSymlinkedExecutables?: boolean;
/** Path to the virtual store directory */
virtualStoreDir: string;
/** Maximum length allowed for virtual store directory paths */
virtualStoreDirMaxLength: number;
}Computes which dependencies should be hoisted without creating symlinks.
/**
* Computes which dependencies should be hoisted without actually creating symlinks
* @param opts - Configuration options for hoisting analysis
* @returns Hoisting analysis result or null if no dependencies to analyze
*/
function getHoistedDependencies<T extends string>(opts: GetHoistedDependenciesOpts<T>): {
hoistedDependencies: HoistedDependencies;
hoistedDependenciesByNodeId: Map<T | ProjectId, Record<string, 'public' | 'private'>>;
hoistedAliasesWithBins: string[];
} | null;
interface GetHoistedDependenciesOpts<T extends string> {
/** The dependency graph to analyze */
graph: DependenciesGraph<T>;
/** Set of dependency paths to skip */
skipped: Set<DepPath>;
/** Maps importer IDs to their direct dependencies */
directDepsByImporterId: DirectDependenciesByImporterId<T>;
/** Optional list of importer IDs to process */
importerIds?: ProjectId[];
/** Glob patterns for private hoisting (hoisted to .pnpm directory) */
privateHoistPattern: string[];
/** Directory for privately hoisted modules */
privateHoistedModulesDir: string;
/** Glob patterns for public hoisting (hoisted to root node_modules) */
publicHoistPattern: string[];
/** Directory for publicly hoisted modules */
publicHoistedModulesDir: string;
/** Optional workspace packages that should be hoisted */
hoistedWorkspacePackages?: Record<ProjectId, HoistedWorkspaceProject>;
}Creates a walker for efficient dependency graph traversal.
/**
* Creates a walker for traversing the dependency graph
* @param graph - The dependency graph to traverse
* @param directDepsByImporterId - Maps importer IDs to their direct dependencies
* @param opts - Optional traversal configuration
* @returns Graph walker instance for step-by-step traversal
*/
function graphWalker<T extends string>(
graph: DependenciesGraph<T>,
directDepsByImporterId: DirectDependenciesByImporterId<T>,
opts?: {
include?: { [dependenciesField in DependenciesField]: boolean };
skipped?: Set<DepPath>;
}
): GraphWalker<T>;
interface GraphWalker<T extends string> {
/** Direct dependencies found during graph analysis */
directDeps: Array<{
alias: string;
nodeId: T;
}>;
/** Initial step for graph traversal */
step: GraphWalkerStep<T>;
}
interface GraphWalkerStep<T extends string> {
/** Dependencies found at this step */
dependencies: Array<GraphDependency<T>>;
/** Linked dependencies found at this step */
links: string[];
/** Missing dependencies found at this step */
missing: string[];
}
interface GraphDependency<T extends string> {
/** Node ID for this dependency */
nodeId: T;
/** The dependency graph node */
node: DependenciesGraphNode<T>;
/** Function to get the next step in traversal */
next: () => GraphWalkerStep<T>;
}interface DependenciesGraphNode<T extends string> {
/** Directory path where the dependency is located */
dir: string;
/** Child dependencies mapped by alias to node ID */
children: Record<string, T>;
/** Set of optional dependency aliases */
optionalDependencies: Set<string>;
/** Whether this dependency has executable binaries */
hasBin: boolean;
/** Package name */
name: string;
/** Dependency path identifier */
depPath: DepPath;
}
type DependenciesGraph<T extends string> = Record<T, DependenciesGraphNode<T>>;
interface DirectDependenciesByImporterId<T extends string> {
[importerId: string]: Map<string, T>;
}interface HoistedWorkspaceProject {
/** Package name */
name: string;
/** Directory path of the workspace project */
dir: string;
}interface Dependency<T extends string> {
/** Child dependencies mapped by alias to node ID or project ID */
children: Record<string, T | ProjectId>;
/** Node ID for this dependency */
nodeId: T;
/** Depth level in the dependency tree */
depth: number;
}These types are imported from @pnpm/types:
type DepPath = string & { __brand: 'DepPath' };
type ProjectId = string & { __brand: 'ProjectId' };
type DependenciesField = 'optionalDependencies' | 'dependencies' | 'devDependencies';
type HoistedDependencies = Record<DepPath | ProjectId, Record<string, 'public' | 'private'>>;The hoisting functions handle various error conditions:
import { hoist } from "@pnpm/hoist";
const result = await hoist({
graph: dependencyGraph,
directDepsByImporterId: { '.': directDeps },
importerIds: ['.'],
privateHoistPattern: [],
privateHoistedModulesDir: '/project/node_modules/.pnpm',
publicHoistPattern: ['lodash', 'react*'],
publicHoistedModulesDir: '/project/node_modules',
virtualStoreDir: '/project/node_modules/.pnpm',
virtualStoreDirMaxLength: 120,
skipped: new Set(),
});import { getHoistedDependencies } from "@pnpm/hoist";
const analysis = getHoistedDependencies({
graph: dependencyGraph,
directDepsByImporterId: { '.': directDeps },
privateHoistPattern: ['*'],
privateHoistedModulesDir: '/project/node_modules/.pnpm',
publicHoistPattern: ['popular-*'],
publicHoistedModulesDir: '/project/node_modules',
skipped: new Set(),
});import { graphWalker } from "@pnpm/hoist";
const walker = graphWalker(graph, directDepsByImporterId, {
include: {
dependencies: true,
devDependencies: false,
optionalDependencies: true,
peerDependencies: true
}
});
// Process direct dependencies
for (const { alias, nodeId } of walker.directDeps) {
console.log(`Direct dependency: ${alias} -> ${nodeId}`);
}
// Walk through dependency steps
let currentStep = walker.step;
while (currentStep.dependencies.length > 0) {
for (const dep of currentStep.dependencies) {
console.log(`Processing: ${dep.nodeId}`);
currentStep = dep.next();
}
}