Skip to content

Commit

Permalink
feat: excluding deps from update (#5432)
Browse files Browse the repository at this point in the history
ref #5408
  • Loading branch information
zkochan committed Sep 30, 2022
1 parent 2bf89ec commit abb41a6
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 36 deletions.
22 changes: 22 additions & 0 deletions .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
```
5 changes: 5 additions & 0 deletions .changeset/eleven-cups-provide.md
@@ -0,0 +1,5 @@
---
"@pnpm/matcher": minor
---

New function added that returns the number of the matched pattern: matcherWithIndex().
66 changes: 49 additions & 17 deletions 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)) {
Expand All @@ -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 {
Expand All @@ -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)
}
54 changes: 53 additions & 1 deletion packages/matcher/test/index.ts
@@ -1,4 +1,4 @@
import matcher from '@pnpm/matcher'
import matcher, { matcherWithIndex } from '@pnpm/matcher'

test('matcher()', () => {
{
Expand Down Expand Up @@ -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)
}
})
32 changes: 14 additions & 18 deletions packages/plugin-commands-installation/src/recursive.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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]
}
}
33 changes: 33 additions & 0 deletions packages/plugin-commands-installation/test/update/update.ts
Expand Up @@ -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([
{
Expand Down

0 comments on commit abb41a6

Please sign in to comment.