From a1ae9016c4f46322ca04228fba066c5b27fc4e4e Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Thu, 14 Jun 2018 14:44:41 +0300 Subject: [PATCH] feat: filtering packages in recursive commands (#1219) * feat: filtering packages in recursive commands close #1213 * test: recursive --scope ignore excluded packages * test: recursive list --scope PR #1219 --- packages/config/src/index.ts | 1 + packages/pnpm/package.json | 2 + packages/pnpm/shrinkwrap.yaml | 4 + packages/pnpm/src/cmd/help.ts | 4 + packages/pnpm/src/cmd/recursive/index.ts | 41 +++++++- packages/pnpm/src/types.ts | 1 + packages/pnpm/test/recursive.ts | 116 +++++++++++++++++++++++ 7 files changed, 167 insertions(+), 2 deletions(-) diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 54e6d9486e..d3ed44140d 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -35,6 +35,7 @@ export const types = Object.assign(npmTypes.types, { 'production': [null, true], 'protocol': ['auto', 'tcp', 'ipc'], 'reporter': String, + 'scope': String, 'shamefully-flatten': Boolean, 'shrinkwrap-only': Boolean, 'side-effects-cache': Boolean, diff --git a/packages/pnpm/package.json b/packages/pnpm/package.json index abd8d96506..6b49fa218f 100644 --- a/packages/pnpm/package.json +++ b/packages/pnpm/package.json @@ -33,6 +33,7 @@ "@types/camelcase-keys": "^4.0.0", "@types/get-port": "^3.2.0", "@types/lru-cache": "^4.1.1", + "@types/minimatch": "^3.0.3", "@types/mz": "^0.0.32", "@zkochan/libnpx": "^9.6.1", "camelcase": "^4.1.0 || 5", @@ -52,6 +53,7 @@ "load-yaml-file": "^0.1.0", "loud-rejection": "^1.6.0", "lru-cache": "^4.1.3", + "minimatch": "^3.0.4", "mkdirp-promise": "^5.0.1", "mz": "^2.7.0", "nopt": "^4.0.1", diff --git a/packages/pnpm/shrinkwrap.yaml b/packages/pnpm/shrinkwrap.yaml index bcf049b3ee..5fe269f629 100644 --- a/packages/pnpm/shrinkwrap.yaml +++ b/packages/pnpm/shrinkwrap.yaml @@ -12,6 +12,7 @@ dependencies: '@types/camelcase-keys': 4.0.0 '@types/get-port': 3.2.0 '@types/lru-cache': 4.1.1 + '@types/minimatch': 3.0.3 '@types/mz': 0.0.32 '@zkochan/libnpx': 9.6.1 camelcase: 5.0.0 @@ -31,6 +32,7 @@ dependencies: load-yaml-file: 0.1.0 loud-rejection: 1.6.0 lru-cache: 4.1.3 + minimatch: 3.0.4 mkdirp-promise: 5.0.1 mz: 2.7.0 nopt: 4.0.1 @@ -6457,6 +6459,7 @@ specifiers: '@types/get-port': ^3.2.0 '@types/load-json-file': ^2.0.6 '@types/lru-cache': ^4.1.1 + '@types/minimatch': ^3.0.3 '@types/mkdirp': ^0.5.0 '@types/mz': ^0.0.32 '@types/node': ^10.0.6 @@ -6492,6 +6495,7 @@ specifiers: load-yaml-file: ^0.1.0 loud-rejection: ^1.6.0 lru-cache: ^4.1.3 + minimatch: ^3.0.4 mkdirp: ^0.5.1 mkdirp-promise: ^5.0.1 mz: ^2.7.0 diff --git a/packages/pnpm/src/cmd/help.ts b/packages/pnpm/src/cmd/help.ts index 21a15a064d..86c4575ccf 100644 --- a/packages/pnpm/src/cmd/help.ts +++ b/packages/pnpm/src/cmd/help.ts @@ -304,6 +304,10 @@ function getHelpText (command: string) { This command runs the "npm build" command on each package. This is useful when you install a new version of node, and must recompile all your C++ addons with the new binary. + + Options: + + --scope restricts the scope to package names matching the given glob. ` default: diff --git a/packages/pnpm/src/cmd/recursive/index.ts b/packages/pnpm/src/cmd/recursive/index.ts index 89ec04fb62..f20f5b8d57 100644 --- a/packages/pnpm/src/cmd/recursive/index.ts +++ b/packages/pnpm/src/cmd/recursive/index.ts @@ -3,10 +3,12 @@ import camelcaseKeys = require('camelcase-keys') import findPackages from 'find-packages' import graphSequencer = require('graph-sequencer') import loadYamlFile = require('load-yaml-file') +import minimatch = require('minimatch') import pLimit = require('p-limit') import { StoreController } from 'package-store' import path = require('path') import createPkgGraph, {PackageNode} from 'pkgs-graph' +import R = require('ramda') import readIniFile = require('read-ini-file') import sortPkgs = require('sort-pkgs') import { @@ -64,7 +66,7 @@ export default async ( const cwd = process.cwd() const packagesManifest = await requirePackagesManifest(cwd) - const pkgs = await findPackages(cwd, { + let pkgs = await findPackages(cwd, { ignore: [ '**/node_modules/**', '**/bower_components/**', @@ -72,6 +74,12 @@ export default async ( patterns: packagesManifest && packagesManifest.packages || undefined, }) + const pkgGraphResult = createPkgGraph(pkgs) + if (opts.scope) { + pkgGraphResult.graph = filterGraph(pkgGraphResult.graph, opts.scope) + pkgs = pkgs.filter((pkg: {path: string}) => pkgGraphResult.graph[pkg.path]) + } + switch (cmdFullName) { case 'list': await list(pkgs, input, cmd, opts as any) // tslint:disable-line:no-any @@ -88,7 +96,6 @@ export default async ( break } - const pkgGraphResult = createPkgGraph(pkgs) const store = await createStoreController(opts) // It is enough to save the store.json file once, @@ -208,3 +215,33 @@ async function requirePackagesManifest (dir: string): Promise<{packages: string[ throw err } } + +interface PackageGraph { + [id: string]: PackageNode, +} + +function filterGraph ( + graph: PackageGraph, + scope: string, +): PackageGraph { + const root = R.keys(graph).filter((id) => graph[id].manifest.name && minimatch(graph[id].manifest.name, scope)) + if (!root.length) return {} + + const subgraphNodeIds = new Set() + pickSubgraph(graph, root, subgraphNodeIds) + + return R.pick(Array.from(subgraphNodeIds), graph) +} + +function pickSubgraph ( + graph: PackageGraph, + nextNodeIds: string[], + walked: Set, +) { + for (const nextNodeId of nextNodeIds) { + if (!walked.has(nextNodeId)) { + walked.add(nextNodeId) + pickSubgraph(graph, graph[nextNodeId].dependencies, walked) + } + } +} diff --git a/packages/pnpm/src/types.ts b/packages/pnpm/src/types.ts index f10d375f64..9ac6631dc8 100644 --- a/packages/pnpm/src/types.ts +++ b/packages/pnpm/src/types.ts @@ -18,6 +18,7 @@ export interface PnpmOptions { saveProd?: boolean, saveDev?: boolean, saveOptional?: boolean, + scope: string, production?: boolean, development?: boolean, fetchRetries?: number, diff --git a/packages/pnpm/test/recursive.ts b/packages/pnpm/test/recursive.ts index 63758e55f7..4a79d3fffd 100644 --- a/packages/pnpm/test/recursive.ts +++ b/packages/pnpm/test/recursive.ts @@ -440,6 +440,49 @@ test('recursive list', async (t: tape.Test) => { ` + '\n\n') }) +test('recursive list --scope', async (t: tape.Test) => { + const projects = prepare(t, [ + { + name: 'project-1', + version: '1.0.0', + dependencies: { + 'is-positive': '1.0.0', + 'project-2': '1.0.0', + }, + }, + { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + }, + }, + { + name: 'project-3', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + 'is-positive': '1.0.0', + }, + }, + ]) + + await execPnpm('recursive', 'link') + + const result = execPnpmSync('recursive', 'list', '--scope', 'project-1') + + t.equal(result.status, 0) + + t.equal(result.stdout.toString(), stripIndent` + project-1@1.0.0 ${path.resolve('project-1')} + ├── is-positive@1.0.0 + └── project-2@link:../project-2 + + project-2@1.0.0 ${path.resolve('project-2')} + └── is-negative@1.0.0 + ` + '\n\n') +}) + test('pnpm recursive outdated', async (t: tape.Test) => { const projects = prepare(t, [ { @@ -771,3 +814,76 @@ test('`pnpm recursive rebuild` specific dependencies', async (t: tape.Test) => { t.ok(typeof generatedByPostinstall === 'function', 'generatedByPostinstall() is available') } }) + +test('recursive --scope', async (t: tape.Test) => { + const projects = prepare(t, [ + { + name: 'project-1', + version: '1.0.0', + dependencies: { + 'is-positive': '1.0.0', + 'project-2': '1.0.0', + }, + }, + { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + }, + }, + { + name: 'project-3', + version: '1.0.0', + dependencies: { + minimatch: '*', + }, + }, + ]) + + await execPnpm('recursive', 'link', '--scope', 'project-1') + + projects['project-1'].has('is-positive') + projects['project-2'].has('is-negative') + projects['project-3'].hasNot('minimatch') +}) + +test('recursive --scope ignore excluded packages', async (t: tape.Test) => { + const projects = prepare(t, [ + { + name: 'project-1', + version: '1.0.0', + dependencies: { + 'is-positive': '1.0.0', + 'project-2': '1.0.0', + }, + }, + { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + }, + }, + { + name: 'project-3', + version: '1.0.0', + dependencies: { + minimatch: '*', + }, + }, + ]) + + await writeYamlFile('pnpm-workspace.yaml', { + packages: [ + '**', + '!project-1' + ], + }) + + await execPnpm('recursive', 'link', '--scope', 'project-1') + + projects['project-1'].hasNot('is-positive') + projects['project-2'].hasNot('is-negative') + projects['project-3'].hasNot('minimatch') +})