Skip to content

Commit

Permalink
fix(core): add warning if running on an outdated global install (#15443)
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Mar 18, 2023
1 parent 02cd5f9 commit 7740bb1
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 3 deletions.
16 changes: 16 additions & 0 deletions docs/generated/manifests/menus.json
Expand Up @@ -1153,6 +1153,14 @@
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{
"name": "Managing your Global Nx Installation",
"path": "/more-concepts/global-nx",
"id": "global-nx",
"isExternal": false,
"children": [],
"disableCollapsible": false
}
],
"disableCollapsible": false
Expand Down Expand Up @@ -1317,6 +1325,14 @@
"children": [],
"disableCollapsible": false
},
{
"name": "Managing your Global Nx Installation",
"path": "/more-concepts/global-nx",
"id": "global-nx",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{
"name": "All Recipes »",
"path": "/recipes",
Expand Down
20 changes: 20 additions & 0 deletions docs/generated/manifests/nx.json
Expand Up @@ -1434,6 +1434,16 @@
"isExternal": false,
"path": "/more-concepts/nx-and-the-wrapper",
"tags": []
},
{
"id": "global-nx",
"name": "Managing your Global Nx Installation",
"description": "",
"file": "shared/guides/global-nx",
"itemList": [],
"isExternal": false,
"path": "/more-concepts/global-nx",
"tags": []
}
],
"isExternal": false,
Expand Down Expand Up @@ -1640,6 +1650,16 @@
"path": "/more-concepts/nx-and-the-wrapper",
"tags": []
},
"/more-concepts/global-nx": {
"id": "global-nx",
"name": "Managing your Global Nx Installation",
"description": "",
"file": "shared/guides/global-nx",
"itemList": [],
"isExternal": false,
"path": "/more-concepts/global-nx",
"tags": []
},
"/recipes": {
"id": "all",
"name": "All Recipes »",
Expand Down
5 changes: 5 additions & 0 deletions docs/map.json
Expand Up @@ -489,6 +489,11 @@
"name": "Nx and the Nx Wrapper",
"id": "nx-and-the-wrapper",
"file": "shared/guides/nx-and-the-wrapper"
},
{
"name": "Managing your Global Nx Installation",
"id": "global-nx",
"file": "shared/guides/global-nx"
}
]
},
Expand Down
102 changes: 102 additions & 0 deletions docs/shared/guides/global-nx.md
@@ -0,0 +1,102 @@
# Managing your Global Nx Installation

Nx can be ran in a total of 3 ways:

- Through your package manager (e.g. `npx nx`, `yarn nx`, or `pnpm exec nx`)
- Through [./nx or ./nx.bat](/more-concepts/nx-and-the-wrapper)
- Through a global Nx installation (e.g. `nx`)

With a global Nx installation, Nx looks for the local copy of Nx in your repo and hands off the process execution to it. This means that whichever version of Nx is installed locally in your repo is still the version of Nx that runs your code. For the most part, this can eliminate any issues that may arise from the global install being outdated.

## Installing Nx Globally

Depending on your package manager of choice, you can install Nx with the following commands:

{% tabs %}
{% tab label="npm" %}

```shell
npm install --global nx@latest
```

{% /tab %}
{% tab label="yarn" %}

```shell
yarn global add nx@latest
```

{% /tab %}
{% tab label="pnpm" %}

```shell
pnpm install --global nx@latest
```

{% /tab %}
{% /tabs %}

## Updating your global Nx installation

There are some cases where an issue could arise when using an outdated global installation of Nx. If the structure of your Nx workspace no longer matches up with what the globally installed copy of Nx expects, it may fail to hand off to your local installation properly and instead error. This commonly results in errors such as:

- Could not find Nx modules in this workspace.
- The current directory isn't part of an Nx workspace.

If you find yourself in this position, you will need to update your global install of Nx.

