fix(core): default to 'run' target when only project is specified (#31452)

## Current Behavior

When running `nx run <project>` without specifying a target, the command
always fails with an error message "Both project and target have to be
specified", even if the project has a "run" target defined.

## Expected Behavior

When running `nx run <project>` without specifying a target, the command
should check if the project has a "run" target defined. If it does, use
it as the default target. This improves developer experience by allowing
simpler commands like `nx run myapp` instead of `nx run myapp:run`.

## Related Issue(s)

This change improves the developer experience for projects that have a
"run" target defined, making the CLI more intuitive.

## Changes Made

- Modified `packages/nx/src/command-line/run/run-one.ts` to check for a
"run" target when no target is specified
- Added comprehensive test coverage in `e2e/nx/src/run.test.ts` to
verify:
- Projects with a "run" target default to it when no target is specified
- Projects without a "run" target still show the original error message
- Maintains full backward compatibility

## Testing

- All existing tests pass
- Added new e2e tests to verify the behavior
- Ran full validation suite (`nx prepush`) successfully
This commit is contained in:
Jason Jean 2025-06-09 12:13:47 -04:00 committed by GitHub
parent c49b941ad0
commit 35f54044ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 553 additions and 4 deletions

View File

@ -453,6 +453,33 @@ describe('Nx Running Tests', () => {
);
}, 10000);
it('should default to "run" target when only project is specified and it has a run target', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app apps/${myapp}`);
// Add a "run" target to the project
updateJson(`apps/${myapp}/project.json`, (c) => {
c.targets['run'] = {
command: 'echo Running the app',
};
return c;
});
// Running with just the project name should default to the "run" target
const output = runCLI(`run ${myapp}`);
expect(output).toContain('Running the app');
expect(output).toContain(`nx run ${myapp}:run`);
});
it('should still require target when project does not have a run target', () => {
const myapp = uniq('app');
runCLI(`generate @nx/web:app apps/${myapp}`);
// Project has no "run" target, so it should fail
const result = runCLI(`run ${myapp}`, { silenceError: true });
expect(result).toContain('Both project and target have to be specified');
});
describe('target defaults + executor specifications', () => {
it('should be able to run targets with unspecified executor given an appropriate targetDefaults entry', () => {
const target = uniq('target');

View File

@ -0,0 +1,511 @@
import { ProjectGraph } from '../../config/project-graph';
import { NxJsonConfiguration } from '../../config/nx-json';
import { parseRunOneOptions } from './run-one';
describe('parseRunOneOptions', () => {
let projectGraph: ProjectGraph;
let nxJson: NxJsonConfiguration;
const testCwd = '/test/workspace';
beforeEach(() => {
projectGraph = {
nodes: {
'my-app': {
name: 'my-app',
type: 'app',
data: {
root: 'apps/my-app',
targets: {
build: { executor: '@nx/webpack:webpack' },
serve: { executor: '@nx/webpack:dev-server' },
run: { executor: '@nx/js:node' },
},
},
},
'my-lib': {
name: 'my-lib',
type: 'lib',
data: {
root: 'libs/my-lib',
targets: {
build: { executor: '@nx/js:tsc' },
test: { executor: '@nx/jest:jest' },
},
},
},
'default-project': {
name: 'default-project',
type: 'app',
data: {
root: '.',
targets: {
build: { executor: '@nx/js:tsc' },
serve: { executor: '@nx/webpack:dev-server' },
},
},
},
},
dependencies: {},
};
nxJson = {
defaultProject: 'default-project',
};
});
describe('when project:target:configuration contains colon', () => {
it('should parse project:target:configuration format', () => {
const parsedArgs = {
'project:target:configuration': 'my-app:build:production',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('my-app');
expect(result.target).toBe('build');
expect(result.configuration).toBe('production');
});
it('should parse project:target format without configuration', () => {
const parsedArgs = {
'project:target:configuration': 'my-app:build',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('my-app');
expect(result.target).toBe('build');
expect(result.configuration).toBeUndefined();
});
});
describe('when parsedArgs.target is provided', () => {
it('should use the provided target', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('build');
expect(result.project).toBe('my-app');
});
});
describe('when project has run target', () => {
it('should set target to "run" when project exists with run target', () => {
const parsedArgs = {
'project:target:configuration': 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('run');
expect(result.project).toBe('my-app');
});
it('should use argument as target when project does not have run target', () => {
const parsedArgs = {
'project:target:configuration': 'my-lib',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('my-lib');
expect(result.project).toBe('default-project');
});
});
describe('when no special conditions are met', () => {
it('should use project:target:configuration as target', () => {
const parsedArgs = {
'project:target:configuration': 'build',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('build');
expect(result.project).toBe('my-app');
});
});
describe('project resolution', () => {
it('should use parsedArgs.project when provided', () => {
const parsedArgs = {
'project:target:configuration': 'build',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('my-app');
});
it('should use default project when no project specified and cwd is workspace root', () => {
const parsedArgs = {
target: 'build',
};
// Test with cwd at workspace root to trigger default project logic
const result = parseRunOneOptions(
'/test/workspace',
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('default-project');
});
});
describe('error handling', () => {
it('should throw error when target is missing', () => {
const parsedArgs = {
project: 'my-app',
// No target and no 'project:target:configuration'
};
expect(() => {
parseRunOneOptions(
'/some/other/path',
parsedArgs,
projectGraph,
nxJson
);
}).toThrow('Both project and target have to be specified');
});
it('should parse successfully when "nx run app" is used even if app does not have run target', () => {
const parsedArgs = {
target: 'run',
project: 'my-lib', // my-lib doesn't have a run target in our test setup
};
// The function should parse successfully - target validation happens later in the pipeline
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('my-lib');
expect(result.target).toBe('run');
});
});
describe('target aliases', () => {
it('should resolve target alias "b" to "build"', () => {
const parsedArgs = {
target: 'b',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('build');
});
it('should resolve target alias "e" to "e2e"', () => {
const parsedArgs = {
target: 'e',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('e2e');
});
it('should resolve target alias "l" to "lint"', () => {
const parsedArgs = {
target: 'l',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('lint');
});
it('should resolve target alias "s" to "serve"', () => {
const parsedArgs = {
target: 's',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('serve');
});
it('should resolve target alias "t" to "test"', () => {
const parsedArgs = {
target: 't',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('test');
});
it('should not resolve non-alias targets', () => {
const parsedArgs = {
target: 'custom-target',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('custom-target');
});
});
describe('configuration handling', () => {
it('should use parsedArgs.configuration when provided', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
configuration: 'staging',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.configuration).toBe('staging');
});
it('should set configuration to "production" when prod flag is true', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
prod: true,
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.configuration).toBe('production');
});
it('should prefer explicit configuration over prod flag', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
configuration: 'staging',
prod: true,
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.configuration).toBe('staging');
});
it('should leave configuration undefined when neither flag is set', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.configuration).toBeUndefined();
});
});
describe('parsedArgs cleanup', () => {
it('should remove specific properties from parsedArgs', () => {
const parsedArgs = {
'project:target:configuration': 'my-app:build',
target: 'build',
project: 'my-app',
configuration: 'production',
prod: true,
c: 'some-value',
otherProperty: 'should-remain',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.parsedArgs).toEqual({
otherProperty: 'should-remain',
target: 'build',
});
expect(result.parsedArgs['project:target:configuration']).toBeUndefined();
expect(result.parsedArgs.project).toBeUndefined();
expect(result.parsedArgs.configuration).toBeUndefined();
expect(result.parsedArgs.prod).toBeUndefined();
expect(result.parsedArgs.c).toBeUndefined();
});
it('should preserve other properties in parsedArgs', () => {
const parsedArgs = {
target: 'build',
project: 'my-app',
verbose: true,
output: 'dist',
customFlag: 'value',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.parsedArgs).toEqual({
verbose: true,
output: 'dist',
customFlag: 'value',
target: 'build',
});
});
});
describe('complex scenarios', () => {
it('should handle project:target format without configuration', () => {
const parsedArgs = {
'project:target:configuration': 'my-app:build',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('my-app');
expect(result.target).toBe('build');
expect(result.configuration).toBeUndefined();
});
it('should handle target alias with configuration', () => {
const parsedArgs = {
target: 's',
project: 'my-app',
configuration: 'development',
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.target).toBe('serve');
expect(result.configuration).toBe('development');
});
it('should use default project with target alias and prod flag', () => {
const parsedArgs = {
target: 'b',
prod: true,
};
const result = parseRunOneOptions(
testCwd,
parsedArgs,
projectGraph,
nxJson
);
expect(result.project).toBe('default-project');
expect(result.target).toBe('build');
expect(result.configuration).toBe('production');
});
});
});

View File

@ -4,7 +4,6 @@ import {
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { connectToNxCloudIfExplicitlyAsked } from '../connect/connect-to-nx-cloud';
import { performance } from 'perf_hooks';
import {
createProjectGraphAsync,
readProjectsConfigurationFromProjectGraph,
@ -148,7 +147,7 @@ const targetAliases = {
t: 'test',
};
function parseRunOneOptions(
export function parseRunOneOptions(
cwd: string,
parsedArgs: { [k: string]: any },
projectGraph: ProjectGraph,
@ -176,8 +175,19 @@ function parseRunOneOptions(
target = project;
project = defaultProjectName;
}
} else if (parsedArgs.target) {
target = parsedArgs.target;
} else if (parsedArgs['project:target:configuration']) {
// If project:target:configuration exists but has no colon, check if it's a project with run target
if (
projectGraph.nodes[parsedArgs['project:target:configuration']]?.data
?.targets?.run
) {
target = 'run';
project = parsedArgs['project:target:configuration'];
} else {
target = parsedArgs.target ?? parsedArgs['project:target:configuration'];
target = parsedArgs['project:target:configuration'];
}
}
if (parsedArgs.project) {
project = parsedArgs.project;
@ -185,6 +195,7 @@ function parseRunOneOptions(
if (!project && defaultProjectName) {
project = defaultProjectName;
}
if (!project || !target) {
throw new Error(`Both project and target have to be specified`);
}