Skip to content

Commit

Permalink
fix(core): support invoking nx from a subdirectory
Browse files Browse the repository at this point in the history
Closes #4298
  • Loading branch information
vsavkin committed Dec 18, 2020
1 parent 399ceba commit 86b5c5c
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 29 deletions.
5 changes: 5 additions & 0 deletions e2e/workspace/src/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ describe('run-one', () => {

// configuration has to be valid for the initiating project
expect(() => runCLI(`build ${myapp} -c=invalid`)).toThrow();
expect(
runCommand(
`cd apps/${myapp}-e2e/src && ../../../node_modules/.bin/nx lint`
)
).toContain(`nx run ${myapp}-e2e:lint`);

// configuration doesn't have to exists for deps (here only the app has production)
const buildWithDeps = runCLI(`build ${myapp} --with-deps --prod`);
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/lib/init-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ function runOneOptions(
.toString()
);

return parseRunOneOptions(workspaceConfigJson, process.argv.slice(2));
return parseRunOneOptions(
workspace.dir,
workspaceConfigJson,
process.argv.slice(2)
);
} catch (e) {
return false;
}
Expand Down
27 changes: 14 additions & 13 deletions packages/cli/lib/parse-run-one-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('parseRunOneOptions', () => {
const args = ['build', 'myproj', '--configuration=production', '--flag=true'];

it('should work', () => {
expect(parseRunOneOptions(workspaceJson, args)).toEqual({
expect(parseRunOneOptions('root', workspaceJson, args)).toEqual({
project: 'myproj',
target: 'build',
configuration: 'production',
Expand All @@ -15,7 +15,7 @@ describe('parseRunOneOptions', () => {

it('should work with --prod', () => {
expect(
parseRunOneOptions(workspaceJson, [
parseRunOneOptions('root', workspaceJson, [
'build',
'myproj',
'--prod',
Expand All @@ -31,7 +31,7 @@ describe('parseRunOneOptions', () => {

it('should override --prod with --configuration', () => {
expect(
parseRunOneOptions(workspaceJson, [
parseRunOneOptions('root', workspaceJson, [
'build',
'myproj',
'--prod',
Expand All @@ -49,7 +49,7 @@ describe('parseRunOneOptions', () => {

it('should work with run syntax', () => {
expect(
parseRunOneOptions(workspaceJson, [
parseRunOneOptions('root', workspaceJson, [
'run',
'myproj:build:production',
'--flag=true',
Expand All @@ -65,6 +65,7 @@ describe('parseRunOneOptions', () => {
it('should use defaultProjectName when no provided', () => {
expect(
parseRunOneOptions(
'root',
{ ...workspaceJson, cli: { defaultProjectName: 'myproj' } },
['build', '--flag=true']
)
Expand All @@ -76,20 +77,20 @@ describe('parseRunOneOptions', () => {
});

it('should return false when the task is not recognized', () => {
expect(parseRunOneOptions({}, args)).toBe(false);
expect(parseRunOneOptions({ projects: {} }, args)).toBe(false);
expect(parseRunOneOptions({ projects: { architect: {} } }, args)).toBe(
false
);
expect(parseRunOneOptions('root', {}, args)).toBe(false);
expect(parseRunOneOptions('root', { projects: {} }, args)).toBe(false);
expect(
parseRunOneOptions('root', { projects: { architect: {} } }, args)
).toBe(false);
});

it('should return false when cannot find the right project', () => {
expect(parseRunOneOptions(workspaceJson, ['build', 'wrongproj'])).toBe(
false
);
expect(
parseRunOneOptions('root', workspaceJson, ['build', 'wrongproj'])
).toBe(false);
});

it('should return false when no project specified', () => {
expect(parseRunOneOptions(workspaceJson, ['build'])).toBe(false);
expect(parseRunOneOptions('root', workspaceJson, ['build'])).toBe(false);
});
});
35 changes: 29 additions & 6 deletions packages/cli/lib/parse-run-one-options.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
import yargsParser = require('yargs-parser');

export function parseRunOneOptions(
workspaceConfigJson: any,
args: string[]
): false | { project; target; configuration; parsedArgs } {
function calculateDefaultProjectName(cwd: string, root: string, wc: any) {
let relativeCwd = cwd.split(root)[1];
if (relativeCwd) {
relativeCwd = relativeCwd.startsWith('/')
? relativeCwd.substring(1)
: relativeCwd;
const matchingProject = Object.keys(wc.projects).find((p) => {
const projectRoot = wc.projects[p].root;
return (
relativeCwd == projectRoot || relativeCwd.startsWith(`${projectRoot}/`)
);
});
if (matchingProject) return matchingProject;
}
let defaultProjectName = null;
try {
defaultProjectName = workspaceConfigJson.cli.defaultProjectName;
defaultProjectName = wc.cli.defaultProjectName;
} catch (e) {}
try {
if (!defaultProjectName) {
defaultProjectName = workspaceConfigJson.defaultProject;
defaultProjectName = wc.defaultProject;
}
} catch (e) {}
return defaultProjectName;
}

export function parseRunOneOptions(
root: string,
workspaceConfigJson: any,
args: string[]
): false | { project; target; configuration; parsedArgs } {
const defaultProjectName = calculateDefaultProjectName(
process.cwd(),
root,
workspaceConfigJson
);

const parsedArgs = yargsParser(args, {
boolean: ['prod', 'help'],
Expand Down
5 changes: 3 additions & 2 deletions packages/nx-plugin/src/utils/testing-utils/nx-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { appRootPath } from '@nrwl/workspace/src/utils/app-root';
import { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager';
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { dirname } from 'path';
import { ensureDirSync } from 'fs-extra';
import { tmpProjPath } from './paths';
import { cleanup } from './utils';

function runNxNewCommand(args?: string, silent?: boolean) {
const localTmpDir = `./tmp/nx-e2e`;
const localTmpDir = dirname(tmpProjPath());
return execSync(
`node ${require.resolve(
'@nrwl/tao'
)} new proj --no-interactive --skip-install --collection=@nrwl/workspace --npmScope=proj --preset=empty ${
)} new proj --nx-workspace-root=${localTmpDir} --no-interactive --skip-install --collection=@nrwl/workspace --npmScope=proj --preset=empty ${
args || ''
}`,
{
Expand Down
23 changes: 22 additions & 1 deletion packages/tao/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/usr/bin/env node
import { dirname, join } from 'path';

const argv = require('yargs-parser')(process.argv.slice(2));
import './src/compat/compat';
import { existsSync } from 'fs-extra';

export async function invokeCommand(
command: string,
Expand Down Expand Up @@ -37,6 +40,7 @@ export async function invokeCommand(
case 'run':
case 'r':
return (await import('./src/commands/run')).run(
process.cwd(),
root,
commandArgs,
isVerbose
Expand All @@ -57,6 +61,7 @@ export async function invokeCommand(
const projectName = projectNameIncluded ? commandArgs[0] : '';
// this is to make `tao test mylib` same as `tao run mylib:test`
return (await import('./src/commands/run')).run(
process.cwd(),
root,
[
`${projectName}:${command}`,
Expand All @@ -68,9 +73,25 @@ export async function invokeCommand(
}
}

function findWorkspaceRoot(dir: string): string {
if (dirname(dir) === dir) {
throw new Error(`The cwd isn't part of an Nx workspace`);
}
if (
existsSync(join(dir, 'angular.json')) ||
existsSync(join(dir, 'workspace.json'))
) {
return dir;
}
return findWorkspaceRoot(dirname(dir));
}

export async function invokeCli(root: string, args: string[]) {
const [command, ...commandArgs] = args;
process.exit(await invokeCommand(command, root, commandArgs));
}

invokeCli(argv.nxWorkspaceRoot || process.cwd(), process.argv.slice(2));
invokeCli(
argv.nxWorkspaceRoot || findWorkspaceRoot(process.cwd()),
process.argv.slice(2)
);
6 changes: 3 additions & 3 deletions packages/tao/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ function printChanges(fileChanges: FileChange[]) {
});
}

export async function taoNew(root: string, args: string[], isVerbose = false) {
const ws = new Workspaces(root);
export async function taoNew(cwd: string, args: string[], isVerbose = false) {
const ws = new Workspaces(cwd);
return handleErrors(isVerbose, async () => {
const opts = parseGenerateOpts(args, 'new', null);

Expand All @@ -177,7 +177,7 @@ export async function taoNew(root: string, args: string[], isVerbose = false) {
opts.interactive
);
return (await import('./ngcli-adapter')).invokeNew(
root,
cwd,
{
...opts,
generatorOptions: combinedOpts,
Expand Down
35 changes: 32 additions & 3 deletions packages/tao/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,32 @@ function throwInvalidInvocation() {
);
}

function calculateDefaultProjectName(
cwd: string,
root: string,
wc: WorkspaceConfiguration
) {
let relativeCwd = cwd.split(root)[1];
if (relativeCwd) {
relativeCwd = relativeCwd.startsWith('/')
? relativeCwd.substring(1)
: relativeCwd;
const matchingProject = Object.keys(wc.projects).find((p) => {
const projectRoot = wc.projects[p].root;
return (
relativeCwd == projectRoot || relativeCwd.startsWith(`${projectRoot}/`)
);
});
if (matchingProject) return matchingProject;
}
return wc.defaultProject;
}

function parseRunOpts(
cwd: string,
root: string,
args: string[],
defaultProjectName: string | null
wc: WorkspaceConfiguration
): RunOptions {
const runOptions = convertToCamelCase(
minimist(args, {
Expand All @@ -47,6 +70,7 @@ function parseRunOpts(
if (!runOptions._ || !runOptions._[0]) {
throwInvalidInvocation();
}
const defaultProjectName = calculateDefaultProjectName(cwd, root, wc);
// eslint-disable-next-line prefer-const
let [project, target, configuration]: [
string,
Expand Down Expand Up @@ -134,12 +158,17 @@ export interface TargetContext {
workspace: WorkspaceConfiguration;
}

export async function run(root: string, args: string[], isVerbose: boolean) {
export async function run(
cwd: string,
root: string,
args: string[],
isVerbose: boolean
) {
const ws = new Workspaces(root);

return handleErrors(isVerbose, async () => {
const workspace = ws.readWorkspaceConfiguration();
const opts = parseRunOpts(args, workspace.defaultProject);
const opts = parseRunOpts(cwd, root, args, workspace);
validateTargetAndConfiguration(workspace, opts);

const target = workspace.projects[opts.project].targets[opts.target];
Expand Down

0 comments on commit 86b5c5c

Please sign in to comment.