@pnpm/core
Version:
Fast, disk space efficient installation engine
388 lines • 18.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.linkPackages = linkPackages;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const calc_dep_state_1 = require("@pnpm/calc-dep-state");
const core_loggers_1 = require("@pnpm/core-loggers");
const lockfile_filtering_1 = require("@pnpm/lockfile.filtering");
const pkg_manager_direct_dep_linker_1 = require("@pnpm/pkg-manager.direct-dep-linker");
const hoist_1 = require("@pnpm/hoist");
const logger_1 = require("@pnpm/logger");
const modules_cleaner_1 = require("@pnpm/modules-cleaner");
const symlink_dependency_1 = require("@pnpm/symlink-dependency");
const worker_1 = require("@pnpm/worker");
const p_limit_1 = __importDefault(require("p-limit"));
const path_exists_1 = __importDefault(require("path-exists"));
const equals_1 = __importDefault(require("ramda/src/equals"));
const isEmpty_1 = __importDefault(require("ramda/src/isEmpty"));
const difference_1 = __importDefault(require("ramda/src/difference"));
const omit_1 = __importDefault(require("ramda/src/omit"));
const pick_1 = __importDefault(require("ramda/src/pick"));
const pickBy_1 = __importDefault(require("ramda/src/pickBy"));
const props_1 = __importDefault(require("ramda/src/props"));
const brokenModulesLogger = (0, logger_1.logger)('_broken_node_modules');
async function linkPackages(projects, depGraph, opts) {
let depNodes = Object.values(depGraph).filter(({ depPath, id }) => {
if (((opts.wantedLockfile.packages?.[depPath]) != null) && !opts.wantedLockfile.packages[depPath].optional) {
opts.skipped.delete(depPath);
return true;
}
if (opts.wantedToBeSkippedPackageIds.has(id)) {
opts.skipped.add(depPath);
return false;
}
opts.skipped.delete(depPath);
return true;
});
if (!opts.include.dependencies) {
depNodes = depNodes.filter(({ dev, optional }) => dev || optional);
}
if (!opts.include.devDependencies) {
depNodes = depNodes.filter(({ optional, prod }) => prod || optional);
}
if (!opts.include.optionalDependencies) {
depNodes = depNodes.filter(({ optional }) => !optional);
}
depGraph = Object.fromEntries(depNodes.map((depNode) => [depNode.depPath, depNode]));
const removedDepPaths = await (0, modules_cleaner_1.prune)(projects, {
currentLockfile: opts.currentLockfile,
dedupeDirectDeps: opts.dedupeDirectDeps,
hoistedDependencies: opts.hoistedDependencies,
hoistedModulesDir: (opts.hoistPattern != null) ? opts.hoistedModulesDir : undefined,
include: opts.include,
lockfileDir: opts.lockfileDir,
pruneStore: opts.pruneStore,
pruneVirtualStore: opts.pruneVirtualStore,
publicHoistedModulesDir: (opts.publicHoistPattern != null) ? opts.rootModulesDir : undefined,
skipped: opts.skipped,
storeController: opts.storeController,
virtualStoreDir: opts.virtualStoreDir,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
wantedLockfile: opts.wantedLockfile,
});
core_loggers_1.stageLogger.debug({
prefix: opts.lockfileDir,
stage: 'importing_started',
});
const projectIds = projects.map(({ id }) => id);
const filterOpts = {
include: opts.include,
registries: opts.registries,
skipped: opts.skipped,
};
const newCurrentLockfile = (0, lockfile_filtering_1.filterLockfileByImporters)(opts.wantedLockfile, projectIds, {
...filterOpts,
failOnMissingDependencies: true,
skipped: new Set(),
});
const { newDepPaths, added } = await linkNewPackages((0, lockfile_filtering_1.filterLockfileByImporters)(opts.currentLockfile, projectIds, {
...filterOpts,
failOnMissingDependencies: false,
}), newCurrentLockfile, depGraph, {
allowBuild: opts.allowBuild,
disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
force: opts.force,
depsStateCache: opts.depsStateCache,
ignoreScripts: opts.ignoreScripts,
lockfileDir: opts.lockfileDir,
optional: opts.include.optionalDependencies,
sideEffectsCacheRead: opts.sideEffectsCacheRead,
symlink: opts.symlink,
skipped: opts.skipped,
storeController: opts.storeController,
virtualStoreDir: opts.virtualStoreDir,
});
core_loggers_1.stageLogger.debug({
prefix: opts.lockfileDir,
stage: 'importing_done',
});
let currentLockfile;
const allImportersIncluded = (0, equals_1.default)(projectIds.sort(), Object.keys(opts.wantedLockfile.importers).sort());
if (opts.makePartialCurrentLockfile ||
!allImportersIncluded) {
const packages = opts.currentLockfile.packages ?? {};
if (opts.wantedLockfile.packages != null) {
for (const depPath in opts.wantedLockfile.packages) { // eslint-disable-line:forin
if (depGraph[depPath]) {
packages[depPath] = opts.wantedLockfile.packages[depPath];
}
}
}
const projects = {
...opts.currentLockfile.importers,
...(0, pick_1.default)(projectIds, opts.wantedLockfile.importers),
};
currentLockfile = (0, lockfile_filtering_1.filterLockfileByImporters)({
...opts.wantedLockfile,
importers: projects,
packages,
}, Object.keys(projects), {
...filterOpts,
failOnMissingDependencies: false,
skipped: new Set(),
});
}
else if (opts.include.dependencies &&
opts.include.devDependencies &&
opts.include.optionalDependencies &&
opts.skipped.size === 0) {
currentLockfile = opts.wantedLockfile;
}
else {
currentLockfile = newCurrentLockfile;
}
let newHoistedDependencies;
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 ? (0, omit_1.default)(Array.from(opts.skipped), currentLockfile.packages) : {},
};
newHoistedDependencies = await (0, hoist_1.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,
};
}
return hoistedWorkspacePackages;
}, {})
: undefined,
});
}
else {
newHoistedDependencies = opts.hoistedDependencies;
}
let linkedToRoot = 0;
if (opts.symlink) {
const projectsToLink = Object.fromEntries(await Promise.all(projects.map(async ({ id, manifest, modulesDir, rootDir }) => {
const deps = opts.dependenciesByProjectId[id];
const importerFromLockfile = newCurrentLockfile.importers[id];
return [id, {
dir: rootDir,
modulesDir,
dependencies: await Promise.all([
...Array.from(deps.entries())
.filter(([rootAlias]) => importerFromLockfile.specifiers[rootAlias])
.map(([rootAlias, depPath]) => ({ rootAlias, depGraphNode: depGraph[depPath] }))
.filter(({ depGraphNode }) => depGraphNode)
.map(async ({ rootAlias, depGraphNode }) => {
const isDev = Boolean(manifest.devDependencies?.[depGraphNode.name]);
const isOptional = Boolean(manifest.optionalDependencies?.[depGraphNode.name]);
return {
alias: rootAlias,
name: depGraphNode.name,
version: depGraphNode.version,
dir: depGraphNode.dir,
id: depGraphNode.id,
dependencyType: (isDev && 'dev' || isOptional && 'optional' || 'prod'),
latest: opts.outdatedDependencies[depGraphNode.id],
isExternalLink: false,
};
}),
...opts.linkedDependenciesByProjectId[id].map(async (linkedDependency) => {
const dir = resolvePath(rootDir, linkedDependency.resolution.directory);
return {
alias: linkedDependency.alias,
name: linkedDependency.name,
version: linkedDependency.version,
dir,
id: linkedDependency.resolution.directory,
dependencyType: (linkedDependency.dev && 'dev' || linkedDependency.optional && 'optional' || 'prod'),
isExternalLink: true,
};
}),
]),
}];
})));
linkedToRoot = await (0, pkg_manager_direct_dep_linker_1.linkDirectDeps)(projectsToLink, { dedupe: opts.dedupeDirectDeps });
}
return {
currentLockfile,
newDepPaths,
newHoistedDependencies,
removedDepPaths,
stats: {
added,
removed: removedDepPaths.size,
linkedToRoot,
},
};
}
const isAbsolutePath = /^\/|^[A-Z]:/i;
// This function is copied from @pnpm/local-resolver
function resolvePath(where, spec) {
if (isAbsolutePath.test(spec))
return spec;
return path_1.default.resolve(where, spec);
}
async function linkNewPackages(currentLockfile, wantedLockfile, depGraph, opts) {
const wantedRelDepPaths = (0, difference_1.default)(Object.keys(wantedLockfile.packages ?? {}), Array.from(opts.skipped));
let newDepPathsSet;
if (opts.force) {
newDepPathsSet = new Set(wantedRelDepPaths
// when installing a new package, not all the nodes are analyzed
// just skip the ones that are in the lockfile but were not analyzed
.filter((depPath) => depGraph[depPath]));
}
else {
newDepPathsSet = await selectNewFromWantedDeps(wantedRelDepPaths, currentLockfile, depGraph);
}
const added = newDepPathsSet.size;
core_loggers_1.statsLogger.debug({
added,
prefix: opts.lockfileDir,
});
const existingWithUpdatedDeps = [];
if (!opts.force && (currentLockfile.packages != null) && (wantedLockfile.packages != null)) {
// add subdependencies that have been updated
// TODO: no need to relink everything. Can be relinked only what was changed
for (const depPath of wantedRelDepPaths) {
if (currentLockfile.packages[depPath] &&
(!(0, equals_1.default)(currentLockfile.packages[depPath].dependencies, wantedLockfile.packages[depPath].dependencies) ||
!(0, isEmpty_1.default)(currentLockfile.packages[depPath].optionalDependencies ?? {}) ||
!(0, isEmpty_1.default)(wantedLockfile.packages[depPath].optionalDependencies ?? {}))) {
// TODO: come up with a test that triggers the usecase of depGraph[depPath] undefined
// see related issue: https://github.com/pnpm/pnpm/issues/870
if (depGraph[depPath] && !newDepPathsSet.has(depPath)) {
existingWithUpdatedDeps.push(depGraph[depPath]);
}
}
}
}
if (!newDepPathsSet.size && (existingWithUpdatedDeps.length === 0))
return { newDepPaths: [], added };
const newDepPaths = Array.from(newDepPathsSet);
const newPkgs = (0, props_1.default)(newDepPaths, depGraph);
await Promise.all(newPkgs.map(async (depNode) => fs_1.promises.mkdir(depNode.modules, { recursive: true })));
await Promise.all([
!opts.symlink
? Promise.resolve()
: linkAllModules([...newPkgs, ...existingWithUpdatedDeps], depGraph, {
lockfileDir: opts.lockfileDir,
optional: opts.optional,
}),
linkAllPkgs(opts.storeController, newPkgs, {
allowBuild: opts.allowBuild,
depGraph,
depsStateCache: opts.depsStateCache,
disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
force: opts.force,
ignoreScripts: opts.ignoreScripts,
lockfileDir: opts.lockfileDir,
sideEffectsCacheRead: opts.sideEffectsCacheRead,
}),
]);
return { newDepPaths, added };
}
async function selectNewFromWantedDeps(wantedRelDepPaths, currentLockfile, depGraph) {
const newDeps = new Set();
const prevDeps = currentLockfile.packages ?? {};
await Promise.all(wantedRelDepPaths.map(async (depPath) => {
const depNode = depGraph[depPath];
if (!depNode)
return;
const prevDep = prevDeps[depPath];
if (prevDep &&
// Local file should always be treated as a new dependency
// https://github.com/pnpm/pnpm/issues/5381
depNode.resolution.type !== 'directory' &&
depNode.resolution.integrity === prevDep.resolution.integrity) {
if (await (0, path_exists_1.default)(depNode.dir)) {
return;
}
brokenModulesLogger.debug({
missing: depNode.dir,
});
}
newDeps.add(depPath);
}));
return newDeps;
}
const limitLinking = (0, p_limit_1.default)(16);
async function linkAllPkgs(storeController, depNodes, opts) {
await Promise.all(depNodes.map(async (depNode) => {
const { files } = await depNode.fetching();
depNode.requiresBuild = files.requiresBuild;
let sideEffectsCacheKey;
if (opts.sideEffectsCacheRead && files.sideEffects && !(0, isEmpty_1.default)(files.sideEffects)) {
if (opts?.allowBuild?.(depNode.name) !== false) {
sideEffectsCacheKey = (0, calc_dep_state_1.calcDepState)(opts.depGraph, opts.depsStateCache, depNode.depPath, {
isBuilt: !opts.ignoreScripts && depNode.requiresBuild,
patchFileHash: depNode.patch?.file.hash,
});
}
}
const { importMethod, isBuilt } = await storeController.importPackage(depNode.dir, {
disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
filesResponse: files,
force: opts.force,
sideEffectsCacheKey,
requiresBuild: depNode.patch != null || depNode.requiresBuild,
});
if (importMethod) {
core_loggers_1.progressLogger.debug({
method: importMethod,
requester: opts.lockfileDir,
status: 'imported',
to: depNode.dir,
});
}
depNode.isBuilt = isBuilt;
const selfDep = depNode.children[depNode.name];
if (selfDep) {
const pkg = opts.depGraph[selfDep];
if (!pkg || !pkg.installable && pkg.optional)
return;
const targetModulesDir = path_1.default.join(depNode.modules, depNode.name, 'node_modules');
await limitLinking(async () => (0, symlink_dependency_1.symlinkDependency)(pkg.dir, targetModulesDir, depNode.name));
}
}));
}
async function linkAllModules(depNodes, depGraph, opts) {
await (0, worker_1.symlinkAllModules)({
deps: depNodes.map((depNode) => {
const children = opts.optional
? depNode.children
: (0, pickBy_1.default)((_, childAlias) => !depNode.optionalDependencies.has(childAlias), depNode.children);
const childrenPaths = {};
for (const [alias, childDepPath] of Object.entries(children ?? {})) {
if (childDepPath.startsWith('link:')) {
childrenPaths[alias] = path_1.default.resolve(opts.lockfileDir, childDepPath.slice(5));
}
else {
const pkg = depGraph[childDepPath];
if (!pkg || !pkg.installable && pkg.optional || alias === depNode.name)
continue;
childrenPaths[alias] = pkg.dir;
}
}
return {
children: childrenPaths,
modules: depNode.modules,
name: depNode.name,
};
}),
});
}
//# sourceMappingURL=link.js.map