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:
cdoucet-cvent
2021-04-22 12:57:27 -03:00
committed by GitHub
parent 85fb21a837
commit dfdf669e64
15 changed files with 242 additions and 19 deletions

View 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 }));
```

View 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

View File

@@ -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)',
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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({

View File

@@ -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,

View File

@@ -8,6 +8,7 @@ export interface PackageSelector {
includeDependents?: boolean
namePattern?: string
parentDir?: string
followProdDepsOnly?: boolean
}
export default (rawSelector: string, prefix: string): PackageSelector => {

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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([
{

View File

@@ -28,6 +28,7 @@ export const GLOBAL_OPTIONS = R.pick([
'color',
'dir',
'filter',
'filter-prod',
'loglevel',
'help',
'parseable',

View File

@@ -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,