Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: excluding deps from update #5432

Merged
merged 2 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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