Skip to content

Commit

Permalink
cleanup(devkit): ensure externalDependencies input from inferred task…
Browse files Browse the repository at this point in the history
… is merged into target inputs when migrating (#26273)

<!-- 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
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
  • Loading branch information
leosvelperez authored May 31, 2024
1 parent d29e314 commit c05e4ac
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,113 @@ describe('Cypress - Convert Executors To Plugin', () => {
(addedTestCypressPlugin as ExpandedPluginConfiguration).include
).toEqual(['myapp-e2e/**/*']);
});

it('should remove inputs when they are inferred', async () => {
const project = createTestProject(tree);
project.targets.e2e.options.exit = false;
updateProjectConfiguration(tree, project.name, project);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/cypress:cypress'] = {
inputs: ['default', '^default'],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toBeUndefined();
});

it('should add external dependencies input from inferred task', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/cypress:cypress'] = {
inputs: ['default', '^default', '{workspaceRoot}/some-file.ts'],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toStrictEqual([
'default',
'^default',
'{workspaceRoot}/some-file.ts',
{ externalDependencies: ['cypress'] },
]);
});

it('should merge external dependencies input from inferred task', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/cypress:cypress'] = {
inputs: [
'default',
'^default',
'{workspaceRoot}/some-file.ts',
{ externalDependencies: ['some-external-dep'] },
],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toStrictEqual([
'default',
'^default',
'{workspaceRoot}/some-file.ts',
{ externalDependencies: ['some-external-dep', 'cypress'] },
]);
});

it('should not duplicate already existing external dependencies input', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/cypress:cypress'] = {
inputs: [
'default',
'^default',
'{workspaceRoot}/some-file.ts',
{ externalDependencies: ['cypress', 'some-external-dep'] },
],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toStrictEqual([
'default',
'^default',
'{workspaceRoot}/some-file.ts',
{ externalDependencies: ['cypress', 'some-external-dep'] },
]);
});
});

describe('--all', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl';

import { minimatch } from 'minimatch';
import { deepStrictEqual } from 'node:assert';

import { forEachExecutorOptions } from '../executor-options-utils';
import { deleteMatchingProperties } from './plugin-migration-utils';
Expand All @@ -25,6 +26,7 @@ import {
ProjectConfigurationsError,
} from 'nx/src/devkit-internals';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import type { InputDefinition } from 'nx/src/config/workspace-json-project-json';

type PluginOptionsBuilder<T> = (targetName: string) => T;
type PostTargetTransformer = (
Expand Down Expand Up @@ -131,6 +133,11 @@ class ExecutorToPluginMigrator<T> {
delete projectTarget.executor;

deleteMatchingProperties(projectTarget, createdTarget);

if (projectTarget.inputs && createdTarget.inputs) {
this.#mergeInputs(projectTarget, createdTarget);
}

projectTarget = this.#postTargetTransformer(
projectTarget,
this.tree,
Expand Down Expand Up @@ -162,6 +169,55 @@ class ExecutorToPluginMigrator<T> {
return `${projectFromGraph.data.root}/**/*`;
}

#mergeInputs(
target: TargetConfiguration,
inferredTarget: TargetConfiguration
) {
const isInputInferred = (input: string | InputDefinition) => {
return inferredTarget.inputs.some((inferredInput) => {
try {
deepStrictEqual(input, inferredInput);
return true;
} catch {
return false;
}
});
};

if (target.inputs.every(isInputInferred)) {
delete target.inputs;
return;
}

const inferredTargetExternalDependencyInput = inferredTarget.inputs.find(
(i): i is { externalDependencies: string[] } =>
typeof i !== 'string' && 'externalDependencies' in i
);
if (!inferredTargetExternalDependencyInput) {
// plugins should normally have an externalDependencies input, but if it
// doesn't, there's nothing to merge
return;
}

const targetExternalDependencyInput = target.inputs.find(
(i): i is { externalDependencies: string[] } =>
typeof i !== 'string' && 'externalDependencies' in i
);
if (!targetExternalDependencyInput) {
// the target doesn't have an externalDependencies input, so we can just
// add the inferred one
target.inputs.push(inferredTargetExternalDependencyInput);
} else {
// the target has an externalDependencies input, so we need to merge them
targetExternalDependencyInput.externalDependencies = Array.from(
new Set([
...targetExternalDependencyInput.externalDependencies,
...inferredTargetExternalDependencyInput.externalDependencies,
])
);
}
}

async #pluginRequiresIncludes(
targetName: string,
plugin: ExpandedPluginConfiguration<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,123 @@ describe('Eslint - Convert Executors To Plugin', () => {
(addedTestEslintPlugin as ExpandedPluginConfiguration).include
).not.toBeDefined();
});

it('should remove inputs when they are inferred', async () => {
const project = createTestProject(tree);
project.targets.lint.options.cacheLocation = 'cache-dir';
updateProjectConfiguration(tree, project.name, project);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/eslint:lint'] = {
inputs: ['default', '^default', '{projectRoot}/.eslintrc.json'],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint.inputs).toBeUndefined();
});

