UNPKG

pnpm

Version:

Fast, disk space efficient package manager

437 lines (423 loc) • 18.7 kB
"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