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:
parent
c49b941ad0
commit
35f54044ca
@ -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');
|
||||
|
||||
511
packages/nx/src/command-line/run/run-one.spec.ts
Normal file
511
packages/nx/src/command-line/run/run-one.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 {
|
||||
target = parsedArgs.target ?? parsedArgs['project:target:configuration'];
|
||||
} 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['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`);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user