From abb41a62632535afecd241e1aae9892a4fae26d7 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 30 Sep 2022 17:52:33 +0300 Subject: [PATCH] feat: excluding deps from update (#5432) ref #5408 --- .changeset/bright-chefs-drop.md | 22 +++++++ .changeset/eleven-cups-provide.md | 5 ++ packages/matcher/src/index.ts | 66 ++++++++++++++----- packages/matcher/test/index.ts | 54 ++++++++++++++- .../src/recursive.ts | 32 ++++----- .../test/update/update.ts | 33 ++++++++++ 6 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 .changeset/bright-chefs-drop.md create mode 100644 .changeset/eleven-cups-provide.md diff --git a/.changeset/bright-chefs-drop.md b/.changeset/bright-chefs-drop.md new file mode 100644 index 00000000000..9c3f1bfd706 --- /dev/null +++ b/.changeset/bright-chefs-drop.md @@ -0,0 +1,22 @@ +--- +"@pnpm/plugin-commands-installation": minor +"pnpm": minor +--- + +It is possible now to update all dependencies except the listed ones using `!`. For instance, update all dependencies, except `lodash`: + +``` +pnpm update !lodash +``` + +It also works with pattends, for instance: + +``` +pnpm update !@babel/* +``` + +And it may be combined with other patterns: + +``` +pnpm update @babel/* !@babel/core +``` diff --git a/.changeset/eleven-cups-provide.md b/.changeset/eleven-cups-provide.md new file mode 100644 index 00000000000..9334d43bdfc --- /dev/null +++ b/.changeset/eleven-cups-provide.md @@ -0,0 +1,5 @@ +--- +"@pnpm/matcher": minor +--- + +New function added that returns the number of the matched pattern: matcherWithIndex(). diff --git a/packages/matcher/src/index.ts b/packages/matcher/src/index.ts index f3c5bca5cbf..f185a968db7 100644 --- a/packages/matcher/src/index.ts +++ b/packages/matcher/src/index.ts @@ -1,14 +1,24 @@ import escapeStringRegexp from 'escape-string-regexp' type Matcher = (input: string) => boolean +type MatcherWithIndex = (input: string) => number export default function matcher (patterns: string[] | string): Matcher { - if (typeof patterns === 'string') return matcherWhenOnlyOnePattern(patterns) + const m = matcherWithIndex(Array.isArray(patterns) ? patterns : [patterns]) + return (input) => m(input) !== -1 +} + +interface MatcherFunction { + match: Matcher + ignore: boolean +} + +export function matcherWithIndex (patterns: string[]): MatcherWithIndex { switch (patterns.length) { - case 0: return () => false - case 1: return matcherWhenOnlyOnePattern(patterns[0]) + case 0: return () => -1 + case 1: return matcherWhenOnlyOnePatternWithIndex(patterns[0]) } - const matchArr: Array<{ match: Matcher, ignore: boolean }> = [] + const matchArr: MatcherFunction[] = [] let hasIgnore = false for (const pattern of patterns) { if (isIgnorePattern(pattern)) { @@ -19,19 +29,33 @@ export default function matcher (patterns: string[] | string): Matcher { } } if (!hasIgnore) { - return (input: string) => matchArr.some(({ match }) => match(input)) - } - return (input: string) => { - let isMatched = false - for (const { ignore, match } of matchArr) { - if (ignore) { - isMatched = !match(input) - } else if (!isMatched && match(input)) { - isMatched = true + return matchInputWithNonIgnoreMatchers.bind(null, matchArr) + } + return matchInputWithMatchersArray.bind(null, matchArr) +} + +function matchInputWithNonIgnoreMatchers (matchArr: MatcherFunction[], input: string): number { + for (let i = 0; i < matchArr.length; i++) { + if (matchArr[i].match(input)) return i + } + return -1 +} + +function matchInputWithMatchersArray (matchArr: MatcherFunction[], input: string): number { + let matchedPatternIndex = -1 + for (let i = 0; i < matchArr.length; i++) { + const { ignore, match } = matchArr[i] + if (ignore) { + if (match(input)) { + matchedPatternIndex = -1 + } else if (matchedPatternIndex === -1) { + matchedPatternIndex = i } + } else if (matchedPatternIndex === -1 && match(input)) { + matchedPatternIndex = i } - return isMatched } + return matchedPatternIndex } function matcherFromPattern (pattern: string): Matcher { @@ -52,8 +76,16 @@ function isIgnorePattern (pattern: string): boolean { return pattern.startsWith('!') } +function matcherWhenOnlyOnePatternWithIndex (pattern: string): MatcherWithIndex { + const m = matcherWhenOnlyOnePattern(pattern) + return (input) => m(input) ? 0 : -1 +} + function matcherWhenOnlyOnePattern (pattern: string): Matcher { - return isIgnorePattern(pattern) - ? () => false - : matcherFromPattern(pattern) + if (!isIgnorePattern(pattern)) { + return matcherFromPattern(pattern) + } + const ignorePattern = pattern.substring(1) + const m = matcherFromPattern(ignorePattern) + return (input) => !m(input) } diff --git a/packages/matcher/test/index.ts b/packages/matcher/test/index.ts index 8547c300cf9..14dd91b5591 100644 --- a/packages/matcher/test/index.ts +++ b/packages/matcher/test/index.ts @@ -1,4 +1,4 @@ -import matcher from '@pnpm/matcher' +import matcher, { matcherWithIndex } from '@pnpm/matcher' test('matcher()', () => { { @@ -47,3 +47,55 @@ test('matcher()', () => { expect(match('eslint-plugin-bar')).toBe(true) } }) + +test('matcherWithIndex()', () => { + { + const match = matcherWithIndex(['*']) + expect(match('@eslint/plugin-foo')).toBe(0) + expect(match('express')).toBe(0) + } + { + const match = matcherWithIndex(['eslint-*']) + expect(match('eslint-plugin-foo')).toBe(0) + expect(match('express')).toBe(-1) + } + { + const match = matcherWithIndex(['*plugin*']) + expect(match('@eslint/plugin-foo')).toBe(0) + expect(match('express')).toBe(-1) + } + { + const match = matcherWithIndex(['a*c']) + expect(match('abc')).toBe(0) + } + { + const match = matcherWithIndex(['*-positive']) + expect(match('is-positive')).toBe(0) + } + { + const match = matcherWithIndex(['foo', 'bar']) + expect(match('foo')).toBe(0) + expect(match('bar')).toBe(1) + expect(match('express')).toBe(-1) + } + { + const match = matcherWithIndex(['eslint-*', '!eslint-plugin-bar']) + expect(match('eslint-plugin-foo')).toBe(0) + expect(match('eslint-plugin-bar')).toBe(-1) + } + { + const match = matcherWithIndex(['!eslint-plugin-bar', 'eslint-*']) + expect(match('eslint-plugin-foo')).toBe(0) + expect(match('eslint-plugin-bar')).toBe(1) + } + { + const match = matcherWithIndex(['eslint-*', '!eslint-plugin-*', 'eslint-plugin-bar']) + expect(match('eslint-config-foo')).toBe(0) + expect(match('eslint-plugin-foo')).toBe(-1) + expect(match('eslint-plugin-bar')).toBe(2) + } + { + const match = matcherWithIndex(['!@pnpm.e2e/peer-*']) + expect(match('@pnpm.e2e/foo')).toBe(0) + } +}) diff --git a/packages/plugin-commands-installation/src/recursive.ts b/packages/plugin-commands-installation/src/recursive.ts index 80fca5d9e74..496ed7445ea 100755 --- a/packages/plugin-commands-installation/src/recursive.ts +++ b/packages/plugin-commands-installation/src/recursive.ts @@ -9,7 +9,7 @@ import PnpmError from '@pnpm/error' import { arrayOfWorkspacePackagesToMap } from '@pnpm/find-workspace-packages' import logger from '@pnpm/logger' import { filterDependenciesByType } from '@pnpm/manifest-utils' -import matcher from '@pnpm/matcher' +import { matcherWithIndex } from '@pnpm/matcher' import { rebuild } from '@pnpm/plugin-commands-rebuild' import { requireHooks } from '@pnpm/pnpmfile' import sortPackages from '@pnpm/sort-packages' @@ -500,26 +500,22 @@ export function matchDependencies ( } export function createMatcher (params: string[]) { - const matchers = params.map((param) => { - const atIndex = param.indexOf('@', 1) - let pattern!: string - let spec!: string + const patterns: string[] = [] + const specs: string[] = [] + for (const param of params) { + const atIndex = param.indexOf('@', param[0] === '!' ? 2 : 1) if (atIndex === -1) { - pattern = param - spec = '' + patterns.push(param) + specs.push('') } else { - pattern = param.slice(0, atIndex) - spec = param.slice(atIndex + 1) + patterns.push(param.slice(0, atIndex)) + specs.push(param.slice(atIndex + 1)) } - return { - match: matcher(pattern), - spec, - } - }) + } + const matcher = matcherWithIndex(patterns) return (depName: string) => { - for (const { spec, match } of matchers) { - if (match(depName)) return spec - } - return null + const index = matcher(depName) + if (index === -1) return null + return specs[index] } } diff --git a/packages/plugin-commands-installation/test/update/update.ts b/packages/plugin-commands-installation/test/update/update.ts index 9937bdaa64e..14a05ab1234 100644 --- a/packages/plugin-commands-installation/test/update/update.ts +++ b/packages/plugin-commands-installation/test/update/update.ts @@ -40,6 +40,39 @@ test('update with "*" pattern', async () => { expect(lockfile.packages['/@pnpm.e2e/foo/1.0.0']).toBeTruthy() }) +test('update with negation pattern', async () => { + await addDistTag({ package: '@pnpm.e2e/peer-a', version: '1.0.1', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/peer-c', version: '2.0.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/foo', version: '2.0.0', distTag: 'latest' }) + + const project = prepare({ + dependencies: { + '@pnpm.e2e/peer-a': '1.0.0', + '@pnpm.e2e/peer-c': '1.0.0', + '@pnpm.e2e/foo': '1.0.0', + }, + }) + + await install.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + workspaceDir: process.cwd(), + }) + + await update.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + latest: true, + workspaceDir: process.cwd(), + }, ['!@pnpm.e2e/peer-*']) + + const lockfile = await project.readLockfile() + + expect(lockfile.packages['/@pnpm.e2e/peer-a/1.0.0']).toBeTruthy() + expect(lockfile.packages['/@pnpm.e2e/peer-c/1.0.0']).toBeTruthy() + expect(lockfile.packages['/@pnpm.e2e/foo/2.0.0']).toBeTruthy() +}) + test('update: fail when both "latest" and "workspace" are true', async () => { preparePackages([ {