feat(core): support encapsulated nx install (#14705)
This commit is contained in:
parent
24e87e6cc4
commit
46b6a710c0
@ -1129,6 +1129,14 @@
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"name": "Encapsulated Nx and the Nx Wrapper",
|
||||
"path": "/more-concepts/encapsulated-nx-and-the-wrapper",
|
||||
"id": "encapsulated-nx-and-the-wrapper",
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"disableCollapsible": false
|
||||
@ -1285,6 +1293,14 @@
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"name": "Encapsulated Nx and the Nx Wrapper",
|
||||
"path": "/more-concepts/encapsulated-nx-and-the-wrapper",
|
||||
"id": "encapsulated-nx-and-the-wrapper",
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"name": "All Recipes »",
|
||||
"path": "/recipes",
|
||||
|
||||
@ -1404,6 +1404,16 @@
|
||||
"isExternal": false,
|
||||
"path": "/more-concepts/how-project-graph-is-built",
|
||||
"tags": ["explore-graph"]
|
||||
},
|
||||
{
|
||||
"id": "encapsulated-nx-and-the-wrapper",
|
||||
"name": "Encapsulated Nx and the Nx Wrapper",
|
||||
"description": "",
|
||||
"file": "shared/guides/encapsulated-nx-and-the-wrapper",
|
||||
"itemList": [],
|
||||
"isExternal": false,
|
||||
"path": "/more-concepts/encapsulated-nx-and-the-wrapper",
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
@ -1600,6 +1610,16 @@
|
||||
"path": "/more-concepts/how-project-graph-is-built",
|
||||
"tags": ["explore-graph"]
|
||||
},
|
||||
"/more-concepts/encapsulated-nx-and-the-wrapper": {
|
||||
"id": "encapsulated-nx-and-the-wrapper",
|
||||
"name": "Encapsulated Nx and the Nx Wrapper",
|
||||
"description": "",
|
||||
"file": "shared/guides/encapsulated-nx-and-the-wrapper",
|
||||
"itemList": [],
|
||||
"isExternal": false,
|
||||
"path": "/more-concepts/encapsulated-nx-and-the-wrapper",
|
||||
"tags": []
|
||||
},
|
||||
"/recipes": {
|
||||
"id": "all",
|
||||
"name": "All Recipes »",
|
||||
|
||||
@ -479,6 +479,11 @@
|
||||
"id": "how-project-graph-is-built",
|
||||
"tags": ["explore-graph"],
|
||||
"file": "shared/concepts/how-project-graph-is-built"
|
||||
},
|
||||
{
|
||||
"name": "Encapsulated Nx and the Nx Wrapper",
|
||||
"id": "encapsulated-nx-and-the-wrapper",
|
||||
"file": "shared/guides/encapsulated-nx-and-the-wrapper"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -14,3 +14,4 @@
|
||||
- [Grouping Libraries](/more-concepts/grouping-libraries)
|
||||
- [Buildable and Publishable Libraries](/more-concepts/buildable-and-publishable-libraries)
|
||||
- [How the Project Graph is Built](/more-concepts/how-project-graph-is-built)
|
||||
- [Encapsulated Nx and the Nx Wrapper](/more-concepts/encapsulated-nx-and-the-wrapper)
|
||||
|
||||
53
docs/shared/guides/encapsulated-nx-and-the-wrapper.md
Normal file
53
docs/shared/guides/encapsulated-nx-and-the-wrapper.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Encapsulated Nx
|
||||
|
||||
{% callout type="note" title="Available since Nx v15.8.0" %}
|
||||
|
||||
Support for `--encapsulated` was added in Nx v15.8.0. To ensure that it is available, specify the version of nx when running your command so that `npx` doesn't accept an older version that is in the cache. (e.g. `npx nx@latest init --encapsulated` or `npx nx@15.8.0 init --encapsulated`)
|
||||
|
||||
{% /callout %}
|
||||
|
||||
`nx init` accepts a flag called `--encapsulated`. When this flag is passed, Nx manages its own installation within a `.nx` directory. This allows Nx to be used without a `node_modules` folder or root `package.json`.
|
||||
|
||||
To manage the local Nx installation, and invoke Nx, a total of 4 files are created.
|
||||
|
||||
- [.nx/nxw.js](#nx-wrapper)
|
||||
- [nx](#nx-and-nxbat)
|
||||
- [nx.bat](#nx-and-nxbat)
|
||||
- [nx.json](#nx-json)
|
||||
|
||||
## Nx Wrapper
|
||||
|
||||
After running `nx init --encapsulated`, a file is created called `.nx/nxw.js`. This javascript file is responsible for ensuring that your local installation of Nx is up to date with what the repository expects, and then invoking Nx. We refer to this as the "Nx Wrapper".
|
||||
|
||||
## Nx and nx bat
|
||||
|
||||
`nx` and `nx.bat` perform the same function, with `nx` working under the bash shell and `nx.bat` working for windows users. Each script simply checks that `node` and `npm` are available, and then invokes the [nx wrapper](#nx-wrapper). They should each be committed to your repository, so that developers on any operating system are able to use Nx.
|
||||
|
||||
To invoke an Nx command, you would use:
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="MacOS / Linux" %}
|
||||
|
||||
```shell
|
||||
> ./nx build my-project
|
||||
> ./nx generate application
|
||||
> ./nx graph
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Windows" %}
|
||||
|
||||
```shell
|
||||
> ./nx.bat build my-project
|
||||
> ./nx.bat generate application
|
||||
> ./nx.bat graph
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Nx Json
|
||||
|
||||
This is the configuration file for Nx. A full description of `nx.json`, and the possible configuration options, is located in [the nx.json reference documentation](/reference/nx-json).
|
||||
|
||||
When Nx is encapsulated, an `installation` key is added to `nx.json`. This property tracks the currently installed version of Nx and plugins.
|
||||
@ -1,13 +1,19 @@
|
||||
import { NxJsonConfiguration } from '@nrwl/devkit';
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
createNonNxProjectDirectory,
|
||||
getPackageManagerCommand,
|
||||
getPublishedVersion,
|
||||
getSelectedPackageManager,
|
||||
newEncapsulatedNxWorkspace,
|
||||
removeFile,
|
||||
renameFile,
|
||||
runCLI,
|
||||
runCommand,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nrwl/e2e/utils';
|
||||
|
||||
describe('nx init', () => {
|
||||
@ -15,8 +21,6 @@ describe('nx init', () => {
|
||||
packageManager: getSelectedPackageManager(),
|
||||
});
|
||||
|
||||
afterEach(() => cleanupProject());
|
||||
|
||||
it('should work in a monorepo', () => {
|
||||
createNonNxProjectDirectory('monorepo', true);
|
||||
updateFile(
|
||||
@ -40,6 +44,7 @@ describe('nx init', () => {
|
||||
renameFile('nx.json', 'nx.json.old');
|
||||
|
||||
expect(runCLI('run package:echo')).toContain('123');
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should work in a regular npm repo', () => {
|
||||
@ -68,6 +73,7 @@ describe('nx init', () => {
|
||||
renameFile('nx.json', 'nx.json.old');
|
||||
|
||||
expect(runCLI('echo')).toContain('123');
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should support compound scripts', () => {
|
||||
@ -94,5 +100,62 @@ describe('nx init', () => {
|
||||
expect(output).toContain('HELLO\n');
|
||||
expect(output).toContain('COMPOUND TEST');
|
||||
expect(output).not.toContain('HELLO COMPOUND');
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
describe('encapsulated', () => {
|
||||
it('should support running targets in a encapsulated repo', () => {
|
||||
const runEncapsulatedNx = newEncapsulatedNxWorkspace();
|
||||
updateFile(
|
||||
'projects/a/project.json',
|
||||
JSON.stringify({
|
||||
name: 'a',
|
||||
targets: {
|
||||
echo: {
|
||||
command: `echo 'Hello from A'`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
updateJson<NxJsonConfiguration>('nx.json', (json) => ({
|
||||
...json,
|
||||
tasksRunnerOptions: {
|
||||
default: {
|
||||
...json.tasksRunnerOptions['default'],
|
||||
options: {
|
||||
...json.tasksRunnerOptions['default'].options,
|
||||
cacheableOperations: ['echo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(runEncapsulatedNx('echo a')).toContain('Hello from A');
|
||||
|
||||
expect(runEncapsulatedNx('echo a')).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
checkFilesDoNotExist(
|
||||
'node_modules',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'yarn-lock.json',
|
||||
'pnpm-lock.yaml'
|
||||
)
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
checkFilesExist(
|
||||
'.nx/installation/package.json',
|
||||
'.nx/installation/package-lock.json',
|
||||
'.nx/cache/terminalOutputs'
|
||||
)
|
||||
).not.toThrow();
|
||||
});
|
||||
cleanupProject({
|
||||
skipReset: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -468,6 +468,22 @@ export function newLernaWorkspace({
|
||||
}
|
||||
}
|
||||
|
||||
export function newEncapsulatedNxWorkspace({
|
||||
name = uniq('encapsulated'),
|
||||
pmc = getPackageManagerCommand(),
|
||||
} = {}): (command: string) => string {
|
||||
projName = name;
|
||||
ensureDirSync(tmpProjPath());
|
||||
runCommand(`${pmc.runUninstalledPackage} nx@latest init --encapsulated`);
|
||||
return (command: string) => {
|
||||
if (process.platform === 'win32') {
|
||||
return runCommand(`./nx.bat ${command}`);
|
||||
} else {
|
||||
return runCommand(`./nx ${command}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const KILL_PORT_DELAY = 5000;
|
||||
|
||||
export async function killPort(port: number): Promise<boolean> {
|
||||
|
||||
@ -7,6 +7,7 @@ import * as chalk from 'chalk';
|
||||
import { initLocal } from './init-local';
|
||||
import { detectPackageManager } from '../src/utils/package-manager';
|
||||
import { output } from '../src/utils/output';
|
||||
import { join } from 'path';
|
||||
|
||||
const workspace = findWorkspaceRoot(process.cwd());
|
||||
// new is a special case because there is no local workspace to load
|
||||
@ -70,11 +71,22 @@ if (
|
||||
}
|
||||
|
||||
function resolveNx(workspace: WorkspaceTypeAndRoot | null) {
|
||||
// prefer Nx installed in .nx/installation
|
||||
try {
|
||||
return require.resolve('nx/bin/nx.js', {
|
||||
paths: workspace
|
||||
? [join(workspace.dir, '.nx', 'installation')]
|
||||
: undefined,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
// check for root install
|
||||
try {
|
||||
return require.resolve('nx/bin/nx.js', {
|
||||
paths: workspace ? [workspace.dir] : undefined,
|
||||
});
|
||||
} catch {
|
||||
// fallback for old CLI install setup
|
||||
return require.resolve('@nrwl/cli/bin/nx.js', {
|
||||
paths: workspace ? [workspace.dir] : undefined,
|
||||
});
|
||||
|
||||
@ -4,14 +4,39 @@ import { addNxToNest } from '../nx-init/add-nx-to-nest';
|
||||
import { addNxToNpmRepo } from '../nx-init/add-nx-to-npm-repo';
|
||||
import { directoryExists, readJsonFile } from '../utils/fileutils';
|
||||
import { PackageJson } from '../utils/package-json';
|
||||
import * as parser from 'yargs-parser';
|
||||
import { generateEncapsulatedNxSetup } from '../nx-init/encapsulated/add-nx-scripts';
|
||||
|
||||
export async function initHandler() {
|
||||
const args = process.argv.slice(2).join(' ');
|
||||
const flags = parser(args, {
|
||||
boolean: ['encapsulated'],
|
||||
default: {
|
||||
encapsulated: false,
|
||||
},
|
||||
}) as any as { encapsulated: boolean };
|
||||
|
||||
const version = process.env.NX_VERSION ?? 'latest';
|
||||
if (process.env.NX_VERSION) {
|
||||
console.log(`Using version ${process.env.NX_VERSION}`);
|
||||
}
|
||||
if (existsSync('package.json')) {
|
||||
if (flags.encapsulated === true) {
|
||||
if (process.platform !== 'win32') {
|
||||
console.log(
|
||||
'Setting Nx up installation in `.nx`. You can run nx commands like: `./nx --help`'
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Setting Nx up installation in `.nx`. You can run nx commands like: `./nx.bat --help`'
|
||||
);
|
||||
}
|
||||
generateEncapsulatedNxSetup(version);
|
||||
if (process.platform === 'win32') {
|
||||
execSync('./nx.bat', { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync('./nx', { stdio: 'inherit' });
|
||||
}
|
||||
} else if (existsSync('package.json')) {
|
||||
const packageJson: PackageJson = readJsonFile('package.json');
|
||||
if (existsSync('angular.json')) {
|
||||
// TODO(leo): remove make-angular-cli-faster
|
||||
|
||||
@ -32,6 +32,18 @@ export interface NrwlJsPluginConfig {
|
||||
analyzePackageJson?: boolean;
|
||||
}
|
||||
|
||||
interface NxInstallationConfiguration {
|
||||
/**
|
||||
* Version used for Nx
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Record<pluginPackageName, pluginVersion>. e.g.
|
||||
* plugins: { '@nrwl/angular': '1.0.0' }
|
||||
*/
|
||||
plugins?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nx.json configuration
|
||||
*
|
||||
@ -135,4 +147,11 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
|
||||
* will be used. Convenient for small workspaces with one main application.
|
||||
*/
|
||||
defaultProject?: string;
|
||||
|
||||
/**
|
||||
* Configures the Nx installation for a repo. Useful for maintaining a separate
|
||||
* set of dependencies for Nx + Plugins compared to the base package.json, but also
|
||||
* useful for workspaces that don't have a root package.json + node_modules.
|
||||
*/
|
||||
installation?: NxInstallationConfiguration;
|
||||
}
|
||||
|
||||
@ -598,7 +598,11 @@ export function globForProjectFiles(
|
||||
|
||||
if (!ignorePluginInference) {
|
||||
projectGlobPatterns.push(
|
||||
...getGlobPatternsFromPlugins(nxJson, [root], root)
|
||||
...getGlobPatternsFromPlugins(
|
||||
nxJson,
|
||||
[root, join(root, '.nx', 'installation')],
|
||||
root
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ export function updateWorkspaceConfiguration(
|
||||
tasksRunnerOptions,
|
||||
affected,
|
||||
extends: ext,
|
||||
installation,
|
||||
} = workspaceConfig;
|
||||
|
||||
const nxJson: Required<NxJsonConfiguration> = {
|
||||
@ -56,6 +57,7 @@ export function updateWorkspaceConfiguration(
|
||||
generators,
|
||||
defaultProject,
|
||||
extends: ext,
|
||||
installation,
|
||||
};
|
||||
|
||||
updateNxJson(tree, nxJson);
|
||||
|
||||
89
packages/nx/src/nx-init/encapsulated/add-nx-scripts.ts
Normal file
89
packages/nx/src/nx-init/encapsulated/add-nx-scripts.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, constants as FsConstants } from 'fs';
|
||||
import { stripIndent } from 'nx/src/utils/logger';
|
||||
import * as path from 'path';
|
||||
import { valid } from 'semver';
|
||||
import { NxJsonConfiguration } from '../../config/nx-json';
|
||||
import {
|
||||
flushChanges,
|
||||
FsTree,
|
||||
printChanges,
|
||||
Tree,
|
||||
} from '../../generators/tree';
|
||||
import { writeJson } from '../../generators/utils/json';
|
||||
|
||||
const nxWrapperPath = (p: typeof import('path') = path) =>
|
||||
p.join('.nx', 'nxw.js');
|
||||
|
||||
const NODE_MISSING_ERR =
|
||||
'Nx requires NodeJS to be available. To install NodeJS and NPM, see: https://nodejs.org/en/download/ .';
|
||||
const NPM_MISSING_ERR =
|
||||
'Nx requires npm to be available. To install NodeJS and NPM, see: https://nodejs.org/en/download/ .';
|
||||
|
||||
const BATCH_SCRIPT_CONTENTS = stripIndent(
|
||||
`set path_to_root=%~dp0
|
||||
WHERE node >nul 2>nul
|
||||
IF %ERRORLEVEL% NEQ 0 (ECHO ${NODE_MISSING_ERR}; EXIT 1)
|
||||
WHERE npm >nul 2>nul
|
||||
IF %ERRORLEVEL% NEQ 0 (ECHO ${NPM_MISSING_ERR}; EXIT 1)
|
||||
node ${path.win32.join('%path_to_root%', nxWrapperPath(path.win32))} %*`
|
||||
);
|
||||
|
||||
const SHELL_SCRIPT_CONTENTS = stripIndent(
|
||||
`#!/bin/bash
|
||||
command -v node >/dev/null 2>&1 || { echo >&2 "${NODE_MISSING_ERR}"; exit 1; }
|
||||
command -v npm >/dev/null 2>&1 || { echo >&2 "${NPM_MISSING_ERR}"; exit 1; }
|
||||
path_to_root=$(dirname $BASH_SOURCE)
|
||||
node ${path.posix.join('$path_to_root', nxWrapperPath(path.posix))} $@`
|
||||
);
|
||||
|
||||
export function generateEncapsulatedNxSetup(version?: string) {
|
||||
const host = new FsTree(process.cwd(), false);
|
||||
writeMinimalNxJson(host, version);
|
||||
updateGitIgnore(host);
|
||||
host.write(nxWrapperPath(), getNodeScriptContents());
|
||||
host.write('nx.bat', BATCH_SCRIPT_CONTENTS);
|
||||
host.write('nx', SHELL_SCRIPT_CONTENTS, {
|
||||
mode: FsConstants.S_IXUSR | FsConstants.S_IRUSR | FsConstants.S_IWUSR,
|
||||
});
|
||||
const changes = host.listChanges();
|
||||
printChanges(changes);
|
||||
flushChanges(host.root, changes);
|
||||
}
|
||||
|
||||
export function writeMinimalNxJson(host: Tree, version: string) {
|
||||
if (!host.exists('nx.json')) {
|
||||
if (!valid(version)) {
|
||||
version = execSync(`npm view nx@${version} version`).toString();
|
||||
}
|
||||
writeJson<NxJsonConfiguration>(host, 'nx.json', {
|
||||
tasksRunnerOptions: {
|
||||
default: {
|
||||
runner: 'nx/tasks-runners/default',
|
||||
options: {
|
||||
cacheableOperations: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
installation: {
|
||||
version: version.trimEnd(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function updateGitIgnore(host: Tree) {
|
||||
const contents = host.read('.gitignore', 'utf-8') ?? '';
|
||||
host.write(
|
||||
'.gitignore',
|
||||
[contents, '.nx/installation', '.nx/cache'].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeScriptContents() {
|
||||
// Read nxw.js, but remove any empty comments or comments that start with `INTERNAL: `
|
||||
return readFileSync(path.join(__dirname, 'nxw.js'), 'utf-8').replace(
|
||||
/(\/\/ INTERNAL: .*)|(\/\/\w*)$/gm,
|
||||
''
|
||||
);
|
||||
}
|
||||
97
packages/nx/src/nx-init/encapsulated/nxw.ts
Normal file
97
packages/nx/src/nx-init/encapsulated/nxw.ts
Normal file
@ -0,0 +1,97 @@
|
||||
// This file should be committed to your repository! It wraps Nx and ensures
|
||||
// that your local installation matches nx.json.
|
||||
// See: https://nx.dev/more-concepts/encapsulated-nx-and-the-wrapper for more info.
|
||||
//
|
||||
// INTERNAL: The contents of this file are executed before packages are installed.
|
||||
// INTERNAL: As such, we should not import anything from nx, other @nrwl packages,
|
||||
// INTERNAL: or any other npm packages. Only import node builtins.
|
||||
|
||||
const fs: typeof import('fs') = require('fs');
|
||||
const path: typeof import('path') = require('path');
|
||||
const cp: typeof import('child_process') = require('child_process');
|
||||
|
||||
import type { NxJsonConfiguration } from '../../config/nx-json';
|
||||
import type { PackageJson } from '../../utils/package-json';
|
||||
|
||||
const installationPath = path.join(__dirname, 'installation', 'package.json');
|
||||
|
||||
function matchesCurrentNxInstall(
|
||||
nxJsonInstallation: NxJsonConfiguration['installation']
|
||||
) {
|
||||
try {
|
||||
const currentInstallation: PackageJson = JSON.parse(
|
||||
fs.readFileSync(installationPath, 'utf-8')
|
||||
);
|
||||
if (
|
||||
currentInstallation.dependencies['nx'] !== nxJsonInstallation.version ||
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(installationPath, 'node_modules', 'nx', 'package.json'),
|
||||
'utf-8'
|
||||
)
|
||||
).version !== nxJsonInstallation.version
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
for (const [plugin, desiredVersion] of Object.entries(
|
||||
nxJsonInstallation.plugins || {}
|
||||
)) {
|
||||
if (currentInstallation.dependencies[plugin] !== desiredVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDir(p: string) {
|
||||
if (!fs.existsSync(p)) {
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUpToDateInstallation() {
|
||||
const nxJsonPath = path.join(__dirname, '..', 'nx.json');
|
||||
|
||||
let nxJson: NxJsonConfiguration;
|
||||
|
||||
try {
|
||||
nxJson = JSON.parse(fs.readFileSync(nxJsonPath, 'utf-8'));
|
||||
} catch {
|
||||
console.error(
|
||||
'[NX]: nx.json is required when running in encapsulated mode. Run `npx nx init --encapsulated` to restore it.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
ensureDir(path.join(__dirname, 'installation'));
|
||||
if (!matchesCurrentNxInstall(nxJson.installation)) {
|
||||
fs.writeFileSync(
|
||||
installationPath,
|
||||
JSON.stringify({
|
||||
name: 'nx-installation',
|
||||
devDependencies: {
|
||||
nx: nxJson.installation.version,
|
||||
...nxJson.installation.plugins,
|
||||
},
|
||||
})
|
||||
);
|
||||
cp.execSync('npm i --legacy-peer-deps', {
|
||||
cwd: path.dirname(installationPath),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[NX]: Nx wrapper failed to synchronize installation.');
|
||||
console.error(e.stack());
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
ensureUpToDateInstallation();
|
||||
require('./installation/node_modules/nx/bin/nx');
|
||||
}
|
||||
@ -1,10 +1,16 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder';
|
||||
import { readJsonFile } from '../../../../utils/fileutils';
|
||||
import { join } from 'path';
|
||||
import { PackageJson } from '../../../../utils/package-json';
|
||||
import { workspaceRoot } from '../../../../utils/workspace-root';
|
||||
|
||||
export function buildNpmPackageNodes(builder: ProjectGraphBuilder) {
|
||||
const packageJson = readJsonFile(join(workspaceRoot, 'package.json'));
|
||||
const packageJsonPath = join(workspaceRoot, 'package.json');
|
||||
const packageJson: Partial<PackageJson> = existsSync(packageJsonPath)
|
||||
? readJsonFile(packageJsonPath)
|
||||
: {};
|
||||
const deps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
|
||||
@ -11,13 +11,14 @@ import {
|
||||
getGlobPatternsFromPlugins,
|
||||
} from '../../../config/workspaces';
|
||||
import { workspaceRoot } from '../../../utils/workspace-root';
|
||||
import { join } from 'path';
|
||||
|
||||
export const getTouchedProjectsFromProjectGlobChanges: TouchedProjectLocator<
|
||||
WholeFileChange | JsonChange | DeletedFileChange
|
||||
> = (touchedFiles, projectGraphNodes, nxJson): string[] => {
|
||||
const pluginGlobPatterns = getGlobPatternsFromPlugins(
|
||||
nxJson,
|
||||
[workspaceRoot],
|
||||
[workspaceRoot, join(workspaceRoot, '.nx', 'installation')],
|
||||
workspaceRoot
|
||||
);
|
||||
const workspacesGlobPatterns =
|
||||
|
||||
@ -39,6 +39,8 @@ import {
|
||||
parseLockFile,
|
||||
} from '../lock-file/lock-file';
|
||||
import { Workspaces } from '../config/workspaces';
|
||||
import { existsSync } from 'fs';
|
||||
import { PackageJson } from '../utils/package-json';
|
||||
|
||||
export async function buildProjectGraph() {
|
||||
const projectConfigurations = new Workspaces(
|
||||
@ -140,8 +142,27 @@ export async function buildProjectGraphUsingProjectFileMap(
|
||||
}
|
||||
|
||||
function readCombinedDeps() {
|
||||
const json = readJsonFile(join(workspaceRoot, 'package.json'));
|
||||
return { ...json.dependencies, ...json.devDependencies };
|
||||
const installationPackageJsonPath = join(
|
||||
workspaceRoot,
|
||||
'.nx',
|
||||
'installation',
|
||||
'package.json'
|
||||
);
|
||||
const installationPackageJson: Partial<PackageJson> = existsSync(
|
||||
installationPackageJsonPath
|
||||
)
|
||||
? readJsonFile(installationPackageJsonPath)
|
||||
: {};
|
||||
const rootPackageJsonPath = join(workspaceRoot, 'package.json');
|
||||
const rootPackageJson: Partial<PackageJson> = existsSync(rootPackageJsonPath)
|
||||
? readJsonFile(rootPackageJsonPath)
|
||||
: {};
|
||||
return {
|
||||
...rootPackageJson.dependencies,
|
||||
...rootPackageJson.devDependencies,
|
||||
...installationPackageJson.dependencies,
|
||||
...installationPackageJson.devDependencies,
|
||||
};
|
||||
}
|
||||
|
||||
// extract only external nodes and their dependencies
|
||||
|
||||
@ -157,7 +157,11 @@ export function defaultFileRead(filePath: string): string | null {
|
||||
}
|
||||
|
||||
export function readPackageJson(): any {
|
||||
return readJsonFile(`${workspaceRoot}/package.json`);
|
||||
try {
|
||||
return readJsonFile(`${workspaceRoot}/package.json`);
|
||||
} catch {
|
||||
return {}; // if package.json doesn't exist
|
||||
}
|
||||
}
|
||||
// Original Exports
|
||||
export { FileData };
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { isAbsolute, join } from 'path';
|
||||
import { NxJsonConfiguration } from '../config/nx-json';
|
||||
import { readJsonFile } from './fileutils';
|
||||
@ -28,10 +29,16 @@ function cacheDirectory(root: string, cacheDirectory: string) {
|
||||
if (cacheDirectory) {
|
||||
return absolutePath(root, cacheDirectory);
|
||||
} else {
|
||||
return join(root, 'node_modules', '.cache', 'nx');
|
||||
return defaultCacheDirectory(root);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultCacheDirectory(root: string) {
|
||||
return existsSync(join(root, '.nx'))
|
||||
? join(root, '.nx', 'cache')
|
||||
: join(root, 'node_modules', '.cache', 'nx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the directory where Nx stores its cache and daemon-related files.
|
||||
*/
|
||||
@ -43,5 +50,5 @@ export const cacheDir = cacheDirectory(
|
||||
export const projectGraphCacheDirectory = absolutePath(
|
||||
workspaceRoot,
|
||||
process.env.NX_PROJECT_GRAPH_CACHE_DIRECTORY ??
|
||||
join(workspaceRoot, 'node_modules', '.cache', 'nx')
|
||||
defaultCacheDirectory(workspaceRoot)
|
||||
);
|
||||
|
||||
@ -85,7 +85,7 @@ function loadNxPlugin(moduleName: string, paths: string[], root: string) {
|
||||
|
||||
export function loadNxPlugins(
|
||||
plugins?: string[],
|
||||
paths = [workspaceRoot],
|
||||
paths = [workspaceRoot, join(workspaceRoot, '.nx', 'installation')],
|
||||
root = workspaceRoot
|
||||
): NxPlugin[] {
|
||||
const result: NxPlugin[] = [];
|
||||
|
||||
105
packages/nx/src/utils/workspace-root.spec.ts
Normal file
105
packages/nx/src/utils/workspace-root.spec.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import * as path from 'path';
|
||||
import * as fileUtils from './fileutils';
|
||||
import { workspaceRootInner } from './workspace-root';
|
||||
|
||||
type FileTree = {
|
||||
files?: string[];
|
||||
[path: string]: string[] | FileTree;
|
||||
};
|
||||
|
||||
const rootMarkers = ['nx.json', 'nx', 'nx.bat'];
|
||||
|
||||
describe('workspaceRootInner', () => {
|
||||
it.each(rootMarkers)('should find workspace root from %s', (marker) => {
|
||||
jest
|
||||
.spyOn(fileUtils, 'fileExists')
|
||||
.mockImplementation((p) =>
|
||||
[
|
||||
`/home/workspace/${marker}`,
|
||||
'/home/workspace/packages/a/package.json',
|
||||
'/home/workspace/packages/b/package.json',
|
||||
'/home/workspace/packages/c/package.json',
|
||||
].includes(p.toString())
|
||||
);
|
||||
|
||||
expect(workspaceRootInner('/home/workspace', null)).toEqual(
|
||||
'/home/workspace'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(rootMarkers)(
|
||||
'should find workspace root from %s when in subpackage',
|
||||
(marker) => {
|
||||
jest
|
||||
.spyOn(fileUtils, 'fileExists')
|
||||
.mockImplementation((p) =>
|
||||
[
|
||||
`/home/workspace/${marker}`,
|
||||
'/home/workspace/packages/a/package.json',
|
||||
'/home/workspace/packages/b/package.json',
|
||||
'/home/workspace/packages/c/package.json',
|
||||
].includes(p.toString())
|
||||
);
|
||||
|
||||
expect(workspaceRootInner('/home/workspace/packages/a', null)).toEqual(
|
||||
'/home/workspace'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(rootMarkers)(
|
||||
'should prefer workspace root from %s when in subpackage containing nx',
|
||||
(marker) => {
|
||||
jest
|
||||
.spyOn(fileUtils, 'fileExists')
|
||||
.mockImplementation((p) =>
|
||||
[
|
||||
`/home/workspace/${marker}`,
|
||||
'/home/workspace/packages/a/node_modules/nx/package.json',
|
||||
'/home/workspace/packages/a/package.json',
|
||||
'/home/workspace/packages/b/package.json',
|
||||
'/home/workspace/packages/c/package.json',
|
||||
].includes(p.toString())
|
||||
);
|
||||
|
||||
expect(workspaceRootInner('/home/workspace/packages/a', null)).toEqual(
|
||||
'/home/workspace'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('should find workspace root from installation when marker not present', () => {
|
||||
jest
|
||||
.spyOn(fileUtils, 'fileExists')
|
||||
.mockImplementation((p) =>
|
||||
[
|
||||
`/home/workspace/node_modules/nx/package.json`,
|
||||
'/home/workspace/packages/a/package.json',
|
||||
'/home/workspace/packages/b/package.json',
|
||||
'/home/workspace/packages/c/package.json',
|
||||
].includes(p.toString())
|
||||
);
|
||||
|
||||
expect(workspaceRootInner('/home/workspace/packages/a', null)).toEqual(
|
||||
'/home/workspace'
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer outer workspace root from installation when marker not present and nested', () => {
|
||||
jest
|
||||
.spyOn(fileUtils, 'fileExists')
|
||||
.mockImplementation((p) =>
|
||||
[
|
||||
`/home/workspace/node_modules/nx/package.json`,
|
||||
'/home/workspace/packages/a/node_modules/nx/package.json',
|
||||
'/home/workspace/packages/a/package.json',
|
||||
'/home/workspace/packages/b/package.json',
|
||||
'/home/workspace/packages/c/package.json',
|
||||
].includes(p.toString())
|
||||
);
|
||||
|
||||
expect(workspaceRootInner('/home/workspace/packages/a', null)).toEqual(
|
||||
'/home/workspace'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -13,8 +13,20 @@ export function workspaceRootInner(
|
||||
if (process.env.NX_WORKSPACE_ROOT_PATH)
|
||||
return process.env.NX_WORKSPACE_ROOT_PATH;
|
||||
if (path.dirname(dir) === dir) return candidateRoot;
|
||||
if (fileExists(path.join(dir, 'nx.json'))) {
|
||||
|
||||
const matches = [
|
||||
path.join(dir, 'nx.json'),
|
||||
path.join(dir, 'nx'),
|
||||
path.join(dir, 'nx.bat'),
|
||||
];
|
||||
|
||||
if (matches.some((x) => fileExists(x))) {
|
||||
return dir;
|
||||
|
||||
// This handles the case where we have a workspace which uses npm / yarn / pnpm
|
||||
// workspaces, and has a project which contains Nx in its dependency tree.
|
||||
// e.g. packages/my-lib/package.json contains @nrwl/devkit, which references Nx and is
|
||||
// thus located in //packages/my-lib/node_modules/nx/package.json
|
||||
} else if (fileExists(path.join(dir, 'node_modules', 'nx', 'package.json'))) {
|
||||
return workspaceRootInner(path.dirname(dir), dir);
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user