diff --git a/packages/nx/src/project-graph/utils/normalize-project-nodes.ts b/packages/nx/src/project-graph/utils/normalize-project-nodes.ts index 2b4f8f5f2b35cf..c7e12a17e085f5 100644 --- a/packages/nx/src/project-graph/utils/normalize-project-nodes.ts +++ b/packages/nx/src/project-graph/utils/normalize-project-nodes.ts @@ -94,11 +94,6 @@ export function normalizeProjectTargets( continue; } - targets[target] = resolveCommandSyntacticSugar( - targets[target], - `${projectName}:${target}` - ); - targets[target].options = resolveNxTokensInOptions( targets[target].options, project, @@ -134,29 +129,3 @@ export function normalizeImplicitDependencies( .concat(implicitDependencies.filter((x) => x.startsWith('!'))) ); } - -function resolveCommandSyntacticSugar( - target: TargetConfiguration, - key: string -): TargetConfiguration { - const { command, ...config } = target ?? {}; - - if (!command) { - return target; - } - - if (config.executor) { - throw new Error( - `${NX_PREFIX} ${key} should not have executor and command both configured.` - ); - } else { - return { - ...config, - executor: 'nx:run-commands', - options: { - ...config.options, - command: command, - }, - }; - } -} diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts index 7df856b50cea66..43fe3ddcf2f769 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts @@ -303,6 +303,106 @@ describe('project-configuration-utils', () => { ).toEqual(projectDefaultConfiguration); }); }); + + describe('run-commands', () => { + it('should merge two run-commands targets appropriately', () => { + const merged = mergeTargetConfigurations( + { + outputs: ['{projectRoot}/outputfile.json'], + options: { + command: 'eslint . -o outputfile.json', + }, + }, + { + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/apps/third-app/.eslintrc.json', + '{workspaceRoot}/tools/eslint-rules/**/*', + { externalDependencies: ['eslint'] }, + ], + options: { cwd: 'apps/third-app', command: 'eslint .' }, + executor: 'nx:run-commands', + configurations: {}, + } + ); + expect(merged).toMatchInlineSnapshot(` + { + "cache": true, + "configurations": {}, + "executor": "nx:run-commands", + "inputs": [ + "default", + "{workspaceRoot}/.eslintrc.json", + "{workspaceRoot}/apps/third-app/.eslintrc.json", + "{workspaceRoot}/tools/eslint-rules/**/*", + { + "externalDependencies": [ + "eslint", + ], + }, + ], + "options": { + "command": "eslint . -o outputfile.json", + "cwd": "apps/third-app", + }, + "outputs": [ + "{projectRoot}/outputfile.json", + ], + } + `); + }); + + it('should merge targets when the base uses command syntactic sugar', () => { + const merged = mergeTargetConfigurations( + { + outputs: ['{projectRoot}/outputfile.json'], + options: { + command: 'eslint . -o outputfile.json', + }, + }, + { + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/apps/third-app/.eslintrc.json', + '{workspaceRoot}/tools/eslint-rules/**/*', + { externalDependencies: ['eslint'] }, + ], + options: { cwd: 'apps/third-app' }, + configurations: {}, + command: 'eslint .', + } + ); + expect(merged).toMatchInlineSnapshot(` + { + "cache": true, + "command": "eslint .", + "configurations": {}, + "inputs": [ + "default", + "{workspaceRoot}/.eslintrc.json", + "{workspaceRoot}/apps/third-app/.eslintrc.json", + "{workspaceRoot}/tools/eslint-rules/**/*", + { + "externalDependencies": [ + "eslint", + ], + }, + ], + "options": { + "command": "eslint . -o outputfile.json", + "cwd": "apps/third-app", + }, + "outputs": [ + "{projectRoot}/outputfile.json", + ], + } + `); + }); + }); }); describe('mergeProjectConfigurationIntoRootMap', () => { @@ -1081,18 +1181,10 @@ describe('project-configuration-utils', () => { expect( isCompatibleTarget( { - command: 'echo', - }, - { - command: 'echo', - } - ) - ).toBe(true); - - expect( - isCompatibleTarget( - { - command: 'echo', + executor: 'nx:run-commands', + options: { + command: 'echo', + }, }, { executor: 'nx:run-commands', @@ -1108,18 +1200,10 @@ describe('project-configuration-utils', () => { expect( isCompatibleTarget( { - command: 'echo', - }, - { - command: 'echo2', - } - ) - ).toBe(false); - - expect( - isCompatibleTarget( - { - command: 'echo', + executor: 'nx:run-commands', + options: { + command: 'echo', + }, }, { executor: 'nx:run-commands', @@ -1130,19 +1214,6 @@ describe('project-configuration-utils', () => { ) ).toBe(false); }); - - it('should return false if one target specifies an executor and the other a command', () => { - expect( - isCompatibleTarget( - { - executor: 'nx:noop', - }, - { - command: 'echo', - } - ) - ).toBe(false); - }); }); }); diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index d7105f94ac96d5..79e13d2320dc71 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -133,36 +133,37 @@ export function mergeProjectConfigurationIntoRootMap( updatedProjectConfiguration.targets = matchingProject?.targets ?? {}; // For each target defined in the new config - for (const target in project.targets) { + for (const targetName in project.targets) { // Always set source map info for the target, but don't overwrite info already there // if augmenting an existing target. - if ( - sourceMap && - !project.targets[target]?.[ONLY_MODIFIES_EXISTING_TARGET] - ) { - sourceMap[`targets.${target}`] = sourceInformation; + + const target = project.targets?.[targetName]; + + if (sourceMap && !target?.[ONLY_MODIFIES_EXISTING_TARGET]) { + sourceMap[`targets.${targetName}`] = sourceInformation; } // If ONLY_MODIFIES_EXISTING_TARGET is true, and its not on the matching project // we shouldn't merge its info into the graph if ( - project.targets[target]?.[ONLY_MODIFIES_EXISTING_TARGET] && - !matchingProject.targets?.[target] + target?.[ONLY_MODIFIES_EXISTING_TARGET] && + !matchingProject.targets?.[targetName] ) { continue; } // We don't want the symbol to live on past the merge process - if (project.targets[target]?.[ONLY_MODIFIES_EXISTING_TARGET]) - delete project.targets[target]?.[ONLY_MODIFIES_EXISTING_TARGET]; - - updatedProjectConfiguration.targets[target] = mergeTargetConfigurations( - project.targets[target], - matchingProject.targets?.[target], - sourceMap, - sourceInformation, - `targets.${target}` - ); + if (target?.[ONLY_MODIFIES_EXISTING_TARGET]) + delete target?.[ONLY_MODIFIES_EXISTING_TARGET]; + + updatedProjectConfiguration.targets[targetName] = + mergeTargetConfigurations( + resolveCommandSyntacticSugar(target, project.root), + matchingProject.targets?.[targetName], + sourceMap, + sourceInformation, + `targets.${target}` + ); } } @@ -487,25 +488,35 @@ export function isCompatibleTarget( a: TargetConfiguration, b: TargetConfiguration ) { - if (a.command || b.command) { - const aCommand = - a.command ?? - (a.executor === 'nx:run-commands' ? a.options?.command : null); - const bCommand = - b.command ?? - (b.executor === 'nx:run-commands' ? b.options?.command : null); - - const sameCommand = aCommand === bCommand; - const aHasNoExecutor = !a.command && !a.executor; - const bHasNoExecutor = !b.command && !b.executor; - - return sameCommand || aHasNoExecutor || bHasNoExecutor; - } - const oneHasNoExecutor = !a.executor || !b.executor; const bothHaveSameExecutor = a.executor === b.executor; - return oneHasNoExecutor || bothHaveSameExecutor; + if (oneHasNoExecutor) return true; + if (!bothHaveSameExecutor) return false; + + const isRunCommands = a.executor === 'nx:run-commands'; + if (isRunCommands) { + const aCommand = a.options?.command ?? a.options?.commands.join(' && '); + const bCommand = b.options?.command ?? b.options?.commands.join(' && '); + + const oneHasNoCommand = !aCommand || !bCommand; + const hasSameCommand = aCommand === bCommand; + + return oneHasNoCommand || hasSameCommand; + } + + const isRunScript = a.executor === 'nx:run-script'; + if (isRunScript) { + const aScript = a.options?.script; + const bScript = b.options?.script; + + const oneHasNoScript = !aScript || !bScript; + const hasSameScript = aScript === bScript; + + return oneHasNoScript || hasSameScript; + } + + return true; } function mergeConfigurations( @@ -616,6 +627,7 @@ export function readTargetDefaultsForTarget( return targetDefaults?.[targetName]; } } + function createRootMap(projectRootMap: Map) { const map: Record = {}; for (const [projectRoot, { name: projectName }] of projectRootMap) { @@ -623,3 +635,29 @@ function createRootMap(projectRootMap: Map) { } return map; } + +function resolveCommandSyntacticSugar( + target: TargetConfiguration, + key: string +): TargetConfiguration { + const { command, ...config } = target ?? {}; + + if (!command) { + return target; + } + + if (config.executor) { + throw new Error( + `${NX_PREFIX} Project at ${key} should not have executor and command both configured.` + ); + } else { + return { + ...config, + executor: 'nx:run-commands', + options: { + ...config.options, + command: command, + }, + }; + } +}