mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 15:48:06 -05:00
feat: add new filter-prod option (#3372)
Add the new filter-prod option that executes the same as the filter option except it does not include devDependencies when building the package graph. Co-authored-by: Caleb Doucet <cdoucet>
This commit is contained in:
22
.changeset/cold-papayas-notice.md
Normal file
22
.changeset/cold-papayas-notice.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
"@pnpm/filter-workspace-packages": major
|
||||
---
|
||||
|
||||
# @pnpm/filter-workspace-packages
|
||||
|
||||
Change `@pnpm/filter-workspace-packages` to handle the new `filter-prod` flag, so that devDependencies are ignored if the filters / packageSelectors include `followProdDepsOnly` as true.
|
||||
|
||||
## filterPackages
|
||||
|
||||
WHAT: Change `filterPackages`'s second arg to accept an array of objects with properties `filter` and `followProdDepsOnly`.
|
||||
|
||||
WHY: Allow `filterPackages` to handle the filter-prod flag which allows the omission of devDependencies when building the package graph.
|
||||
|
||||
HOW: Update your code by converting the filters into an array of objects. The `filter` property of this object maps to the filter that was previously passed in. The `followProdDepsOnly` is a boolean that will
|
||||
ignore devDependencies when building the package graph.
|
||||
|
||||
If you do not care about ignoring devDependencies and want `filterPackages` to work as it did in the previous major version then you can use a simple map to convert your filters.
|
||||
|
||||
```
|
||||
const newFilters = oldFilters.map(filter => ({ filter, followProdDepsOnly: false }));
|
||||
```
|
||||
10
.changeset/cuddly-hats-remember.md
Normal file
10
.changeset/cuddly-hats-remember.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@pnpm/common-cli-options-help": minor
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/filter-workspace-packages": minor
|
||||
"@pnpm/parse-cli-args": minor
|
||||
"pkgs-graph": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add new cli arg --filter-prod. --filter-prod acts the same as --filter, but it omits devDependencies when building dependencies
|
||||
@@ -100,6 +100,10 @@ export const FILTERING = {
|
||||
description: 'Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: pnpm --filter="...[origin/master]" --test-pattern="test/*" test',
|
||||
name: '--test-pattern <pattern>',
|
||||
},
|
||||
{
|
||||
description: 'Restricts the scope to package names matching the given pattern similar to --filter, but it ignores devDependencies when searching for dependencies and dependents.',
|
||||
name: '--filter-prod <pattern>',
|
||||
},
|
||||
],
|
||||
title: 'Filtering options (run the command only on packages that satisfy at least one of the selectors)',
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface Config {
|
||||
useBetaCli: boolean
|
||||
extraBinPaths: string[]
|
||||
filter: string[]
|
||||
filterProd: string[]
|
||||
rawLocalConfig: Record<string, any>, // eslint-disable-line
|
||||
rawConfig: Record<string, any>, // eslint-disable-line
|
||||
dryRun?: boolean // This option might be not supported ever
|
||||
|
||||
@@ -39,6 +39,7 @@ export const types = Object.assign({
|
||||
'enable-pre-post-scripts': Boolean,
|
||||
'fetching-concurrency': Number,
|
||||
filter: [String, Array],
|
||||
'filter-prod': [String, Array],
|
||||
'frozen-lockfile': Boolean,
|
||||
'frozen-shrinkwrap': Boolean,
|
||||
'git-checks': Boolean,
|
||||
@@ -144,7 +145,7 @@ export default async (
|
||||
if (node.toUpperCase() !== process.execPath.toUpperCase()) {
|
||||
process.execPath = node
|
||||
}
|
||||
} catch (err) {} // eslint-disable-line:no-empty
|
||||
} catch (err) { } // eslint-disable-line:no-empty
|
||||
|
||||
if (cliOptions.dir) {
|
||||
cliOptions.dir = await realpathMissing(cliOptions.dir)
|
||||
@@ -336,6 +337,10 @@ export default async (
|
||||
pnpmConfig.filter = (pnpmConfig.filter as string).split(' ')
|
||||
}
|
||||
|
||||
if (typeof pnpmConfig.filterProd === 'string') {
|
||||
pnpmConfig.filterProd = (pnpmConfig.filterProd as string).split(' ')
|
||||
}
|
||||
|
||||
if (!pnpmConfig.ignoreScripts && pnpmConfig.workspaceDir) {
|
||||
pnpmConfig.extraBinPaths = [path.join(pnpmConfig.workspaceDir, 'node_modules', '.bin')]
|
||||
} else {
|
||||
|
||||
@@ -235,6 +235,24 @@ test('filter is read from .npmrc as an array', async () => {
|
||||
expect(config.filter).toStrictEqual(['foo', 'bar...'])
|
||||
})
|
||||
|
||||
test('filter-prod is read from .npmrc as an array', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
await fs.writeFile('.npmrc', 'filter-prod=foo bar...', 'utf8')
|
||||
await fs.writeFile('pnpm-workspace.yaml', '', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {
|
||||
global: false,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.filterProd).toStrictEqual(['foo', 'bar...'])
|
||||
})
|
||||
|
||||
test('throw error if --save-prod is used with --save-peer', async () => {
|
||||
try {
|
||||
await getConfig({
|
||||
|
||||
@@ -8,6 +8,11 @@ import parsePackageSelector, { PackageSelector } from './parsePackageSelector'
|
||||
|
||||
export { parsePackageSelector, PackageSelector }
|
||||
|
||||
export interface WorkspaceFilter {
|
||||
filter: string
|
||||
followProdDepsOnly: boolean
|
||||
}
|
||||
|
||||
export interface PackageGraph<T> {
|
||||
[id: string]: PackageNode<T>
|
||||
}
|
||||
@@ -16,6 +21,11 @@ interface Graph {
|
||||
[nodeId: string]: string[]
|
||||
}
|
||||
|
||||
interface FilteredGraph<T> {
|
||||
selectedProjectsGraph: PackageGraph<T>
|
||||
unmatchedFilters: string[]
|
||||
}
|
||||
|
||||
export async function readProjects (
|
||||
workspaceDir: string,
|
||||
pkgSelectors: PackageSelector[],
|
||||
@@ -37,7 +47,7 @@ export async function readProjects (
|
||||
|
||||
export async function filterPackages<T> (
|
||||
pkgs: Array<Package & T>,
|
||||
filter: string[],
|
||||
filter: WorkspaceFilter[],
|
||||
opts: {
|
||||
linkWorkspacePackages?: boolean
|
||||
prefix: string
|
||||
@@ -48,8 +58,7 @@ export async function filterPackages<T> (
|
||||
selectedProjectsGraph: PackageGraph<T>
|
||||
unmatchedFilters: string[]
|
||||
}> {
|
||||
const packageSelectors = filter
|
||||
.map((f) => parsePackageSelector(f, opts.prefix))
|
||||
const packageSelectors = filter.map(({ filter: f, followProdDepsOnly }) => ({ ...parsePackageSelector(f, opts.prefix), followProdDepsOnly }))
|
||||
|
||||
return filterPkgsBySelectorObjects(pkgs, packageSelectors, opts)
|
||||
}
|
||||
@@ -66,13 +75,41 @@ export async function filterPkgsBySelectorObjects<T> (
|
||||
selectedProjectsGraph: PackageGraph<T>
|
||||
unmatchedFilters: string[]
|
||||
}> {
|
||||
const { graph } = createPkgGraph<T>(pkgs, { linkWorkspacePackages: opts.linkWorkspacePackages })
|
||||
if (packageSelectors?.length) {
|
||||
return filterGraph(graph, packageSelectors, {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
testPattern: opts.testPattern,
|
||||
const [prodPackageSelectors, allPackageSelectors] = R.partition(({ followProdDepsOnly }) => !!followProdDepsOnly, packageSelectors)
|
||||
|
||||
if ((allPackageSelectors.length > 0) || (prodPackageSelectors.length > 0)) {
|
||||
let filteredGraph: FilteredGraph<T> | undefined
|
||||
|
||||
if (allPackageSelectors.length > 0) {
|
||||
const { graph } = createPkgGraph<T>(pkgs, { linkWorkspacePackages: opts.linkWorkspacePackages })
|
||||
filteredGraph = await filterGraph(graph, allPackageSelectors, {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
testPattern: opts.testPattern,
|
||||
})
|
||||
}
|
||||
|
||||
let prodFilteredGraph: FilteredGraph<T> | undefined
|
||||
|
||||
if (prodPackageSelectors.length > 0) {
|
||||
const { graph } = createPkgGraph<T>(pkgs, { ignoreDevDeps: true, linkWorkspacePackages: opts.linkWorkspacePackages })
|
||||
prodFilteredGraph = await filterGraph(graph, prodPackageSelectors, {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
testPattern: opts.testPattern,
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
selectedProjectsGraph: {
|
||||
...prodFilteredGraph?.selectedProjectsGraph,
|
||||
...filteredGraph?.selectedProjectsGraph,
|
||||
},
|
||||
unmatchedFilters: [
|
||||
...(prodFilteredGraph !== undefined ? prodFilteredGraph.unmatchedFilters : []),
|
||||
...(filteredGraph !== undefined ? filteredGraph.unmatchedFilters : []),
|
||||
],
|
||||
})
|
||||
} else {
|
||||
const { graph } = createPkgGraph<T>(pkgs, { linkWorkspacePackages: opts.linkWorkspacePackages })
|
||||
return Promise.resolve({ selectedProjectsGraph: graph, unmatchedFilters: [] })
|
||||
}
|
||||
}
|
||||
@@ -128,7 +165,7 @@ async function _filterGraph<T> (
|
||||
let entryPackages: string[] | null = null
|
||||
if (selector.diff) {
|
||||
let ignoreDependentForPkgs: string[] = []
|
||||
;[entryPackages, ignoreDependentForPkgs] = await getChangedPkgs(Object.keys(pkgGraph),
|
||||
;[entryPackages, ignoreDependentForPkgs] = await getChangedPkgs(Object.keys(pkgGraph),
|
||||
selector.diff, { workspaceDir: selector.parentDir ?? opts.workspaceDir, testPattern: opts.testPattern })
|
||||
selectEntries({
|
||||
...selector,
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface PackageSelector {
|
||||
includeDependents?: boolean
|
||||
namePattern?: string
|
||||
parentDir?: string
|
||||
followProdDepsOnly?: boolean
|
||||
}
|
||||
|
||||
export default (rawSelector: string, prefix: string): PackageSelector => {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/lockfile-types#readme",
|
||||
"scripts": {
|
||||
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix",
|
||||
"lint": "eslint -c ../../eslint.json src/**/*.ts",
|
||||
"prepublishOnly": "pnpm run compile"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm"
|
||||
|
||||
@@ -105,7 +105,7 @@ export default async function parseCliArgs (
|
||||
// `pnpm install ""` is going to be just `pnpm install`
|
||||
const params = argv.remain.slice(1).filter(Boolean)
|
||||
|
||||
if (options['recursive'] !== true && (options['filter'] || recursiveCommandUsed)) {
|
||||
if (options['recursive'] !== true && (options['filter'] || options['filter-prod'] || recursiveCommandUsed)) {
|
||||
options['recursive'] = true
|
||||
const subCmd: string | null = argv.remain[1] && opts.getCommandLongName(argv.remain[1])
|
||||
if (subCmd && recursiveCommandUsed) {
|
||||
|
||||
@@ -21,6 +21,15 @@ test('a command is recursive if it has a --filter option', async () => {
|
||||
expect(options).toHaveProperty(['recursive'])
|
||||
})
|
||||
|
||||
test('a command is recursive if it has a --filter-prod option', async () => {
|
||||
const { options, cmd } = await parseCliArgs({
|
||||
...DEFAULT_OPTS,
|
||||
universalOptionsTypes: { 'filter-prod': [String, Array] },
|
||||
}, ['--filter-prod', 'foo', 'update'])
|
||||
expect(cmd).toBe('update')
|
||||
expect(options).toHaveProperty(['recursive'])
|
||||
})
|
||||
|
||||
test('a command is recursive if -r option is used', async () => {
|
||||
const { options, cmd } = await parseCliArgs({
|
||||
...DEFAULT_OPTS,
|
||||
|
||||
@@ -27,14 +27,15 @@ export interface PackageNode<T> {
|
||||
dependencies: string[]
|
||||
}
|
||||
|
||||
export default function<T> (pkgs: Array<Package & T>, opts?: {
|
||||
export default function <T> (pkgs: Array<Package & T>, opts?: {
|
||||
ignoreDevDeps?: boolean
|
||||
linkWorkspacePackages?: boolean
|
||||
}): {
|
||||
graph: {[id: string]: PackageNode<T>}
|
||||
unmatched: Array<{pkgName: string, range: string}>
|
||||
graph: { [id: string]: PackageNode<T> }
|
||||
unmatched: Array<{ pkgName: string, range: string }>
|
||||
} {
|
||||
const pkgMap = createPkgMap(pkgs)
|
||||
const unmatched: Array<{pkgName: string, range: string}> = []
|
||||
const unmatched: Array<{ pkgName: string, range: string }> = []
|
||||
const graph = Object.keys(pkgMap)
|
||||
.reduce((acc, pkgSpec) => {
|
||||
acc[pkgSpec] = {
|
||||
@@ -48,7 +49,7 @@ export default function<T> (pkgs: Array<Package & T>, opts?: {
|
||||
|
||||
function createNode (pkg: Package): string[] {
|
||||
const dependencies = {
|
||||
...pkg.manifest.devDependencies,
|
||||
...(!opts?.ignoreDevDeps && pkg.manifest.devDependencies),
|
||||
...pkg.manifest.optionalDependencies,
|
||||
...pkg.manifest.dependencies,
|
||||
}
|
||||
|
||||
@@ -418,6 +418,112 @@ test('create package graph respects linked-workspace-packages = false', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('create package graph respects ignoreDevDeps = true', () => {
|
||||
const result = createPkgGraph([
|
||||
{
|
||||
dir: BAR1_PATH,
|
||||
manifest: {
|
||||
name: 'bar',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: FOO1_PATH,
|
||||
manifest: {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
bar: '^10.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: BAR2_PATH,
|
||||
manifest: {
|
||||
name: 'bar',
|
||||
version: '2.0.0',
|
||||
|
||||
dependencies: {
|
||||
foo: '^2.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: FOO2_PATH,
|
||||
manifest: {
|
||||
name: 'foo',
|
||||
version: '2.0.0',
|
||||
},
|
||||
},
|
||||
], { ignoreDevDeps: true })
|
||||
expect(result.unmatched).toStrictEqual([{ pkgName: 'bar', range: '^10.0.0' }])
|
||||
expect(result.graph).toStrictEqual({
|
||||
[BAR1_PATH]: {
|
||||
dependencies: [],
|
||||
package: {
|
||||
dir: BAR1_PATH,
|
||||
manifest: {
|
||||
name: 'bar',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[FOO1_PATH]: {
|
||||
dependencies: [],
|
||||
package: {
|
||||
dir: FOO1_PATH,
|
||||
manifest: {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
bar: '^10.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[BAR2_PATH]: {
|
||||
dependencies: [FOO2_PATH],
|
||||
package: {
|
||||
dir: BAR2_PATH,
|
||||
manifest: {
|
||||
name: 'bar',
|
||||
version: '2.0.0',
|
||||
|
||||
dependencies: {
|
||||
foo: '^2.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[FOO2_PATH]: {
|
||||
dependencies: [],
|
||||
package: {
|
||||
dir: FOO2_PATH,
|
||||
manifest: {
|
||||
name: 'foo',
|
||||
version: '2.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('* matches prerelease versions', () => {
|
||||
const result = createPkgGraph([
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ export const GLOBAL_OPTIONS = R.pick([
|
||||
'color',
|
||||
'dir',
|
||||
'filter',
|
||||
'filter-prod',
|
||||
'loglevel',
|
||||
'help',
|
||||
'parseable',
|
||||
|
||||
@@ -145,7 +145,7 @@ export default async function run (inputArgv: string[]) {
|
||||
cliOptions['recursive'] = true
|
||||
config.recursive = true
|
||||
|
||||
if (!config.recursiveInstall && !config.filter) {
|
||||
if (!config.recursiveInstall && !config.filter && !config.filterProd) {
|
||||
config.filter = ['{.}...']
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,16 @@ export default async function run (inputArgv: string[]) {
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
const filterResults = await filterPackages(allProjects, config.filter ?? [], {
|
||||
|
||||
config.filter = config.filter ?? []
|
||||
config.filterProd = config.filterProd ?? []
|
||||
|
||||
const filters = [
|
||||
...config.filter.map((filter) => ({ filter, followProdDepsOnly: false })),
|
||||
...config.filterProd.map((filter) => ({ filter, followProdDepsOnly: true })),
|
||||
]
|
||||
|
||||
const filterResults = await filterPackages(allProjects, filters, {
|
||||
linkWorkspacePackages: !!config.linkWorkspacePackages,
|
||||
prefix: process.cwd(),
|
||||
workspaceDir: wsDir,
|
||||
|
||||
Reference in New Issue
Block a user