Skip to content

Commit cc875a6

Browse files
authored
fix(devkit): check if includes is actually necessary (#23181)
<!-- 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` --> ## Current Behavior We run the createNodes function once when converting to inferred targets, and remove includes if its not needed anymore based solely on the config files returned. ## Expected Behavior We run the createNodes function twice, and remove includes if the result was the same with / without it. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent fd5ea92 commit cc875a6

File tree

3 files changed

+103
-41
lines changed

3 files changed

+103
-41
lines changed

packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class ExecutorToPluginMigrator<T> {
8181
for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
8282
this.#migrateTarget(targetName);
8383
}
84-
this.#addPlugins();
84+
await this.#addPlugins();
8585
}
8686
return this.#targetAndProjectsToMigrate;
8787
}
@@ -159,7 +159,39 @@ class ExecutorToPluginMigrator<T> {
159159
return `${projectFromGraph.data.root}/**/*`;
160160
}
161161

162-
#addPlugins() {
162+
async #pluginRequiresIncludes(
163+
targetName: string,
164+
plugin: ExpandedPluginConfiguration<T>
165+
) {
166+
const loadedPlugin = new LoadedNxPlugin(
167+
{
168+
createNodes: this.#createNodes,
169+
name: this.#pluginPath,
170+
},
171+
plugin
172+
);
173+
174+
const originalResults = this.#createNodesResultsForTargets.get(targetName);
175+
176+
let resultsWithIncludes: ConfigurationResult;
177+
try {
178+
resultsWithIncludes = await retrieveProjectConfigurations(
179+
[loadedPlugin],
180+
this.tree.root,
181+
this.#nxJson
182+
);
183+
} catch (e) {
184+
if (e instanceof ProjectConfigurationsError) {
185+
resultsWithIncludes = e.partialProjectConfigurationsResult;
186+
} else {
187+
throw e;
188+
}
189+
}
190+
191+
return !deepEqual(originalResults, resultsWithIncludes);
192+
}
193+
194+
async #addPlugins() {
163195
for (const [targetName, plugin] of this.#pluginToAddForTarget.entries()) {
164196
const pluginOptions = this.#pluginOptionsBuilder(targetName);
165197

@@ -183,42 +215,25 @@ class ExecutorToPluginMigrator<T> {
183215
) as ExpandedPluginConfiguration<T>;
184216

185217
if (existingPlugin?.include) {
186-
for (const pluginIncludes of existingPlugin.include) {
187-
for (const projectPath of plugin.include) {
188-
if (!minimatch(projectPath, pluginIncludes, { dot: true })) {
189-
existingPlugin.include.push(projectPath);
190-
}
191-
}
192-
}
193-
194-
const allConfigFilesAreIncluded = this.#configFiles.every(
195-
(configFile) => {
196-
for (const includePattern of existingPlugin.include) {
197-
if (minimatch(configFile, includePattern, { dot: true })) {
198-
return true;
199-
}
200-
}
201-
return false;
202-
}
218+
// Add to the existing plugin includes
219+
existingPlugin.include = existingPlugin.include.concat(
220+
// Any include that is in the new plugin's include list
221+
plugin.include.filter(
222+
(projectPath) =>
223+
// And is not already covered by the existing plugin's include list
224+
!existingPlugin.include.some((pluginIncludes) =>
225+
minimatch(projectPath, pluginIncludes, { dot: true })
226+
)
227+
)
203228
);
204229

205-
if (allConfigFilesAreIncluded) {
206-
existingPlugin.include = undefined;
230+
if (!(await this.#pluginRequiresIncludes(targetName, existingPlugin))) {
231+
delete existingPlugin.include;
207232
}
208233
}
209234

210235
if (!existingPlugin) {
211-
const allConfigFilesAreIncluded = this.#configFiles.every(
212-
(configFile) => {
213-
for (const includePattern of plugin.include) {
214-
if (minimatch(configFile, includePattern, { dot: true })) {
215-
return true;
216-
}
217-
}
218-
return false;
219-
}
220-
);
221-
if (allConfigFilesAreIncluded) {
236+
if (!(await this.#pluginRequiresIncludes(targetName, plugin))) {
222237
plugin.include = undefined;
223238
}
224239
this.#nxJson.plugins.push(plugin);
@@ -274,11 +289,17 @@ class ExecutorToPluginMigrator<T> {
274289
}
275290

276291
#getCreatedTargetForProjectRoot(targetName: string, projectRoot: string) {
277-
const createdProject = Object.entries(
292+
const entry = Object.entries(
278293
this.#createNodesResultsForTargets.get(targetName)?.projects ?? {}
279-
).find(([root]) => root === projectRoot)[1];
294+
).find(([root]) => root === projectRoot);
295+
if (!entry) {
296+
throw new Error(
297+
`The nx plugin did not find a project inside ${projectRoot}. File an issue at https://github.com/nrwl/nx with information about your project structure.`
298+
);
299+
}
300+
const createdProject = entry[1];
280301
const createdTarget: TargetConfiguration<RunCommandsOptions> =
281-
createdProject.targets[targetName];
302+
structuredClone(createdProject.targets[targetName]);
282303
delete createdTarget.command;
283304
delete createdTarget.options?.cwd;
284305

