mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: support finder functions for performing complex searches with list and why commands (#9946)
This commit is contained in:
5
.changeset/chubby-yaks-teach.md
Normal file
5
.changeset/chubby-yaks-teach.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/read-package-json": minor
|
||||
---
|
||||
|
||||
Implemented `readPackageJsonSync`.
|
||||
11
.changeset/short-coats-wish.md
Normal file
11
.changeset/short-coats-wish.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-listing": minor
|
||||
"@pnpm/reviewing.dependencies-hierarchy": minor
|
||||
"@pnpm/pnpmfile": minor
|
||||
"@pnpm/types": minor
|
||||
"@pnpm/list": minor
|
||||
"@pnpm/cli-utils": minor
|
||||
"@pnpm/config": minor
|
||||
---
|
||||
|
||||
Added support for `finders` [#9946](https://github.com/pnpm/pnpm/pull/9946).
|
||||
78
.changeset/tender-lamps-behave.md
Normal file
78
.changeset/tender-lamps-behave.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added support for `finders` [#9946](https://github.com/pnpm/pnpm/pull/9946).
|
||||
|
||||
In the past, `pnpm list` and `pnpm why` could only search for dependencies by **name** (and optionally version). For example:
|
||||
|
||||
```
|
||||
pnpm why minimist
|
||||
```
|
||||
|
||||
prints the chain of dependencies to any installed instance of `minimist`:
|
||||
|
||||
```
|
||||
verdaccio 5.20.1
|
||||
├─┬ handlebars 4.7.7
|
||||
│ └── minimist 1.2.8
|
||||
└─┬ mv 2.1.1
|
||||
└─┬ mkdirp 0.5.6
|
||||
└── minimist 1.2.8
|
||||
```
|
||||
|
||||
What if we want to search by **other properties** of a dependency, not just its name? For instance, find all packages that have `react@17` in their peer dependencies?
|
||||
|
||||
This is now possible with "finder functions". Finder functions can be declared in `.pnpmfile.cjs` and invoked with the `--find-by=<function name>` flag when running `pnpm list` or `pnpm why`.
|
||||
|
||||
Let's say we want to find any dependencies that have React 17 in peer dependencies. We can add this finder to our `.pnpmfile.cjs`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
finders: {
|
||||
react17: (ctx) => {
|
||||
return ctx.readManifest().peerDependencies?.react === '^17.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we can use this finder function by running:
|
||||
|
||||
```
|
||||
pnpm why --find-by=react17
|
||||
```
|
||||
|
||||
pnpm will find all dependencies that have this React in peer dependencies and print their exact locations in the dependency graph.
|
||||
|
||||
```
|
||||
@apollo/client 4.0.4
|
||||
├── @graphql-typed-document-node/core 3.2.0
|
||||
└── graphql-tag 2.12.6
|
||||
```
|
||||
|
||||
It is also possible to print out some additional information in the output by returning a string from the finder. For example, with the following finder:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
finders: {
|
||||
react17: (ctx) => {
|
||||
const manifest = ctx.readManifest()
|
||||
if (manifest.peerDependencies?.react === '^17.0.0') {
|
||||
return `license: ${manifest.license}`
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every matched package will also print out the license from its `package.json`:
|
||||
|
||||
```
|
||||
@apollo/client 4.0.4
|
||||
├── @graphql-typed-document-node/core 3.2.0
|
||||
│ license: MIT
|
||||
└── graphql-tag 2.12.6
|
||||
license: MIT
|
||||
```
|
||||
@@ -43,12 +43,13 @@ export async function getConfig (
|
||||
const configModulesDir = path.join(config.lockfileDir ?? config.rootProjectManifestDir, 'node_modules/.pnpm-config')
|
||||
pnpmfiles.unshift(...calcPnpmfilePathsOfPluginDeps(configModulesDir, config.configDependencies))
|
||||
}
|
||||
const { hooks, resolvedPnpmfilePaths } = requireHooks(config.lockfileDir ?? config.dir, {
|
||||
const { hooks, finders, resolvedPnpmfilePaths } = requireHooks(config.lockfileDir ?? config.dir, {
|
||||
globalPnpmfile: config.globalPnpmfile,
|
||||
pnpmfiles,
|
||||
tryLoadDefaultPnpmfile: config.tryLoadDefaultPnpmfile,
|
||||
})
|
||||
config.hooks = hooks
|
||||
config.finders = finders
|
||||
config.pnpmfile = resolvedPnpmfilePaths
|
||||
if (config.hooks?.updateConfig) {
|
||||
for (const updateConfig of config.hooks.updateConfig) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Catalogs } from '@pnpm/catalogs.types'
|
||||
import {
|
||||
type Finder,
|
||||
type Project,
|
||||
type ProjectManifest,
|
||||
type ProjectsGraph,
|
||||
@@ -141,6 +142,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
pnpmfile: string[] | string
|
||||
tryLoadDefaultPnpmfile?: boolean
|
||||
hooks?: Hooks
|
||||
finders?: Record<string, Finder>
|
||||
packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone' | 'clone-or-copy'
|
||||
hoistPattern?: string[]
|
||||
publicHoistPattern?: string[] | string
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
"devDependencies": {
|
||||
"@pnpm/fetcher-base": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/pnpmfile": "workspace:*"
|
||||
"@pnpm/pnpmfile": "workspace:*",
|
||||
"@pnpm/test-fixtures": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createHashFromMultipleFiles } from '@pnpm/crypto.hash'
|
||||
import pathAbsolute from 'path-absolute'
|
||||
import type { CustomFetchers } from '@pnpm/fetcher-base'
|
||||
import { type ImportIndexedPackageAsync } from '@pnpm/store-controller-types'
|
||||
import { requirePnpmfile, type Pnpmfile } from './requirePnpmfile.js'
|
||||
import { requirePnpmfile, type Pnpmfile, type Finders } from './requirePnpmfile.js'
|
||||
import { type HookContext, type Hooks } from './Hooks.js'
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -24,6 +24,7 @@ interface PnpmfileEntry {
|
||||
interface PnpmfileEntryLoaded {
|
||||
file: string
|
||||
hooks: Pnpmfile['hooks'] | undefined
|
||||
finders: Pnpmfile['finders'] | undefined
|
||||
includeInChecksum: boolean
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ export interface CookedHooks {
|
||||
|
||||
export interface RequireHooksResult {
|
||||
hooks: CookedHooks
|
||||
finders: Finders
|
||||
resolvedPnpmfilePaths: string[]
|
||||
}
|
||||
|
||||
@@ -85,6 +87,7 @@ export function requireHooks (
|
||||
file,
|
||||
includeInChecksum,
|
||||
hooks: requirePnpmfileResult.pnpmfileModule?.hooks,
|
||||
finders: requirePnpmfileResult.pnpmfileModule?.finders,
|
||||
})
|
||||
} else if (!optional) {
|
||||
throw new PnpmError('PNPMFILE_NOT_FOUND', `pnpmfile at "${file}" is not found`)
|
||||
@@ -92,6 +95,7 @@ export function requireHooks (
|
||||
}
|
||||
}
|
||||
|
||||
const mergedFinders: Finders = {}
|
||||
const cookedHooks: CookedHooks & Required<Pick<CookedHooks, 'readPackage' | 'preResolution' | 'afterAllResolved' | 'filterLog' | 'updateConfig'>> = {
|
||||
readPackage: [],
|
||||
preResolution: [],
|
||||
@@ -116,9 +120,23 @@ export function requireHooks (
|
||||
|
||||
let importProvider: string | undefined
|
||||
let fetchersProvider: string | undefined
|
||||
const finderProviders: Record<string, string> = {}
|
||||
|
||||
// process hooks in order
|
||||
for (const { hooks, file } of entries) {
|
||||
for (const { hooks, file, finders } of entries) {
|
||||
if (finders != null) {
|
||||
for (const [finderName, finder] of Object.entries(finders)) {
|
||||
if (mergedFinders[finderName] != null) {
|
||||
const firstDefinedIn = finderProviders[finderName]
|
||||
throw new PnpmError(
|
||||
'DUPLICATE_FINDER',
|
||||
`Finder "${finderName}" defined in both ${firstDefinedIn} and ${file}`
|
||||
)
|
||||
}
|
||||
mergedFinders[finderName] = finder
|
||||
finderProviders[finderName] = file
|
||||
}
|
||||
}
|
||||
const fileHooks: Hooks = hooks ?? {}
|
||||
|
||||
// readPackage & afterAllResolved
|
||||
@@ -180,6 +198,7 @@ export function requireHooks (
|
||||
|
||||
return {
|
||||
hooks: cookedHooks,
|
||||
finders: mergedFinders,
|
||||
resolvedPnpmfilePaths: entries.map(({ file }) => file),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs'
|
||||
import util from 'util'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { type PackageManifest } from '@pnpm/types'
|
||||
import { type PackageManifest, type Finder } from '@pnpm/types'
|
||||
import chalk from 'chalk'
|
||||
import { type Hooks } from './Hooks.js'
|
||||
|
||||
@@ -27,8 +27,11 @@ class PnpmFileFailError extends PnpmError {
|
||||
}
|
||||
}
|
||||
|
||||
export type Finders = Record<string, Finder>
|
||||
|
||||
export interface Pnpmfile {
|
||||
hooks?: Hooks
|
||||
finders?: Finders
|
||||
}
|
||||
|
||||
export function requirePnpmfile (pnpmFilePath: string, prefix: string): { pnpmfileModule: Pnpmfile | undefined } | undefined {
|
||||
|
||||
6
hooks/pnpmfile/test/__fixtures__/finders/finderBar.js
Normal file
6
hooks/pnpmfile/test/__fixtures__/finders/finderBar.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
finders: {
|
||||
bar: () => false,
|
||||
},
|
||||
}
|
||||
|
||||
5
hooks/pnpmfile/test/__fixtures__/finders/finderFoo1.js
Normal file
5
hooks/pnpmfile/test/__fixtures__/finders/finderFoo1.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
finders: {
|
||||
foo: () => false,
|
||||
},
|
||||
}
|
||||
5
hooks/pnpmfile/test/__fixtures__/finders/finderFoo2.js
Normal file
5
hooks/pnpmfile/test/__fixtures__/finders/finderFoo2.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
finders: {
|
||||
foo: () => false,
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from 'path'
|
||||
import { type Log } from '@pnpm/core-loggers'
|
||||
import { requireHooks, BadReadPackageHookError, type HookContext } from '@pnpm/pnpmfile'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { requirePnpmfile } from '../src/requirePnpmfile.js'
|
||||
|
||||
const defaultHookContext: HookContext = { log () {} }
|
||||
const f = fixtures(__dirname)
|
||||
|
||||
test('ignoring a pnpmfile that exports undefined', () => {
|
||||
const { pnpmfileModule: pnpmfile } = requirePnpmfile(path.join(__dirname, '__fixtures__/undefined.js'), __dirname)!
|
||||
@@ -83,3 +85,19 @@ test('updateConfig throws an error if it returns undefined', async () => {
|
||||
test('requireHooks throw an error if one of the specified pnpmfiles does not exist', async () => {
|
||||
expect(() => requireHooks(__dirname, { pnpmfiles: ['does-not-exist.cjs'] })).toThrow('is not found')
|
||||
})
|
||||
|
||||
test('requireHooks throws an error if there are two finders with the same name', async () => {
|
||||
const findersDir = f.find('finders')
|
||||
const pnpmfile1 = path.join(findersDir, 'finderFoo1.js')
|
||||
const pnpmfile2 = path.join(findersDir, 'finderFoo2.js')
|
||||
expect(() => requireHooks(__dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })).toThrow('Finder "foo" defined in both')
|
||||
})
|
||||
|
||||
test('requireHooks merges all the finders', async () => {
|
||||
const findersDir = f.find('finders')
|
||||
const pnpmfile1 = path.join(findersDir, 'finderFoo1.js')
|
||||
const pnpmfile2 = path.join(findersDir, 'finderBar.js')
|
||||
const { finders } = requireHooks(__dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })
|
||||
expect(finders.foo).toBeDefined()
|
||||
expect(finders.bar).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../__utils__/test-fixtures"
|
||||
},
|
||||
{
|
||||
"path": "../../crypto/hash"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type DependenciesField } from './misc.js'
|
||||
import { type BaseManifest } from './package.js'
|
||||
import { type BaseManifest, type DependencyManifest } from './package.js'
|
||||
|
||||
export type LogBase = {
|
||||
level: 'debug' | 'error'
|
||||
@@ -14,3 +14,11 @@ export type IncludedDependencies = {
|
||||
}
|
||||
|
||||
export type ReadPackageHook = <Pkg extends BaseManifest> (pkg: Pkg, dir?: string) => Pkg | Promise<Pkg>
|
||||
|
||||
export interface FinderContext {
|
||||
name: string
|
||||
version: string
|
||||
readManifest: () => DependencyManifest
|
||||
}
|
||||
|
||||
export type Finder = (ctx: FinderContext) => boolean | string
|
||||
|
||||
@@ -4,6 +4,17 @@ import { type PackageManifest } from '@pnpm/types'
|
||||
import loadJsonFile from 'load-json-file'
|
||||
import normalizePackageData from 'normalize-package-data'
|
||||
|
||||
export function readPackageJsonSync (pkgPath: string): PackageManifest {
|
||||
try {
|
||||
const manifest = loadJsonFile.sync<PackageManifest>(pkgPath)
|
||||
normalizePackageData(manifest)
|
||||
return manifest
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err.code) throw err
|
||||
throw new PnpmError('BAD_PACKAGE_JSON', `${pkgPath}: ${err.message as string}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function readPackageJson (pkgPath: string): Promise<PackageManifest> {
|
||||
try {
|
||||
const manifest = await loadJsonFile<PackageManifest>(pkgPath)
|
||||
@@ -15,6 +26,10 @@ export async function readPackageJson (pkgPath: string): Promise<PackageManifest
|
||||
}
|
||||
}
|
||||
|
||||
export function readPackageJsonFromDirSync (pkgPath: string): PackageManifest {
|
||||
return readPackageJsonSync(path.join(pkgPath, 'package.json'))
|
||||
}
|
||||
|
||||
export async function readPackageJsonFromDir (pkgPath: string): Promise<PackageManifest> {
|
||||
return readPackageJson(path.join(pkgPath, 'package.json'))
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -3444,6 +3444,9 @@ importers:
|
||||
'@pnpm/pnpmfile':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/test-fixtures':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/test-fixtures
|
||||
|
||||
hooks/read-package-hook:
|
||||
dependencies:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import fs from 'fs'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { execPnpmSync } from './utils/index.js'
|
||||
import { execPnpm, execPnpmSync } from './utils/index.js'
|
||||
|
||||
test('ls --filter=not-exist --json should prints an empty array (#9672)', async () => {
|
||||
preparePackages([
|
||||
@@ -21,3 +22,23 @@ test('ls --filter=not-exist --json should prints an empty array (#9672)', async
|
||||
const { stdout } = execPnpmSync(['ls', '--filter=project-that-does-not-exist', '--json'], { expectSuccess: true })
|
||||
expect(JSON.parse(stdout.toString())).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('ls should load a finder from .pnpmfile.cjs', async () => {
|
||||
prepare()
|
||||
const pnpmfile = `
|
||||
module.exports = { finders: { hasPeerA } }
|
||||
function hasPeerA (context) {
|
||||
const manifest = context.readManifest()
|
||||
if (manifest?.peerDependencies?.['@pnpm.e2e/peer-a'] == null) {
|
||||
return false
|
||||
}
|
||||
return \`@pnpm.e2e/peer-a@$\{manifest.peerDependencies['@pnpm.e2e/peer-a']}\`
|
||||
}
|
||||
`
|
||||
fs.writeFileSync('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
await execPnpm(['add', 'is-positive@1.0.0', '@pnpm.e2e/abc@1.0.0'])
|
||||
const result = execPnpmSync(['list', '--find-by=hasPeerA'])
|
||||
expect(result.stdout.toString()).toMatch(`dependencies:
|
||||
@pnpm.e2e/abc 1.0.0
|
||||
@pnpm.e2e/peer-a@^1.0.0`)
|
||||
})
|
||||
|
||||
@@ -12,4 +12,5 @@ export interface PackageNode {
|
||||
resolved?: string
|
||||
searched?: true
|
||||
version: string
|
||||
searchMessage?: string
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ import { detectDepTypes } from '@pnpm/lockfile.detect-dep-types'
|
||||
import { readModulesManifest } from '@pnpm/modules-yaml'
|
||||
import { normalizeRegistries } from '@pnpm/normalize-registries'
|
||||
import { readModulesDir } from '@pnpm/read-modules-dir'
|
||||
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||
import { type DependenciesField, DEPENDENCIES_FIELDS, type Registries } from '@pnpm/types'
|
||||
import { safeReadPackageJsonFromDir, readPackageJsonFromDirSync } from '@pnpm/read-package-json'
|
||||
import { type DependenciesField, type Finder, DEPENDENCIES_FIELDS, type Registries } from '@pnpm/types'
|
||||
import normalizePath from 'normalize-path'
|
||||
import realpathMissing from 'realpath-missing'
|
||||
import resolveLinkTarget from 'resolve-link-target'
|
||||
import { type PackageNode } from './PackageNode.js'
|
||||
import { type SearchFunction } from './types.js'
|
||||
import { getTree } from './getTree.js'
|
||||
import { getTreeNodeChildId } from './getTreeNodeChildId.js'
|
||||
import { getPkgInfo } from './getPkgInfo.js'
|
||||
@@ -38,7 +37,7 @@ export async function buildDependenciesHierarchy (
|
||||
include?: { [dependenciesField in DependenciesField]: boolean }
|
||||
registries?: Registries
|
||||
onlyProjects?: boolean
|
||||
search?: SearchFunction
|
||||
search?: Finder
|
||||
lockfileDir: string
|
||||
modulesDir?: string
|
||||
virtualStoreDirMaxLength: number
|
||||
@@ -109,7 +108,7 @@ async function dependenciesHierarchyForPackage (
|
||||
include: { [dependenciesField in DependenciesField]: boolean }
|
||||
registries: Registries
|
||||
onlyProjects?: boolean
|
||||
search?: SearchFunction
|
||||
search?: Finder
|
||||
skipped: Set<string>
|
||||
lockfileDir: string
|
||||
modulesDir?: string
|
||||
@@ -152,7 +151,7 @@ async function dependenciesHierarchyForPackage (
|
||||
result[dependenciesField] = []
|
||||
for (const alias in topDeps) {
|
||||
const ref = topDeps[alias]
|
||||
const packageInfo = getPkgInfo({
|
||||
const { pkgInfo: packageInfo, readManifest } = getPkgInfo({
|
||||
alias,
|
||||
currentPackages: currentLockfile.packages ?? {},
|
||||
depTypes,
|
||||
@@ -166,7 +165,11 @@ async function dependenciesHierarchyForPackage (
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
})
|
||||
let newEntry: PackageNode | null = null
|
||||
const matchedSearched = opts.search?.(packageInfo)
|
||||
const matchedSearched = opts.search?.({
|
||||
name: packageInfo.name,
|
||||
version: packageInfo.version,
|
||||
readManifest,
|
||||
})
|
||||
const nodeId = getTreeNodeChildId({
|
||||
parentId,
|
||||
dep: { alias, ref },
|
||||
@@ -192,6 +195,9 @@ async function dependenciesHierarchyForPackage (
|
||||
if (newEntry != null) {
|
||||
if (matchedSearched) {
|
||||
newEntry.searched = true
|
||||
if (typeof matchedSearched === 'string') {
|
||||
newEntry.searchMessage = matchedSearched
|
||||
}
|
||||
}
|
||||
result[dependenciesField]!.push(newEntry)
|
||||
}
|
||||
@@ -219,11 +225,18 @@ async function dependenciesHierarchyForPackage (
|
||||
path: pkgPath,
|
||||
version,
|
||||
}
|
||||
const matchedSearched = opts.search?.(pkg)
|
||||
const matchedSearched = opts.search?.({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
readManifest: () => readPackageJsonFromDirSync(pkgPath),
|
||||
})
|
||||
if ((opts.search != null) && !matchedSearched) return
|
||||
const newEntry: PackageNode = pkg
|
||||
if (matchedSearched) {
|
||||
newEntry.searched = true
|
||||
if (typeof matchedSearched === 'string') {
|
||||
newEntry.searchMessage = matchedSearched
|
||||
}
|
||||
}
|
||||
result.unsavedDependencies = result.unsavedDependencies ?? []
|
||||
result.unsavedDependencies.push(newEntry)
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import npa from '@pnpm/npm-package-arg'
|
||||
import { type SearchFunction } from './types.js'
|
||||
import { type FinderContext, type Finder } from '@pnpm/types'
|
||||
import semver from 'semver'
|
||||
|
||||
export function createPackagesSearcher (queries: string[]): SearchFunction {
|
||||
const searchers: SearchFunction[] = queries
|
||||
export function createPackagesSearcher (queries: string[], finders?: Finder[]): Finder {
|
||||
const searchers: Finder[] = queries
|
||||
.map(parseSearchQuery)
|
||||
.map((packageSelector) => search.bind(null, packageSelector))
|
||||
return (pkg) => searchers.some((search) => search(pkg))
|
||||
return (pkg) => {
|
||||
if (searchers.length > 0 && searchers.some((search) => search(pkg))) {
|
||||
return true
|
||||
}
|
||||
if (finders == null) return false
|
||||
const messages: string[] = []
|
||||
let found = false
|
||||
for (const finder of finders) {
|
||||
const result = finder(pkg)
|
||||
if (result) {
|
||||
found = true
|
||||
if (typeof result === 'string') {
|
||||
messages.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (messages.length) return messages.join('\n')
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
type MatchFunction = (entry: string) => boolean
|
||||
@@ -17,15 +35,15 @@ function search (
|
||||
matchName: MatchFunction
|
||||
matchVersion?: MatchFunction
|
||||
},
|
||||
pkg: { name: string, version: string }
|
||||
{ name, version }: FinderContext
|
||||
): boolean {
|
||||
if (!packageSelector.matchName(pkg.name)) {
|
||||
if (!packageSelector.matchName(name)) {
|
||||
return false
|
||||
}
|
||||
if (packageSelector.matchVersion == null) {
|
||||
return true
|
||||
}
|
||||
return !pkg.version.startsWith('link:') && packageSelector.matchVersion(pkg.version)
|
||||
return !version.startsWith('link:') && packageSelector.matchVersion(version)
|
||||
}
|
||||
|
||||
interface ParsedSearchQuery {
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
pkgSnapshotToResolution,
|
||||
} from '@pnpm/lockfile.utils'
|
||||
import { type DepTypes, DepType } from '@pnpm/lockfile.detect-dep-types'
|
||||
import { type Registries } from '@pnpm/types'
|
||||
import { type DependencyManifest, type Registries } from '@pnpm/types'
|
||||
import { depPathToFilename, refToRelative } from '@pnpm/dependency-path'
|
||||
import { readPackageJsonFromDirSync } from '@pnpm/read-package-json'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
export interface GetPkgInfoOpts {
|
||||
@@ -40,7 +41,7 @@ export interface GetPkgInfoOpts {
|
||||
readonly rewriteLinkVersionDir?: string
|
||||
}
|
||||
|
||||
export function getPkgInfo (opts: GetPkgInfoOpts): PackageInfo {
|
||||
export function getPkgInfo (opts: GetPkgInfoOpts): { pkgInfo: PackageInfo, readManifest: () => DependencyManifest } {
|
||||
let name!: string
|
||||
let version: string
|
||||
let resolved: string | undefined
|
||||
@@ -107,7 +108,10 @@ export function getPkgInfo (opts: GetPkgInfoOpts): PackageInfo {
|
||||
} else if (depType === DepType.ProdOnly) {
|
||||
packageInfo.dev = false
|
||||
}
|
||||
return packageInfo
|
||||
return {
|
||||
pkgInfo: packageInfo,
|
||||
readManifest: () => readPackageJsonFromDirSync(fullPackagePath),
|
||||
}
|
||||
}
|
||||
|
||||
interface PackageInfo {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import path from 'path'
|
||||
import { type PackageSnapshots, type ProjectSnapshot } from '@pnpm/lockfile.fs'
|
||||
import { type DepTypes } from '@pnpm/lockfile.detect-dep-types'
|
||||
import { type Registries } from '@pnpm/types'
|
||||
import { type SearchFunction } from './types.js'
|
||||
import { type Finder, type Registries } from '@pnpm/types'
|
||||
import { type PackageNode } from './PackageNode.js'
|
||||
import { getPkgInfo } from './getPkgInfo.js'
|
||||
import { getTreeNodeChildId } from './getTreeNodeChildId.js'
|
||||
@@ -16,7 +15,7 @@ interface GetTreeOpts {
|
||||
excludePeerDependencies?: boolean
|
||||
lockfileDir: string
|
||||
onlyProjects?: boolean
|
||||
search?: SearchFunction
|
||||
search?: Finder
|
||||
skipped: Set<string>
|
||||
registries: Registries
|
||||
importers: Record<string, ProjectSnapshot>
|
||||
@@ -127,7 +126,7 @@ function getTreeHelper (
|
||||
|
||||
for (const alias in deps) {
|
||||
const ref = deps[alias]
|
||||
const packageInfo = getPkgInfo({
|
||||
const { pkgInfo: packageInfo, readManifest } = getPkgInfo({
|
||||
alias,
|
||||
currentPackages: opts.currentPackages,
|
||||
depTypes: opts.depTypes,
|
||||
@@ -142,7 +141,11 @@ function getTreeHelper (
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
})
|
||||
let circular: boolean
|
||||
const matchedSearched = opts.search?.(packageInfo)
|
||||
const matchedSearched = opts.search?.({
|
||||
name: packageInfo.name,
|
||||
version: packageInfo.version,
|
||||
readManifest,
|
||||
})
|
||||
let newEntry: PackageNode | null = null
|
||||
const nodeId = getTreeNodeChildId({
|
||||
parentId,
|
||||
@@ -210,6 +213,9 @@ function getTreeHelper (
|
||||
}
|
||||
if (matchedSearched) {
|
||||
newEntry.searched = true
|
||||
if (typeof matchedSearched === 'string') {
|
||||
newEntry.searchMessage = matchedSearched
|
||||
}
|
||||
}
|
||||
if (!newEntry.isPeer || !opts.excludePeerDependencies || newEntry.dependencies?.length) {
|
||||
resultDependencies.push(newEntry)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { buildDependenciesHierarchy, type DependenciesHierarchy } from './buildDependenciesHierarchy.js'
|
||||
export { type PackageNode } from './PackageNode.js'
|
||||
export { type SearchFunction } from './types.js'
|
||||
export { createPackagesSearcher } from './createPackagesSearcher.js'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type SearchFunction = (pkg: { name: string, version: string }) => boolean
|
||||
@@ -1,25 +1,44 @@
|
||||
import { type DependencyManifest } from '@pnpm/types'
|
||||
import { createPackagesSearcher } from '../lib/createPackagesSearcher.js'
|
||||
|
||||
test('packages searcher', () => {
|
||||
{
|
||||
const search = createPackagesSearcher(['rimraf@*'])
|
||||
expect(search({ name: 'rimraf', version: '1.0.0' })).toBeTruthy()
|
||||
expect(search({ name: 'express', version: '1.0.0' })).not.toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '1.0.0' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'express', version: '1.0.0' }))).not.toBeTruthy()
|
||||
}
|
||||
{
|
||||
const search = createPackagesSearcher(['rim*'])
|
||||
expect(search({ name: 'rimraf', version: '1.0.0' })).toBeTruthy()
|
||||
expect(search({ name: 'express', version: '1.0.0' })).not.toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '1.0.0' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'express', version: '1.0.0' }))).not.toBeTruthy()
|
||||
}
|
||||
{
|
||||
const search = createPackagesSearcher(['rim*@2'])
|
||||
expect(search({ name: 'rimraf', version: '2.0.0' })).toBeTruthy()
|
||||
expect(search({ name: 'rimraf', version: '1.0.0' })).not.toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '2.0.0' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '1.0.0' }))).not.toBeTruthy()
|
||||
}
|
||||
{
|
||||
const search = createPackagesSearcher(['minimatch', 'once@1.4'])
|
||||
expect(search({ name: 'minimatch', version: '2.0.0' })).toBeTruthy()
|
||||
expect(search({ name: 'once', version: '1.4.1' })).toBeTruthy()
|
||||
expect(search({ name: 'rimraf', version: '1.0.0' })).not.toBeTruthy()
|
||||
expect(search(mockContext({ name: 'minimatch', version: '2.0.0' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'once', version: '1.4.1' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '1.0.0' }))).not.toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('package searcher with 2 finders', () => {
|
||||
const search = createPackagesSearcher([], [
|
||||
(ctx) => ctx.name === 'once',
|
||||
(ctx) => ctx.name === 'rimraf',
|
||||
])
|
||||
expect(search(mockContext({ name: 'minimatch', version: '2.0.0' }))).toBeFalsy()
|
||||
expect(search(mockContext({ name: 'once', version: '1.4.1' }))).toBeTruthy()
|
||||
expect(search(mockContext({ name: 'rimraf', version: '1.0.0' }))).toBeTruthy()
|
||||
})
|
||||
|
||||
function mockContext (manifest: DependencyManifest) {
|
||||
return {
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
readManifest: () => manifest,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||
import { type DependenciesField, type Registries } from '@pnpm/types'
|
||||
import { type DependenciesField, type Registries, type Finder } from '@pnpm/types'
|
||||
import { type PackageNode, buildDependenciesHierarchy, type DependenciesHierarchy, createPackagesSearcher } from '@pnpm/reviewing.dependencies-hierarchy'
|
||||
import { renderJson } from './renderJson.js'
|
||||
import { renderParseable } from './renderParseable.js'
|
||||
@@ -66,9 +66,10 @@ export async function searchForPackages (
|
||||
registries?: Registries
|
||||
modulesDir?: string
|
||||
virtualStoreDirMaxLength: number
|
||||
finders?: Finder[]
|
||||
}
|
||||
): Promise<PackageDependencyHierarchy[]> {
|
||||
const search = createPackagesSearcher(packages)
|
||||
const search = createPackagesSearcher(packages, opts.finders)
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(await buildDependenciesHierarchy(projectPaths, {
|
||||
@@ -110,6 +111,7 @@ export async function listForPackages (
|
||||
registries?: Registries
|
||||
modulesDir?: string
|
||||
virtualStoreDirMaxLength: number
|
||||
finders?: Finder[]
|
||||
}
|
||||
): Promise<string> {
|
||||
const opts = { ...DEFAULTS, ...maybeOpts }
|
||||
@@ -143,6 +145,7 @@ export async function list (
|
||||
showExtraneous?: boolean
|
||||
modulesDir?: string
|
||||
virtualStoreDirMaxLength: number
|
||||
finders?: Finder[]
|
||||
}
|
||||
): Promise<string> {
|
||||
const opts = { ...DEFAULTS, ...maybeOpts }
|
||||
|
||||
@@ -114,12 +114,17 @@ export async function toArchyTree (
|
||||
return Promise.all(
|
||||
sortPackages(entryNodes).map(async (node) => {
|
||||
const nodes = await toArchyTree(getPkgColor, node.dependencies ?? [], opts)
|
||||
const labelLines: string[] = [
|
||||
printLabel(getPkgColor, node),
|
||||
]
|
||||
if (node.searchMessage) {
|
||||
labelLines.push(node.searchMessage)
|
||||
}
|
||||
if (opts.long) {
|
||||
const pkg = await getPkgInfo(node)
|
||||
const labelLines = [
|
||||
printLabel(getPkgColor, node),
|
||||
pkg.description,
|
||||
]
|
||||
if (pkg.description) {
|
||||
labelLines.push(pkg.description)
|
||||
}
|
||||
if (pkg.repository) {
|
||||
labelLines.push(pkg.repository)
|
||||
}
|
||||
@@ -129,14 +134,9 @@ export async function toArchyTree (
|
||||
if (pkg.path) {
|
||||
labelLines.push(pkg.path)
|
||||
}
|
||||
|
||||
return {
|
||||
label: labelLines.join('\n'),
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: printLabel(getPkgColor, node),
|
||||
label: labelLines.join('\n'),
|
||||
nodes,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
|
||||
import { type Config, types as allTypes } from '@pnpm/config'
|
||||
import { list, listForPackages } from '@pnpm/list'
|
||||
import { type IncludedDependencies } from '@pnpm/types'
|
||||
import { type Finder, type IncludedDependencies } from '@pnpm/types'
|
||||
import pick from 'ramda/src/pick'
|
||||
import renderHelp from 'render-help'
|
||||
import { listRecursive } from './recursive.js'
|
||||
@@ -32,6 +33,7 @@ export const cliOptionsTypes = (): Record<string, unknown> => ({
|
||||
'exclude-peers': Boolean,
|
||||
'only-projects': Boolean,
|
||||
recursive: Boolean,
|
||||
'find-by': [String, Array],
|
||||
})
|
||||
|
||||
export const shorthands: Record<string, string> = {
|
||||
@@ -124,6 +126,7 @@ export type ListCommandOptions = Pick<Config,
|
||||
| 'allProjects'
|
||||
| 'dev'
|
||||
| 'dir'
|
||||
| 'finders'
|
||||
| 'optional'
|
||||
| 'production'
|
||||
| 'selectedProjectsGraph'
|
||||
@@ -139,6 +142,7 @@ export type ListCommandOptions = Pick<Config,
|
||||
parseable?: boolean
|
||||
onlyProjects?: boolean
|
||||
recursive?: boolean
|
||||
findBy?: string[]
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
@@ -178,8 +182,19 @@ export async function render (
|
||||
parseable?: boolean
|
||||
modulesDir?: string
|
||||
virtualStoreDirMaxLength: number
|
||||
finders?: Record<string, Finder>
|
||||
findBy?: string[]
|
||||
}
|
||||
): Promise<string> {
|
||||
const finders: Finder[] = []
|
||||
if (opts.findBy) {
|
||||
for (const finderName of opts.findBy) {
|
||||
if (opts.finders?.[finderName] == null) {
|
||||
throw new PnpmError('FINDER_NOT_FOUND', `No finder with name ${finderName} is found`)
|
||||
}
|
||||
finders.push(opts.finders[finderName])
|
||||
}
|
||||
}
|
||||
const listOpts = {
|
||||
alwaysPrintRootPackage: opts.alwaysPrintRootPackage,
|
||||
depth: opts.depth ?? 0,
|
||||
@@ -192,8 +207,9 @@ export async function render (
|
||||
showExtraneous: false,
|
||||
modulesDir: opts.modulesDir,
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
finders,
|
||||
}
|
||||
return (params.length > 0)
|
||||
return (params.length > 0) || listOpts.finders.length > 0
|
||||
? listForPackages(params, prefixes, listOpts)
|
||||
: list(prefixes, listOpts)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const cliOptionsTypes = (): Record<string, unknown> => ({
|
||||
...rcOptionsTypes(),
|
||||
'exclude-peers': Boolean,
|
||||
recursive: Boolean,
|
||||
'find-by': [String, Array],
|
||||
})
|
||||
|
||||
export const shorthands: Record<string, string> = {
|
||||
@@ -103,8 +104,8 @@ export async function handler (
|
||||
opts: ListCommandOptions,
|
||||
params: string[]
|
||||
): Promise<string> {
|
||||
if (params.length === 0) {
|
||||
throw new PnpmError('MISSING_PACKAGE_NAME', '`pnpm why` requires the package name')
|
||||
if (params.length === 0 && opts.findBy == null) {
|
||||
throw new PnpmError('MISSING_PACKAGE_NAME', '`pnpm why` requires the package name or --find-by=<finder-name>')
|
||||
}
|
||||
return list({
|
||||
...opts,
|
||||
|
||||
@@ -260,5 +260,5 @@ test('`pnpm recursive why` should fail if no package name was provided', async (
|
||||
}
|
||||
|
||||
expect(err.code).toBe('ERR_PNPM_MISSING_PACKAGE_NAME')
|
||||
expect(err.message).toBe('`pnpm why` requires the package name')
|
||||
expect(err.message).toMatch('`pnpm why` requires the package name')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user