In most cases, you can update a globally installed npm package by rerunning the command you used to install it, as described [above](#installing-nx-globally)

If you cannot remember which package manager you installed Nx globally with or are still encountering issues, you can locate other installs of Nx with these commands:

{% tabs %}
{% tab label="npm" %}

```shell
npm list --global nx
```

{% /tab %}
{% tab label="yarn" %}

```shell
yarn global list nx
```

{% /tab %}
{% tab label="pnpm" %}

```shell
pnpm list --global nx
```

{% /tab %}
{% /tabs %}

You can then remove the extra global installations by running the following commands for the duplicate installations:

{% tabs %}
{% tab label="npm" %}

```shell
npm rm --global nx
```

{% /tab %}
{% tab label="yarn" %}

```shell
yarn global remove nx
```

{% /tab %}
{% tab label="pnpm" %}

```shell
pnpm rm --global nx
```

{% /tab %}
{% /tabs %}

Finally, to complete your global installation update, simply reinstall it as described [above](#installing-nx-globally).
68 changes: 67 additions & 1 deletion e2e/nx-misc/src/misc.test.ts
@@ -1,6 +1,8 @@
import type { NxJsonConfiguration } from '@nrwl/devkit';
import {
cleanupProject,
e2eCwd,
getPackageManagerCommand,
getPublishedVersion,
isNotWindows,
newProject,
Expand All @@ -15,8 +17,10 @@ import {
updateFile,
updateJson,
} from '@nrwl/e2e/utils';
import { renameSync } from 'fs';
import { renameSync, writeFileSync } from 'fs';
import { ensureDirSync } from 'fs-extra';
import * as path from 'path';
import { major } from 'semver';

describe('Nx Commands', () => {
let proj: string;
Expand Down Expand Up @@ -568,3 +572,65 @@ describe('migrate', () => {
expect(output).toContain(`Migrations file 'migrations.json' doesn't exist`);
});
});

describe('global installation', () => {
// Additionally, installing Nx under e2eCwd like this still acts like a global install,
// but is easier to cleanup and doesn't mess with the users PC if running tests locally.
const globalsPath = path.join(e2eCwd, 'globals', 'node_modules', '.bin');

let oldPath: string;

beforeAll(() => {
newProject();

ensureDirSync(globalsPath);
writeFileSync(
path.join(path.dirname(path.dirname(globalsPath)), 'package.json'),
JSON.stringify(
{
dependencies: {
nx: getPublishedVersion(),
},
},
null,
2
)
);

runCommand(getPackageManagerCommand().install, {
cwd: path.join(e2eCwd, 'globals'),
});

// Update process.path to have access to modules installed in e2ecwd/node_modules/.bin,
// this lets commands run things like `nx`. We put it at the beginning so they are found first.
oldPath = process.env.PATH;
process.env.PATH = globalsPath + path.delimiter + process.env.PATH;
});

afterAll(() => {
process.env.PATH = oldPath;
});

it('should invoke Nx commands from local repo', () => {
const nxJsContents = readFile('node_modules/nx/bin/nx.js');
updateFile('node_modules/nx/bin/nx.js', `console.log('local install');`);
let output: string;
expect(() => {
output = runCommand(`nx show projects`);
}).not.toThrow();
expect(output).toContain('local install');
updateFile('node_modules/nx/bin/nx.js', nxJsContents);
});

it('should warn if local Nx has higher major version', () => {
updateJson('node_modules/nx/package.json', (json) => {
json.version = `${major(getPublishedVersion()) + 2}.0.0`;
return json;
});
let output: string;
expect(() => {
output = runCommand(`nx show projects`);
}).not.toThrow();
expect(output).toContain('Its time to update Nx');
});
});
3 changes: 3 additions & 0 deletions nx.json
Expand Up @@ -120,6 +120,9 @@
},
{
"env": "NX_E2E_CI_CACHE_KEY"
},
{
"env": "CI"
}
],
"options": {
Expand Down
69 changes: 67 additions & 2 deletions packages/nx/bin/nx.ts
Expand Up @@ -6,7 +6,13 @@ import {
import * as chalk from 'chalk';
import { initLocal } from './init-local';
import { output } from '../src/utils/output';
import { getNxInstallationPath } from 'nx/src/utils/installation-directory';
import {
getNxInstallationPath,
getNxRequirePaths,
} from '../src/utils/installation-directory';
import { major } from 'semver';
import { readJsonFile } from '../src/utils/fileutils';
import { execSync } from 'child_process';

const workspace = findWorkspaceRoot(process.cwd());
// new is a special case because there is no local workspace to load
Expand All @@ -22,7 +28,7 @@ if (
if (workspace && workspace.type === 'nx') {
require('v8-compile-cache');
}
// polyfill rxjs observable to avoid issues with multiple version fo Observable installed in node_modules
// polyfill rxjs observable to avoid issues with multiple version of Observable installed in node_modules
// https://twitter.com/BenLesh/status/1192478226385428483?s=20
if (!(Symbol as any).observable)
(Symbol as any).observable = Symbol('observable polyfill');
Expand Down Expand Up @@ -53,6 +59,8 @@ if (
try {
localNx = resolveNx(workspace);
} catch {
// If we can't resolve a local copy of Nx, we must be global.
warnIfUsingOutdatedGlobalInstall();
output.error({
title: `Could not find Nx modules in this workspace.`,
bodyLines: [`Have you run ${chalk.bold.white(`npm/yarn install`)}?`],
Expand All @@ -65,6 +73,7 @@ if (
initLocal(workspace);
} else {
// Nx is being run from globally installed CLI - hand off to the local
warnIfUsingOutdatedGlobalInstall(getLocalNxVersion(workspace));
if (localNx.includes('.nx')) {
const nxWrapperPath = localNx.replace(/\.nx.*/, '.nx/') + 'nxw.js';
require(nxWrapperPath);
Expand Down Expand Up @@ -94,3 +103,59 @@ function resolveNx(workspace: WorkspaceTypeAndRoot | null) {
});
}
}

/**
* Assumes currently running Nx is global install.
* Warns if out of date by 1 major version or more.
*/
function warnIfUsingOutdatedGlobalInstall(localNxVersion?: string) {
const globalVersion = require('../package.json').version;
const isOutdatedGlobalInstall =
globalVersion &&
((localNxVersion && major(globalVersion) < major(localNxVersion)) ||
(!localNxVersion &&
getLatestVersionOfNx() &&
major(globalVersion) < major(getLatestVersionOfNx())));

// Using a global Nx Install
if (isOutdatedGlobalInstall) {
const bodyLines = localNxVersion
? [
`Your repository uses a higher version of Nx (${localNxVersion}) than your global CLI version (${globalVersion})`,
]
: [];

bodyLines.push(
'For more information, see https://nx.dev/more-concepts/global-nx'
);
output.warn({
title: `Its time to update Nx 🎉`,
bodyLines,
});
}
}

function getLocalNxVersion(workspace: WorkspaceTypeAndRoot): string {
return readJsonFile(
require.resolve('nx/package.json', {
paths: getNxRequirePaths(workspace.dir),
})
).version;
}

function _getLatestVersionOfNx(): string {
try {
return execSync('npm view nx@latest version').toString().trim();
} catch {
try {
return execSync('pnpm view nx@latest version').toString().trim();
} catch {
return null;
}
}
}

const getLatestVersionOfNx = ((fn: () => string) => {
let cache: string = null;
return () => cache || (cache = fn());
})(_getLatestVersionOfNx);

1 comment on commit 7740bb1

@vercel
Copy link

@vercel vercel bot commented on 7740bb1 Mar 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-five.vercel.app
nx.dev
nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app

Please sign in to comment.