mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-25 08:08:14 -05:00
feat: changed-files-ignore-pattern option (#3797)
This commit is contained in:
8
.changeset/pretty-hornets-pull.md
Normal file
8
.changeset/pretty-hornets-pull.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/common-cli-options-help": minor
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/filter-workspace-packages": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add option 'changed-files-ignore-pattern' to ignore changed files by glob patterns when filtering for changed projects since the specified commit/branch.
|
||||
@@ -104,6 +104,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: 'Defines files to ignore when filtering for changed projects since the specified commit/branch. Usage example: pnpm --filter="...[origin/master]" --changed-files-ignore-pattern="**/README.md" build',
|
||||
name: '--changed-files-ignore-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>',
|
||||
|
||||
@@ -133,6 +133,7 @@ export interface Config {
|
||||
workspaceRoot: boolean
|
||||
|
||||
testPattern?: string[]
|
||||
changedFilesIgnorePattern?: string[]
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
|
||||
@@ -111,6 +111,7 @@ export const types = Object.assign({
|
||||
'workspace-packages': [String, Array],
|
||||
'workspace-root': Boolean,
|
||||
'test-pattern': [String, Array],
|
||||
'changed-files-ignore-pattern': [String, Array],
|
||||
}, npmTypes.types)
|
||||
|
||||
export type CliOptions = Record<string, unknown> & { dir?: string }
|
||||
|
||||
@@ -622,6 +622,42 @@ test('respects test-pattern', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('respects changed-files-ignore-pattern', async () => {
|
||||
{
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(config.changedFilesIgnorePattern).toBeUndefined()
|
||||
}
|
||||
{
|
||||
prepareEmpty()
|
||||
|
||||
const npmrc = [
|
||||
'changed-files-ignore-pattern[]=.github/**',
|
||||
'changed-files-ignore-pattern[]=**/README.md',
|
||||
].join('\n')
|
||||
|
||||
await fs.writeFile('.npmrc', npmrc, 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {
|
||||
global: false,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(config.changedFilesIgnorePattern).toEqual(['.github/**', '**/README.md'])
|
||||
}
|
||||
})
|
||||
|
||||
test('dir is resolved to real path', async () => {
|
||||
prepareEmpty()
|
||||
const realDir = path.resolve('real-path')
|
||||
|
||||
@@ -8,9 +8,9 @@ type ChangeType = 'source' | 'test'
|
||||
|
||||
interface ChangedDir { dir: string, changeType: ChangeType }
|
||||
|
||||
export default async function changedSince (packageDirs: string[], commit: string, opts: { workspaceDir: string, testPattern?: string[] }): Promise<[string[], string[]]> {
|
||||
export default async function changedSince (packageDirs: string[], commit: string, opts: { workspaceDir: string, testPattern?: string[], changedFilesIgnorePattern?: string[] }): Promise<[string[], string[]]> {
|
||||
const repoRoot = path.resolve(await findUp('.git', { cwd: opts.workspaceDir, type: 'directory' }) ?? opts.workspaceDir, '..')
|
||||
const changedDirs = (await getChangedDirsSinceCommit(commit, opts.workspaceDir, opts.testPattern ?? []))
|
||||
const changedDirs = (await getChangedDirsSinceCommit(commit, opts.workspaceDir, opts.testPattern ?? [], opts.changedFilesIgnorePattern ?? []))
|
||||
.map(changedDir => ({ ...changedDir, dir: path.join(repoRoot, changedDir.dir) }))
|
||||
const pkgChangeTypes = new Map<string, ChangeType | undefined>()
|
||||
for (const pkgDir of packageDirs) {
|
||||
@@ -42,7 +42,7 @@ export default async function changedSince (packageDirs: string[], commit: strin
|
||||
return [changedPkgs, ignoreDependentForPkgs]
|
||||
}
|
||||
|
||||
async function getChangedDirsSinceCommit (commit: string, workingDir: string, testPattern: string[]): Promise<ChangedDir[]> {
|
||||
async function getChangedDirsSinceCommit (commit: string, workingDir: string, testPattern: string[], changedFilesIgnorePattern: string[]): Promise<ChangedDir[]> {
|
||||
let diff!: string
|
||||
try {
|
||||
diff = (
|
||||
@@ -63,7 +63,15 @@ async function getChangedDirsSinceCommit (commit: string, workingDir: string, te
|
||||
return []
|
||||
}
|
||||
|
||||
const changedFiles = diff.split('\n')
|
||||
const allChangedFiles = diff.split('\n')
|
||||
const patterns = changedFilesIgnorePattern.filter(
|
||||
(pattern) => pattern.length
|
||||
)
|
||||
const changedFiles = patterns.length
|
||||
? micromatch.not(allChangedFiles, patterns, {
|
||||
dot: true,
|
||||
})
|
||||
: allChangedFiles
|
||||
|
||||
for (const changedFile of changedFiles) {
|
||||
const dir = path.dirname(changedFile)
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function readProjects (
|
||||
pkgSelectors: PackageSelector[],
|
||||
opts?: {
|
||||
linkWorkspacePackages?: boolean
|
||||
changedFilesIgnorePattern?: string[]
|
||||
}
|
||||
) {
|
||||
const allProjects = await findWorkspacePackages(workspaceDir, {})
|
||||
@@ -43,6 +44,7 @@ export async function readProjects (
|
||||
{
|
||||
linkWorkspacePackages: opts?.linkWorkspacePackages,
|
||||
workspaceDir,
|
||||
changedFilesIgnorePattern: opts?.changedFilesIgnorePattern,
|
||||
}
|
||||
)
|
||||
return { allProjects, selectedProjectsGraph }
|
||||
@@ -56,6 +58,7 @@ export async function filterPackages<T> (
|
||||
prefix: string
|
||||
workspaceDir: string
|
||||
testPattern?: string[]
|
||||
changedFilesIgnorePattern?: string[]
|
||||
useGlobDirFiltering?: boolean
|
||||
}
|
||||
): Promise<{
|
||||
@@ -74,6 +77,7 @@ export async function filterPkgsBySelectorObjects<T> (
|
||||
linkWorkspacePackages?: boolean
|
||||
workspaceDir: string
|
||||
testPattern?: string[]
|
||||
changedFilesIgnorePattern?: string[]
|
||||
useGlobDirFiltering?: boolean
|
||||
}
|
||||
): Promise<{
|
||||
@@ -90,6 +94,7 @@ export async function filterPkgsBySelectorObjects<T> (
|
||||
filteredGraph = await filterGraph(graph, allPackageSelectors, {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
testPattern: opts.testPattern,
|
||||
changedFilesIgnorePattern: opts.changedFilesIgnorePattern,
|
||||
useGlobDirFiltering: opts.useGlobDirFiltering,
|
||||
})
|
||||
}
|
||||
@@ -101,6 +106,7 @@ export async function filterPkgsBySelectorObjects<T> (
|
||||
prodFilteredGraph = await filterGraph(graph, prodPackageSelectors, {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
testPattern: opts.testPattern,
|
||||
changedFilesIgnorePattern: opts.changedFilesIgnorePattern,
|
||||
useGlobDirFiltering: opts.useGlobDirFiltering,
|
||||
})
|
||||
}
|
||||
@@ -127,6 +133,7 @@ export default async function filterGraph<T> (
|
||||
opts: {
|
||||
workspaceDir: string
|
||||
testPattern?: string[]
|
||||
changedFilesIgnorePattern?: string[]
|
||||
useGlobDirFiltering?: boolean
|
||||
}
|
||||
): Promise<{
|
||||
@@ -156,6 +163,7 @@ async function _filterGraph<T> (
|
||||
opts: {
|
||||
workspaceDir: string
|
||||
testPattern?: string[]
|
||||
changedFilesIgnorePattern?: string[]
|
||||
useGlobDirFiltering?: boolean
|
||||
},
|
||||
packageSelectors: PackageSelector[]
|
||||
@@ -178,7 +186,7 @@ async function _filterGraph<T> (
|
||||
if (selector.diff) {
|
||||
let ignoreDependentForPkgs: string[] = []
|
||||
;[entryPackages, ignoreDependentForPkgs] = await getChangedPkgs(Object.keys(pkgGraph),
|
||||
selector.diff, { workspaceDir: selector.parentDir ?? opts.workspaceDir, testPattern: opts.testPattern })
|
||||
selector.diff, { workspaceDir: selector.parentDir ?? opts.workspaceDir, testPattern: opts.testPattern, changedFilesIgnorePattern: opts.changedFilesIgnorePattern })
|
||||
selectEntries({
|
||||
...selector,
|
||||
includeDependents: false,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const GLOBAL_OPTIONS = pick([
|
||||
'reporter',
|
||||
'stream',
|
||||
'test-pattern',
|
||||
'changed-files-ignore-pattern',
|
||||
'use-stderr',
|
||||
'workspace-packages',
|
||||
'workspace-root',
|
||||
|
||||
@@ -190,6 +190,7 @@ export default async function run (inputArgv: string[]) {
|
||||
prefix: process.cwd(),
|
||||
workspaceDir: wsDir,
|
||||
testPattern: config.testPattern,
|
||||
changedFilesIgnorePattern: config.changedFilesIgnorePattern,
|
||||
useGlobDirFiltering: config.useBetaCli,
|
||||
})
|
||||
config.selectedProjectsGraph = filterResults.selectedProjectsGraph
|
||||
|
||||
@@ -353,6 +353,111 @@ test('test-pattern is respected by the test script', async () => {
|
||||
expect(output.sort()).toStrictEqual(['project-2', 'project-4'])
|
||||
})
|
||||
|
||||
test('changed-files-ignore-pattern is respected', async () => {
|
||||
const remote = tempy.directory()
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-1-no-changes',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-2-change-is-never-ignored',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-3-ignored-by-pattern',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-4-ignored-by-pattern',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-5-ignored-by-pattern',
|
||||
version: '1.0.0',
|
||||
},
|
||||
])
|
||||
|
||||
await execa('git', ['init'])
|
||||
await execa('git', ['config', 'user.email', 'x@y.z'])
|
||||
await execa('git', ['config', 'user.name', 'xyz'])
|
||||
await execa('git', ['init', '--bare'], { cwd: remote })
|
||||
await execa('git', ['add', '*'])
|
||||
await execa('git', ['commit', '-m', 'init', '--no-gpg-sign'])
|
||||
await execa('git', ['remote', 'add', 'origin', remote])
|
||||
await execa('git', ['push', '-u', 'origin', 'master'])
|
||||
|
||||
const npmrcLines = []
|
||||
await fs.writeFile('project-2-change-is-never-ignored/index.js', '')
|
||||
|
||||
npmrcLines.push('changed-files-ignore-pattern[]=**/{*.spec.js,*.md}')
|
||||
await fs.writeFile('project-3-ignored-by-pattern/index.spec.js', '')
|
||||
await fs.writeFile('project-3-ignored-by-pattern/README.md', '')
|
||||
|
||||
npmrcLines.push('changed-files-ignore-pattern[]=**/buildscript.js')
|
||||
await fs.mkdir('project-4-ignored-by-pattern/a/b/c', {
|
||||
recursive: true,
|
||||
})
|
||||
await fs.writeFile('project-4-ignored-by-pattern/a/b/c/buildscript.js', '')
|
||||
|
||||
npmrcLines.push('changed-files-ignore-pattern[]=**/cache/**')
|
||||
await fs.mkdir('project-5-ignored-by-pattern/cache/a/b', {
|
||||
recursive: true,
|
||||
})
|
||||
await fs.writeFile('project-5-ignored-by-pattern/cache/a/b/index.js', '')
|
||||
|
||||
await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
await execa('git', ['add', '.'])
|
||||
await execa('git', [
|
||||
'commit',
|
||||
'--allow-empty-message',
|
||||
'-m',
|
||||
'',
|
||||
'--no-gpg-sign',
|
||||
])
|
||||
|
||||
await fs.writeFile('.npmrc', npmrcLines.join('\n'), 'utf8')
|
||||
await execPnpm(['install'])
|
||||
|
||||
const getChangedProjects = async (opts?: {
|
||||
overrideChangedFilesIgnorePatternWithNoPattern: boolean
|
||||
}) => {
|
||||
const result = await execPnpmSync(
|
||||
[
|
||||
'--filter',
|
||||
'[origin/master]',
|
||||
opts?.overrideChangedFilesIgnorePatternWithNoPattern
|
||||
? '--changed-files-ignore-pattern='
|
||||
: '',
|
||||
'ls',
|
||||
'--depth',
|
||||
'-1',
|
||||
'--json',
|
||||
].filter(Boolean)
|
||||
)
|
||||
return JSON.parse(result.stdout.toString())
|
||||
.map((p: { name: string }) => p.name)
|
||||
.sort()
|
||||
}
|
||||
|
||||
expect(await getChangedProjects()).toStrictEqual([
|
||||
'project-2-change-is-never-ignored',
|
||||
])
|
||||
|
||||
expect(
|
||||
await getChangedProjects({
|
||||
overrideChangedFilesIgnorePatternWithNoPattern: true,
|
||||
})
|
||||
).toStrictEqual([
|
||||
'project-2-change-is-never-ignored',
|
||||
'project-3-ignored-by-pattern',
|
||||
'project-4-ignored-by-pattern',
|
||||
'project-5-ignored-by-pattern',
|
||||
])
|
||||
})
|
||||
|
||||
test('do not get confused by filtered dependencies when searching for dependents in monorepo', async () => {
|
||||
/*
|
||||
In this test case, we are filtering for 'project-2' and its dependents with
|
||||
|
||||
Reference in New Issue
Block a user