diff --git a/packages/common-cli-options-help/src/index.ts b/packages/common-cli-options-help/src/index.ts index 6353b63de1..a497342b3e 100644 --- a/packages/common-cli-options-help/src/index.ts +++ b/packages/common-cli-options-help/src/index.ts @@ -51,10 +51,18 @@ export const FILTERING = { description: 'Includes all direct and indirect dependencies of the matched packages. E.g.: foo...', name: '--filter ...', }, + { + description: 'Includes only the direct and indirect dependencies of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: foo^... (foo^^... in Command Prompt)', + name: '--filter ^...', + }, { description: 'Includes all direct and indirect dependents of the matched packages. E.g.: ...foo, ...@bar/*', name: '--filter ...', }, + { + description: 'Includes only the direct and indirect dependents of the matched packages without including the matched packages themselves. ^ must be doubled at the Windows Command Prompt. E.g.: ...^foo (...^^foo in Command Prompt)', + name: '--filter ...^', + }, { description: 'Includes all packages that are inside a given subdirectory. E.g.: ./components', name: '--filter ./', diff --git a/packages/plugin-commands-recursive/src/filter.ts b/packages/plugin-commands-recursive/src/filter.ts index d185c4594f..7bbd5e6a0e 100644 --- a/packages/plugin-commands-recursive/src/filter.ts +++ b/packages/plugin-commands-recursive/src/filter.ts @@ -21,20 +21,20 @@ export function filterGraph ( const walkedDependents = new Set() const graph = pkgGraphToGraph(pkgGraph) let reversedGraph: Graph | undefined - for (const { pattern, scope, selectBy } of packageSelectors) { + for (const { excludeSelf, pattern, scope, selectBy } of packageSelectors) { const entryPackages = selectBy === 'name' ? matchPackages(pkgGraph, pattern) : matchPackagesByPath(pkgGraph, pattern) switch (scope) { case 'dependencies': - pickSubgraph(graph, entryPackages, walkedDependencies) + pickSubgraph(graph, entryPackages, walkedDependencies, { includeRoot: !excludeSelf }) continue case 'dependents': if (!reversedGraph) { reversedGraph = reverseGraph(graph) } - pickSubgraph(reversedGraph, entryPackages, walkedDependents) + pickSubgraph(reversedGraph, entryPackages, walkedDependents, { includeRoot: !excludeSelf }) continue case 'exact': Array.prototype.push.apply(cherryPickedPackages, entryPackages) @@ -87,11 +87,17 @@ function pickSubgraph ( graph: Graph, nextNodeIds: string[], walked: Set, + opts: { + includeRoot: boolean + }, ) { for (const nextNodeId of nextNodeIds) { if (!walked.has(nextNodeId)) { - walked.add(nextNodeId) - if (graph[nextNodeId]) pickSubgraph(graph, graph[nextNodeId], walked) + if (opts.includeRoot) { + walked.add(nextNodeId) + } + + if (graph[nextNodeId]) pickSubgraph(graph, graph[nextNodeId], walked, { includeRoot: true }) } } } diff --git a/packages/plugin-commands-recursive/src/parsePackageSelectors.ts b/packages/plugin-commands-recursive/src/parsePackageSelectors.ts index 3cb40a111a..101f82f32b 100644 --- a/packages/plugin-commands-recursive/src/parsePackageSelectors.ts +++ b/packages/plugin-commands-recursive/src/parsePackageSelectors.ts @@ -1,15 +1,35 @@ import path = require('path') export interface PackageSelector { + excludeSelf?: boolean, pattern: string, scope: 'exact' | 'dependencies' | 'dependents', selectBy: 'name' | 'location', } export default (rawSelector: string, prefix: string): PackageSelector => { + if (rawSelector.endsWith('^...')) { + const pattern = rawSelector.substring(0, rawSelector.length - 4) + return { + excludeSelf: true, + pattern, + scope: 'dependencies', + selectBy: 'name', + } + } + if (rawSelector.startsWith('...^')) { + const pattern = rawSelector.substring(4) + return { + excludeSelf: true, + pattern, + scope: 'dependents', + selectBy: 'name', + } + } if (rawSelector.endsWith('...')) { const pattern = rawSelector.substring(0, rawSelector.length - 3) return { + excludeSelf: false, pattern, scope: 'dependencies', selectBy: 'name', @@ -18,6 +38,7 @@ export default (rawSelector: string, prefix: string): PackageSelector => { if (rawSelector.startsWith('...')) { const pattern = rawSelector.substring(3) return { + excludeSelf: false, pattern, scope: 'dependents', selectBy: 'name', @@ -25,12 +46,14 @@ export default (rawSelector: string, prefix: string): PackageSelector => { } if (isSelectorByLocation(rawSelector)) { return { + excludeSelf: false, pattern: path.join(prefix, rawSelector), scope: 'exact', selectBy: 'location', } } return { + excludeSelf: false, pattern: rawSelector, scope: 'exact', selectBy: 'name', diff --git a/packages/plugin-commands-recursive/test/misc.ts b/packages/plugin-commands-recursive/test/misc.ts index 9aed5aecdd..b4e27d66d4 100644 --- a/packages/plugin-commands-recursive/test/misc.ts +++ b/packages/plugin-commands-recursive/test/misc.ts @@ -374,6 +374,58 @@ test('recursive filter package with dependencies', async (t) => { t.end() }) +test('recursive filter only package dependencies', async (t) => { + const projects = preparePackages(t, [ + { + name: 'project-1', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + 'project-2': '1.0.0', + 'project-4': '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: '*', + }, + }, + { + name: 'project-4', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + }, + }, + ]) + + await recursive.handler(['install'], { + ...DEFAULT_OPTS, + dir: process.cwd(), + filter: ['project-1^...'], + }) + + projects['project-1'].hasNot('is-positive') + projects['project-2'].has('is-negative') + projects['project-3'].hasNot('minimatch') + projects['project-4'].has('is-positive') + + t.end() +}) + test('recursive filter package with dependents', async (t) => { const projects = preparePackages(t, [ { @@ -425,6 +477,57 @@ test('recursive filter package with dependents', async (t) => { t.end() }) +test('recursive filter only package dependents', async (t) => { + const projects = preparePackages(t, [ + { + name: 'project-0', + version: '1.0.0', + + dependencies: { + 'is-positive': '1.0.0', + 'project-1': '1.0.0', + }, + }, + { + 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 recursive.handler(['install'], { + ...DEFAULT_OPTS, + dir: process.cwd(), + filter: ['...^project-2'], + }) + + projects['project-0'].has('is-positive') + projects['project-1'].has('is-positive') + projects['project-2'].hasNot('is-negative') + projects['project-3'].hasNot('minimatch') + t.end() +}) + test('recursive filter package with dependents and filter with dependencies', async (t) => { const projects = preparePackages(t, [ {