it('should add external dependencies input from inferred task', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/eslint:lint'] = {
inputs: [
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint.inputs).toStrictEqual([
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
{ externalDependencies: ['eslint'] },
]);
});

it('should merge external dependencies input from inferred task', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/eslint:lint'] = {
inputs: [
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
{ externalDependencies: ['eslint-plugin-react'] },
],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint.inputs).toStrictEqual([
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
{ externalDependencies: ['eslint-plugin-react', 'eslint'] },
]);
});

it('should not duplicate already existing external dependencies input', async () => {
const project = createTestProject(tree);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/eslint:lint'] = {
inputs: [
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
{ externalDependencies: ['eslint', 'eslint-plugin-react'] },
],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint.inputs).toStrictEqual([
'default',
'{projectRoot}/.eslintrc.json',
'{projectRoot}/.eslintignore',
'{projectRoot}/eslint.config.js',
{ externalDependencies: ['eslint', 'eslint-plugin-react'] },
]);
});
});

describe('--all', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function postTargetTransformer(
projectDetails: { projectName: string; root: string }
): TargetConfiguration {
if (target.inputs) {
target.inputs = target.inputs.filter(
const inputs = target.inputs.filter(
(input) =>
typeof input === 'string' &&
![
Expand All @@ -69,7 +69,7 @@ function postTargetTransformer(
'{workspaceRoot}/eslint.config.js',
].includes(input)
);
if (target.inputs.length === 0) {
if (inputs.length === 0) {
delete target.inputs;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,54 @@ describe('Playwright - Convert Executors To Plugin', () => {
(addedTestPlaywrightPlugin as ExpandedPluginConfiguration).include
).not.toBeDefined();
});

it('should remove inputs when they are inferred', async () => {
const project = createTestProject(tree);
project.targets.e2e.options.runnerUi = true;
updateProjectConfiguration(tree, project.name, project);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/playwright:playwright'] = {
inputs: ['default', '^default'],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toBeUndefined();
});

it('should keep inputs when any of them are not inferred', async () => {
const project = createTestProject(tree);
project.targets.e2e.options.runnerUi = true;
updateProjectConfiguration(tree, project.name, project);
createTestProject(tree, { appRoot: 'second', appName: 'second' });
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/playwright:playwright'] = {
inputs: ['default', '^default', '{workspaceRoot}/some-file.ts'],
};
updateNxJson(tree, nxJson);

await convertToInferred(tree, {
project: project.name,
skipFormat: true,
});

// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.e2e.inputs).toStrictEqual([
'default',
'^default',
'{workspaceRoot}/some-file.ts',
]);
});
});

describe('--all', () => {
Expand Down

0 comments on commit c05e4ac

Please sign in to comment.