pnpm
Version:
Fast, disk space efficient package manager
437 lines (423 loc) • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const logger_1 = require("@pnpm/logger");
const utils_1 = require("@pnpm/utils");
const camelcaseKeys = require("camelcase-keys");
const graphSequencer = require("graph-sequencer");
const isSubdir = require("is-subdir");
const mem_1 = require("mem");
const fs = require("mz/fs");
const pFilter = require("p-filter");
const pLimit = require("p-limit");
const path = require("path");
const pkgs_graph_1 = require("pkgs-graph");
const R = require("ramda");
const readIniFile = require("read-ini-file");
const supi_1 = require("supi");
const writePkg = require("write-pkg");
const createStoreController_1 = require("../../createStoreController");
const findWorkspacePackages_1 = require("../../findWorkspacePackages");
const getCommandFullName_1 = require("../../getCommandFullName");
const getPinnedVersion_1 = require("../../getPinnedVersion");
const loggers_1 = require("../../loggers");
const parsePackageSelectors_1 = require("../../parsePackageSelectors");
const readImporterManifest_1 = require("../../readImporterManifest");
const requireHooks_1 = require("../../requireHooks");
const updateToLatestSpecsFromManifest_1 = require("../../updateToLatestSpecsFromManifest");
const help_1 = require("../help");
const exec_1 = require("./exec");
const filter_1 = require("./filter");
const list_1 = require("./list");
const outdated_1 = require("./outdated");
const recursiveSummary_1 = require("./recursiveSummary");
const run_1 = require("./run");
const supportedRecursiveCommands = new Set([
'install',
'uninstall',
'update',
'unlink',
'list',
'outdated',
'rebuild',
'run',
'test',
'exec',
]);
exports.default = async (input, opts) => {
if (opts.workspaceConcurrency < 1) {
const err = new Error('Workspace concurrency should be at least 1');
err['code'] = 'ERR_PNPM_INVALID_WORKSPACE_CONCURRENCY'; // tslint:disable-line:no-string-literal
throw err;
}
const cmd = input.shift();
if (!cmd) {
help_1.default(['recursive']);
return;
}
const cmdFullName = getCommandFullName_1.default(cmd);
if (!supportedRecursiveCommands.has(cmdFullName)) {
help_1.default(['recursive']);
const err = new Error(`"recursive ${cmdFullName}" is not a pnpm command. See "pnpm help recursive".`);
err['code'] = 'ERR_PNPM_INVALID_RECURSIVE_COMMAND'; // tslint:disable-line:no-string-literal
throw err;
}
const workspacePrefix = opts.workspacePrefix || process.cwd();
const allWorkspacePkgs = await findWorkspacePackages_1.default(workspacePrefix);
if (!allWorkspacePkgs.length) {
logger_1.default.info({ message: `No packages found in "${workspacePrefix}"`, prefix: workspacePrefix });
return;
}
if (opts.filter) {
// TODO: maybe @pnpm/config should return this in a parsed form already?
// We don't use opts.prefix in this case because opts.prefix searches for a package.json in parent directories and
// selects the directory where it finds one
opts['packageSelectors'] = opts.filter.map((f) => parsePackageSelectors_1.default(f, process.cwd())); // tslint:disable-line
}
const atLeastOnePackageMatched = await recursive(allWorkspacePkgs, input, opts, cmdFullName, cmd);
if (!atLeastOnePackageMatched) {
logger_1.default.info({ message: `No packages matched the filters in "${workspacePrefix}"`, prefix: workspacePrefix });
return;
}
};
async function recursive(allPkgs, input, opts, cmdFullName, cmd) {
if (allPkgs.length === 0) {
// It might make sense to throw an exception in this case
return false;
}
const pkgGraphResult = pkgs_graph_1.default(allPkgs);
let pkgs;
if (opts.packageSelectors && opts.packageSelectors.length) {
pkgGraphResult.graph = filter_1.filterGraph(pkgGraphResult.graph, opts.packageSelectors);
pkgs = allPkgs.filter((pkg) => pkgGraphResult.graph[pkg.path]);
}
else {
pkgs = allPkgs;
}
if (pkgs.length === 0) {
return false;
}
loggers_1.scopeLogger.debug({
selected: pkgs.length,
total: allPkgs.length,
workspacePrefix: opts.workspacePrefix,
});
const throwOnFail = recursiveSummary_1.throwOnCommandFail.bind(null, `pnpm recursive ${cmd}`);
switch (cmdFullName) {
case 'list':
await list_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
return true;
case 'outdated':
await outdated_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
return true;
}
const chunks = opts.sort
? sortPackages(pkgGraphResult.graph)
: [Object.keys(pkgGraphResult.graph).sort()];
switch (cmdFullName) {
case 'test':
throwOnFail(await run_1.default(chunks, pkgGraphResult.graph, ['test', ...input], cmd, opts)); // tslint:disable-line:no-any
return true;
case 'run':
throwOnFail(await run_1.default(chunks, pkgGraphResult.graph, input, cmd, opts)); // tslint:disable-line:no-any
return true;
case 'update':
opts = Object.assign({}, opts, { update: true, allowNew: false }); // tslint:disable-line:no-any
break;
case 'exec':
throwOnFail(await exec_1.default(chunks, pkgGraphResult.graph, input, cmd, opts)); // tslint:disable-line:no-any
return true;
}
const store = await createStoreController_1.default(opts);
// It is enough to save the store.json file once,
// once all installations are done.
// That's why saveState that is passed to the install engine
// does nothing.
const saveState = store.ctrl.saveState;
const storeController = Object.assign({}, store.ctrl, { saveState: async () => undefined });
const localPackages = opts.linkWorkspacePackages && cmdFullName !== 'unlink'
? findWorkspacePackages_1.arrayOfLocalPackagesToMap(allPkgs)
: {};
const installOpts = Object.assign(opts, {
localPackages,
ownLifecycleHooksStdio: 'pipe',
peer: opts.savePeer,
pruneLockfileImporters: (!opts.ignoredPackages || opts.ignoredPackages.size === 0)
&& pkgs.length === allPkgs.length,
store: store.path,
storeController,
targetDependenciesField: utils_1.getSaveType(opts),
});
const result = {
fails: [],
passes: 0,
};
const memReadLocalConfigs = mem_1.default(readLocalConfigs);
async function getImporters() {
const importers = [];
await Promise.all(chunks.map((prefixes, buildIndex) => {
if (opts.ignoredPackages) {
prefixes = prefixes.filter((prefix) => !opts.ignoredPackages.has(prefix));
}
return Promise.all(prefixes.map(async (prefix) => {
importers.push({
buildIndex,
manifest: await readImporterManifest_1.readImporterManifestFromDir(prefix),
prefix,
});
}));
}));
return importers;
}
const updateToLatest = opts.update && opts.latest;
const include = opts.include;
if (updateToLatest) {
delete opts.include;
}
if (cmdFullName !== 'rebuild') {
if (opts.lockfileDirectory && ['install', 'uninstall', 'update'].includes(cmdFullName)) {
let importers = await getImporters();
const isFromWorkspace = isSubdir.bind(null, opts.lockfileDirectory);
importers = await pFilter(importers, async ({ prefix }) => isFromWorkspace(await fs.realpath(prefix)));
if (importers.length === 0)
return true;
const hooks = opts.ignorePnpmfile ? {} : requireHooks_1.default(opts.lockfileDirectory, opts);
const mutation = cmdFullName === 'uninstall' ? 'uninstallSome' : (input.length === 0 && !updateToLatest ? 'install' : 'installSome');
const mutatedImporters = await Promise.all(importers.map(async ({ buildIndex, prefix }) => {
const localConfigs = await memReadLocalConfigs(prefix);
const manifest = await readImporterManifest_1.readImporterManifestFromDir(prefix);
const shamefullyFlatten = typeof localConfigs.shamefullyFlatten === 'boolean'
? localConfigs.shamefullyFlatten
: opts.shamefullyFlatten;
let currentInput = [...input];
if (updateToLatest) {
if (!currentInput || !currentInput.length) {
currentInput = updateToLatestSpecsFromManifest_1.default(manifest, include);
}
else {
currentInput = updateToLatestSpecsFromManifest_1.createLatestSpecs(currentInput, manifest);
}
}
switch (mutation) {
case 'uninstallSome':
return {
dependencyNames: currentInput,
manifest,
mutation,
prefix,
shamefullyFlatten,
targetDependenciesField: utils_1.getSaveType(installOpts),
};
case 'installSome':
return {
allowNew: cmdFullName === 'install',
dependencySelectors: currentInput,
manifest,
mutation,
peer: opts.savePeer,
pinnedVersion: getPinnedVersion_1.default({
saveExact: typeof localConfigs.saveExact === 'boolean' ? localConfigs.saveExact : opts.saveExact,
savePrefix: typeof localConfigs.savePrefix === 'string' ? localConfigs.savePrefix : opts.savePrefix,
}),
prefix,
shamefullyFlatten,
targetDependenciesField: utils_1.getSaveType(installOpts),
};
case 'install':
return {
buildIndex,
manifest,
mutation,
prefix,
shamefullyFlatten,
};
}
}));
const mutatedPkgs = await supi_1.mutateModules(mutatedImporters, Object.assign({}, installOpts, { hooks, storeController: store.ctrl }));
await Promise.all(mutatedPkgs
.filter((mutatedPkg, index) => mutatedImporters[index].mutation !== 'install')
.map(({ manifest, prefix }) => writePkg(prefix, manifest)));
return true;
}
let pkgPaths = chunks.length === 0
? chunks[0]
: Object.keys(pkgGraphResult.graph).sort();
const limitInstallation = pLimit(opts.workspaceConcurrency);
await Promise.all(pkgPaths.map((prefix) => limitInstallation(async () => {
const hooks = opts.ignorePnpmfile ? {} : requireHooks_1.default(prefix, opts);
try {
if (opts.ignoredPackages && opts.ignoredPackages.has(prefix)) {
return;
}
const manifest = await readImporterManifest_1.readImporterManifestFromDir(prefix);
let currentInput = [...input];
if (updateToLatest) {
if (!currentInput || !currentInput.length) {
currentInput = updateToLatestSpecsFromManifest_1.default(manifest, include);
}
else {
currentInput = updateToLatestSpecsFromManifest_1.createLatestSpecs(currentInput, manifest);
}
}
let action; // tslint:disable-line:no-any
switch (cmdFullName) {
case 'unlink':
action = (currentInput.length === 0 ? unlink : unlinkPkgs.bind(null, currentInput));
break;
case 'uninstall':
action = R.flip(supi_1.uninstall).bind(null, currentInput);
break;
default:
action = currentInput.length === 0 ? supi_1.install : R.flip(supi_1.addDependenciesToPackage).bind(null, currentInput);
break;
}
const localConfigs = await memReadLocalConfigs(prefix);
const newPkg = await action(manifest, Object.assign({}, installOpts, localConfigs, { bin: path.join(prefix, 'node_modules', '.bin'), hooks, ignoreScripts: true, prefix, rawNpmConfig: Object.assign({}, installOpts.rawNpmConfig, localConfigs), storeController }));
if (action !== supi_1.install) {
await writePkg(prefix, newPkg);
}
result.passes++;
}
catch (err) {
logger_1.default.info(err);
if (!opts.bail) {
result.fails.push({
error: err,
message: err.message,
prefix,
});
return;
}
err['prefix'] = prefix; // tslint:disable-line:no-string-literal
throw err;
}
})));
await saveState();
}
if (cmdFullName === 'rebuild' ||
!opts.lockfileOnly && !opts.ignoreScripts && (cmdFullName === 'install' || cmdFullName === 'update' || cmdFullName === 'unlink')) {
const action = (cmdFullName !== 'rebuild' || input.length === 0
? supi_1.rebuild
: (importers, opts) => supi_1.rebuildPkgs(importers, input, opts) // tslint:disable-line
);
if (opts.lockfileDirectory) {
const importers = await getImporters();
await action(importers, Object.assign({}, installOpts, { pending: cmdFullName !== 'rebuild' || opts.pending === true }));
return true;
}
const limitRebuild = pLimit(opts.workspaceConcurrency);
for (const chunk of chunks) {
await Promise.all(chunk.map((prefix) => limitRebuild(async () => {
try {
if (opts.ignoredPackages && opts.ignoredPackages.has(prefix)) {
return;
}
const localConfigs = await memReadLocalConfigs(prefix);
await action([
{
buildIndex: 0,
manifest: await readImporterManifest_1.readImporterManifestFromDir(prefix),
prefix,
},
], Object.assign({}, installOpts, localConfigs, { bin: path.join(prefix, 'node_modules', '.bin'), pending: cmdFullName !== 'rebuild' || opts.pending === true, prefix, rawNpmConfig: Object.assign({}, installOpts.rawNpmConfig, localConfigs) }));
result.passes++;
}
catch (err) {
logger_1.default.info(err);
if (!opts.bail) {
result.fails.push({
error: err,
message: err.message,
prefix,
});
return;
}
err['prefix'] = prefix; // tslint:disable-line:no-string-literal
throw err;
}
})));
}
}
throwOnFail(result);
return true;
}
exports.recursive = recursive;
async function unlink(manifest, opts) {
return supi_1.mutateModules([
{
manifest,
mutation: 'unlink',
prefix: opts.prefix,
},
], opts);
}
async function unlinkPkgs(dependencyNames, manifest, opts) {
return supi_1.mutateModules([
{
dependencyNames,
manifest,
mutation: 'unlinkSome',
prefix: opts.prefix,
},
], opts);
}
function sortPackages(pkgGraph) {
const keys = Object.keys(pkgGraph);
const setOfKeys = new Set(keys);
const graph = new Map(keys.map((pkgPath) => [
pkgPath,
pkgGraph[pkgPath].dependencies.filter(
/* remove cycles of length 1 (ie., package 'a' depends on 'a'). They
confuse the graph-sequencer, but can be ignored when ordering packages
topologically.
See the following example where 'b' and 'c' depend on themselves:
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['b', ['b']],
['c', ['b', 'c']]]
),
groups: [['a', 'b', 'c']]})
returns chunks:
[['b'],['a'],['c']]
But both 'b' and 'c' should be executed _before_ 'a', because 'a' depends on
them. It works (and is considered 'safe' if we run:)
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['b', []],
['c', ['b']]]
), groups: [['a', 'b', 'c']]})
returning:
[['b'], ['c'], ['a']]
*/
d => d !== pkgPath &&
/* remove unused dependencies that we can ignore due to a filter expression.
Again, the graph sequencer used to behave weirdly in the following edge case:
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['d', ['a']],
['e', ['a', 'b', 'c']]]
),
groups: [['a', 'e', 'e']]})
returns chunks:
[['d'],['a'],['e']]
But we really want 'a' to be executed first.
*/
setOfKeys.has(d))
]));
const graphSequencerResult = graphSequencer({
graph,
groups: [keys],
});
return graphSequencerResult.chunks;
}
async function readLocalConfigs(prefix) {
try {
const ini = await readIniFile(path.join(prefix, '.npmrc'));
return camelcaseKeys(ini);
}
catch (err) {
if (err.code !== 'ENOENT')
throw err;
return {};
}
}
//# sourceMappingURL=index.js.map