@@ -346,3 +367,30 @@ export async function migrateExecutorToPlugin<T>(
346367
);
347368
return await migrator.run();
348369
}
370+
371+
// Checks if two objects are structurely equal, without caring
372+
// about the order of the keys.
373+
function deepEqual<T extends Object>(a: T, b: T, logKey = ''): boolean {
374+
const aKeys = Object.keys(a);
375+
const bKeys = new Set(Object.keys(b));
376+
377+
if (aKeys.length !== bKeys.size) {
378+
return false;
379+
}
380+
381+
for (const key of aKeys) {
382+
if (!bKeys.has(key)) {
383+
return false;
384+
}
385+
386+
if (typeof a[key] === 'object' && typeof b[key] === 'object') {
387+
if (!deepEqual(a[key], b[key], logKey + '.' + key)) {
388+
return false;
389+
}
390+
} else if (a[key] !== b[key]) {
391+
return false;
392+
}
393+
}
394+
395+
return true;
396+
}

packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,21 @@ describe('Eslint - Convert Executors To Plugin', () => {
377377
});
378378

379379
it('should remove include when all projects are included', async () => {
380+
jest.doMock(
381+
'.eslintrc.base.json',
382+
() => ({
383+
ignorePatterns: ['**/*'],
384+
}),
385+
{ virtual: true }
386+
);
387+
fs.createFileSync(
388+
'.eslintrc.base.json',
389+
JSON.stringify({ ignorePatterns: ['**/*'] })
390+
);
391+
tree.write(
392+
'.eslintrc.base.json',
393+
JSON.stringify({ ignorePatterns: ['**/*'] })
394+
);
380395
// ARRANGE
381396
const existingProject = createTestProject(tree, {
382397
appRoot: 'existing',

packages/eslint/src/plugins/plugin.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,11 @@ function getProjectsUsingESLintConfig(
116116
): CreateNodesResult['projects'] {
117117
const projects: CreateNodesResult['projects'] = {};
118118

119-
const rootEslintConfig = context.configFiles.find(
120-
(f) =>
121-
f === baseEsLintConfigFile ||
122-
f === baseEsLintFlatConfigFile ||
123-
ESLINT_CONFIG_FILENAMES.includes(f)
124-
);
119+
const rootEslintConfig = [
120+
baseEsLintConfigFile,
121+
baseEsLintFlatConfigFile,
122+
...ESLINT_CONFIG_FILENAMES,
123+
].find((f) => existsSync(join(context.workspaceRoot, f)));
125124

126125
// Add a lint target for each child project without an eslint config, with the root level config as an input
127126
for (const projectRoot of childProjectRoots) {

0 commit comments

Comments
 (0)