Skip to content

Commit 754b01a

Browse files
llwtclaude
andauthored
feat(core): add negation pattern support for plugin include/exclude (#34160)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior Negation patterns are ignored in plugin configuration for the `include` and `exclude` properties. ## Expected Behavior - Negation patterns should work in the same way that they do for other `include`/`exclude` configurations **Example: Excluding all e2e projects except one** ```jsonc // nx.json { "plugins": [ { "plugin": "@nx/jest/plugin", "exclude": ["**/*-e2e/**/*", "!**/toolkit-workspace-e2e/**/*"], }, ], } ``` This will exclude all e2e projects except `toolkit-workspace-e2e`. **Example: Including packages except legacy ones** ```jsonc // nx.json { "plugins": [ { "plugin": "@nx/vite/plugin", "include": ["packages/**/*", "!packages/legacy/**/*"], }, ], } ``` **How negation patterns work:** - Patterns are processed in order from first to last - A pattern starting with `!` removes files from the match set - A pattern without `!` adds files to the match set - The last matching pattern determines if a file is included - If the first pattern is a negation, all files are matched initially --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5a1735b commit 754b01a

3 files changed

Lines changed: 390 additions & 14 deletions

File tree

astro-docs/src/content/docs/reference/nx-json.mdoc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,48 @@ Plugins use config files to infer tasks for projects. You can specify which conf
136136

137137
The `include` and `exclude` properties are each file glob patterns that are used to include or exclude the configuration file that the plugin is interpreting. In the example provided, the `@nx/jest/plugin` plugin will only infer tasks for projects where the `jest.config.ts` file path matches the `packages/**/*` glob but does not match the `**/*-e2e/**/*` glob.
138138

139+
#### Using Negation Patterns
140+
141+
You can use negation patterns (patterns starting with `!`) to create more precise include/exclude rules. Patterns are processed in order, with later patterns overriding earlier ones.
142+
143+
**Example: Excluding all e2e projects except one**
144+
145+
```jsonc
146+
// nx.json
147+
{
148+
"plugins": [
149+
{
150+
"plugin": "@nx/jest/plugin",
151+
"exclude": ["**/*-e2e/**/*", "!**/toolkit-workspace-e2e/**/*"],
152+
},
153+
],
154+
}
155+
```
156+
157+
This will exclude all e2e projects except `toolkit-workspace-e2e`.
158+
159+
**Example: Including packages except legacy ones**
160+
161+
```jsonc
162+
// nx.json
163+
{
164+
"plugins": [
165+
{
166+
"plugin": "@nx/vite/plugin",
167+
"include": ["packages/**/*", "!packages/legacy/**/*"],
168+
},
169+
],
170+
}
171+
```
172+
173+
**How negation patterns work:**
174+
175+
- Patterns are processed in order from first to last
176+
- A pattern starting with `!` removes files from the match set
177+
- A pattern without `!` adds files to the match set
178+
- The last matching pattern determines if a file is included
179+
- If the first pattern is a negation, all files are matched initially
180+
139181
## Task Options
140182

141183
The following properties affect the way Nx runs tasks and can be set at the root of `nx.json`.

packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,6 +1861,294 @@ describe('project-configuration-utils', () => {
18611861
});
18621862
});
18631863

1864+
describe('negation pattern support', () => {
1865+
it('should support negation patterns in exclude to re-include specific files', async () => {
1866+
const projectConfigurations =
1867+
await createProjectConfigurationsWithPlugins(
1868+
undefined,
1869+
{},
1870+
[
1871+
[
1872+
'libs/a-e2e/project.json',
1873+
'libs/b-e2e/project.json',
1874+
'libs/toolkit-workspace-e2e/project.json',
1875+
],
1876+
],
1877+
[
1878+
new LoadedNxPlugin(fakeTagPlugin, {
1879+
plugin: fakeTagPlugin.name,
1880+
exclude: ['**/*-e2e/**', '!**/toolkit-workspace-e2e/**'],
1881+
}),
1882+
]
1883+
);
1884+
1885+
expect(projectConfigurations.projects).toEqual({
1886+
'libs/toolkit-workspace-e2e': {
1887+
name: 'toolkit-workspace-e2e',
1888+
root: 'libs/toolkit-workspace-e2e',
1889+
tags: ['fake-lib'],
1890+
},
1891+
});
1892+
});
1893+
1894+
it('should support negation patterns in include to exclude specific files', async () => {
1895+
const projectConfigurations =
1896+
await createProjectConfigurationsWithPlugins(
1897+
undefined,
1898+
{},
1899+
[
1900+
[
1901+
'libs/a/project.json',
1902+
'libs/b/project.json',
1903+
'libs/c/project.json',
1904+
],
1905+
],
1906+
[
1907+
new LoadedNxPlugin(fakeTagPlugin, {
1908+
plugin: fakeTagPlugin.name,
1909+
include: ['libs/**', '!libs/b/**'],
1910+
}),
1911+
]
1912+
);
1913+
1914+
expect(projectConfigurations.projects).toEqual({
1915+
'libs/a': {
1916+
name: 'a',
1917+
root: 'libs/a',
1918+
tags: ['fake-lib'],
1919+
},
1920+
'libs/c': {
1921+
name: 'c',
1922+
root: 'libs/c',
1923+
tags: ['fake-lib'],
1924+
},
1925+
});
1926+
});
1927+
1928+
it('should handle multiple negation patterns correctly', async () => {
1929+
const projectConfigurations =
1930+
await createProjectConfigurationsWithPlugins(
1931+
undefined,
1932+
{},
1933+
[
1934+
[
1935+
'libs/a/project.json',
1936+
'libs/b/project.json',
1937+
'libs/c/project.json',
1938+
'libs/d/project.json',
1939+
],
1940+
],
1941+
[
1942+
new LoadedNxPlugin(fakeTagPlugin, {
1943+
plugin: fakeTagPlugin.name,
1944+
exclude: ['libs/**', '!libs/b/**', '!libs/c/**'],
1945+
}),
1946+
]
1947+
);
1948+
1949+
expect(projectConfigurations.projects).toEqual({
1950+
'libs/b': {
1951+
name: 'b',
1952+
root: 'libs/b',
1953+
tags: ['fake-lib'],
1954+
},
1955+
'libs/c': {
1956+
name: 'c',
1957+
root: 'libs/c',
1958+
tags: ['fake-lib'],
1959+
},
1960+
});
1961+
});
1962+
1963+
it('should handle starting with negation pattern in exclude', async () => {
1964+
const projectConfigurations =
1965+
await createProjectConfigurationsWithPlugins(
1966+
undefined,
1967+
{},
1968+
[
1969+
[
1970+
'libs/a/project.json',
1971+
'libs/b/project.json',
1972+
'libs/c/project.json',
1973+
],
1974+
],
1975+
[
1976+
new LoadedNxPlugin(fakeTagPlugin, {
1977+
plugin: fakeTagPlugin.name,
1978+
exclude: ['!libs/a/**'],
1979+
}),
1980+
]
1981+
);
1982+
1983+
// Should exclude everything except libs/a (first pattern is negation)
1984+
expect(projectConfigurations.projects).toEqual({
1985+
'libs/a': {
1986+
name: 'a',
1987+
root: 'libs/a',
1988+
tags: ['fake-lib'],
1989+
},
1990+
});
1991+
});
1992+
1993+
it('should handle starting with negation pattern in include', async () => {
1994+
const projectConfigurations =
1995+
await createProjectConfigurationsWithPlugins(
1996+
undefined,
1997+
{},
1998+
[
1999+
[
2000+
'libs/a/project.json',
2001+
'libs/b/project.json',
2002+
'libs/c/project.json',
2003+
],
2004+
],
2005+
[
2006+
new LoadedNxPlugin(fakeTagPlugin, {
2007+
plugin: fakeTagPlugin.name,
2008+
include: ['!libs/b/**'],
2009+
}),
2010+
]
2011+
);
2012+
2013+
// Should include everything except libs/b (first pattern is negation)
2014+
expect(projectConfigurations.projects).toEqual({
2015+
'libs/a': {
2016+
name: 'a',
2017+
root: 'libs/a',
2018+
tags: ['fake-lib'],
2019+
},
2020+
'libs/c': {
2021+
name: 'c',
2022+
root: 'libs/c',
2023+
tags: ['fake-lib'],
2024+
},
2025+
});
2026+
});
2027+
2028+
it('should maintain backward compatibility with non-negation patterns', async () => {
2029+
const projectConfigurations =
2030+
await createProjectConfigurationsWithPlugins(
2031+
undefined,
2032+
{},
2033+
[['libs/a/project.json', 'libs/b/project.json']],
2034+
[
2035+
new LoadedNxPlugin(fakeTagPlugin, {
2036+
plugin: fakeTagPlugin.name,
2037+
include: ['libs/a/**'],
2038+
exclude: ['libs/b/**'],
2039+
}),
2040+
]
2041+
);
2042+
2043+
expect(projectConfigurations.projects).toEqual({
2044+
'libs/a': {
2045+
name: 'a',
2046+
root: 'libs/a',
2047+
tags: ['fake-lib'],
2048+
},
2049+
});
2050+
});
2051+
2052+
it('should handle overlapping patterns with last match winning', async () => {
2053+
const projectConfigurations =
2054+
await createProjectConfigurationsWithPlugins(
2055+
undefined,
2056+
{},
2057+
[
2058+
[
2059+
'libs/a/project.json',
2060+
'libs/a/special/project.json',
2061+
'libs/b/project.json',
2062+
],
2063+
],
2064+
[
2065+
new LoadedNxPlugin(fakeTagPlugin, {
2066+
plugin: fakeTagPlugin.name,
2067+
exclude: ['libs/**', '!libs/a/**', 'libs/a/special/**'],
2068+
}),
2069+
]
2070+
);
2071+
2072+
// Exclude all libs, except a, but re-exclude a/special (last match wins)
2073+
expect(projectConfigurations.projects).toEqual({
2074+
'libs/a': {
2075+
name: 'a',
2076+
root: 'libs/a',
2077+
tags: ['fake-lib'],
2078+
},
2079+
});
2080+
});
2081+
2082+
it('should work with both include and exclude having negation patterns', async () => {
2083+
const projectConfigurations =
2084+
await createProjectConfigurationsWithPlugins(
2085+
undefined,
2086+
{},
2087+
[
2088+
[
2089+
'libs/a/project.json',
2090+
'libs/b/project.json',
2091+
'libs/c/project.json',
2092+
'libs/d/project.json',
2093+
],
2094+
],
2095+
[
2096+
new LoadedNxPlugin(fakeTagPlugin, {
2097+
plugin: fakeTagPlugin.name,
2098+
include: ['libs/**', '!libs/d/**'],
2099+
exclude: ['libs/b/**', '!libs/c/**'],
2100+
}),
2101+
]
2102+
);
2103+
2104+
// Include: a, b, c (all except d)
2105+
// Exclude: b (but not c due to negation)
2106+
// Result: a, c
2107+
expect(projectConfigurations.projects).toEqual({
2108+
'libs/a': {
2109+
name: 'a',
2110+
root: 'libs/a',
2111+
tags: ['fake-lib'],
2112+
},
2113+
'libs/c': {
2114+
name: 'c',
2115+
root: 'libs/c',
2116+
tags: ['fake-lib'],
2117+
},
2118+
});
2119+
});
2120+
2121+
it('should handle empty arrays with negation support intact', async () => {
2122+
const projectConfigurations =
2123+
await createProjectConfigurationsWithPlugins(
2124+
undefined,
2125+
{},
2126+
[['libs/a/project.json', 'libs/b/project.json']],
2127+
[
2128+
new LoadedNxPlugin(fakeTagPlugin, {
2129+
plugin: fakeTagPlugin.name,
2130+
include: [],
2131+
exclude: [],
2132+
}),
2133+
]
2134+
);
2135+
2136+
// Empty arrays should not filter anything
2137+
expect(projectConfigurations.projects).toEqual({
2138+
'libs/a': {
2139+
name: 'a',
2140+
root: 'libs/a',
2141+
tags: ['fake-lib'],
2142+
},
2143+
'libs/b': {
2144+
name: 'b',
2145+
root: 'libs/b',
2146+
tags: ['fake-lib'],
2147+
},
2148+
});
2149+
});
2150+
});
2151+
18642152
it('should normalize targets', async () => {
18652153
const { projects } = await createProjectConfigurationsWithPlugins(
18662154
undefined,

0 commit comments

Comments
 (0)