Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e81c5d3466 | |||
| 8d9a62d9de | |||
| 91eae77a51 | |||
| 5067887cdd | |||
| 76a62bd0d3 | |||
| 3771061076 | |||
| b856175d34 | |||
| 3369237b25 | |||
| dcc14e3f56 | |||
| 8398e6d980 | |||
| 4afbda5759 | |||
| afea39efe2 | |||
| 49bc160167 | |||
| 9a48b69cac | |||
| ccddf0ec92 | |||
| 0b03198cb4 | |||
| 2d4646d30c | |||
| 9b5a1cbd4f | |||
| f07ac05d62 | |||
| c5b0aea60c | |||
| 397da97e40 | |||
| 3db58e12fa | |||
| ea583069df | |||
| 72fe71a1a2 | |||
| 581ede1486 | |||
| f4ea297b7b | |||
| 39f8713dc9 | |||
| 645e2d8231 | |||
| 7a9a7799a0 | |||
| 8bdedc82e0 | |||
| fdfa18b1ba | |||
| 9e32083ce8 | |||
| 792092255f | |||
| 6ab9ae3626 | |||
| b633c0409c | |||
| becd93d923 | |||
| d21fd019dd | |||
| 7ac5ca5f16 | |||
| ed39d8cf36 | |||
| aa1fdf66ad | |||
| a0b4ac14ff | |||
| e981d11e8e | |||
| 6afcdf158b |
@@ -17,6 +17,10 @@
|
||||
{
|
||||
"group": ["nx/src/plugins/js*"],
|
||||
"message": "Imports from 'nx/src/plugins/js' are not allowed. Use '@nx/js' instead"
|
||||
},
|
||||
{
|
||||
"group": ["**/native-bindings", "**/native-bindings.js", ""],
|
||||
"message": "Direct imports from native-bindings.js are not allowed. Import from index.js instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
NX_RUN_GROUP: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -18,7 +19,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
- host: macos-13
|
||||
target: x86_64-apple-darwin
|
||||
build: |
|
||||
pnpm nx run-many --target=build-native -- --target=x86_64-apple-darwin
|
||||
@@ -36,12 +37,16 @@ jobs:
|
||||
build: |-
|
||||
set -e &&
|
||||
pnpm --version &&
|
||||
pnpm nx run-many --target=build-native -- --target=x86_64-unknown-linux-gnu
|
||||
pnpm install --frozen-lockfile &&
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=x86_64-unknown-linux-gnu
|
||||
- host: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
build: set -e && pnpm nx run-many --target=build-native -- --target=x86_64-unknown-linux-musl
|
||||
- host: macos-latest
|
||||
build: |-
|
||||
set -e &&
|
||||
pnpm install --frozen-lockfile &&
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=x86_64-unknown-linux-musl
|
||||
- host: macos-13
|
||||
target: aarch64-apple-darwin
|
||||
build: |
|
||||
sudo rm -Rf /Library/Developer/CommandLineTools/SDKs/*;
|
||||
@@ -56,7 +61,8 @@ jobs:
|
||||
build: |-
|
||||
set -e &&
|
||||
pnpm --version &&
|
||||
pnpm nx run-many --target=build-native -- --target=aarch64-unknown-linux-gnu
|
||||
pnpm install --frozen-lockfile &&
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=aarch64-unknown-linux-gnu
|
||||
- host: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
setup: |
|
||||
@@ -79,7 +85,8 @@ jobs:
|
||||
build: |-
|
||||
set -e &&
|
||||
rustup target add aarch64-unknown-linux-musl &&
|
||||
pnpm nx run-many --target=build-native -- --target=aarch64-unknown-linux-musl
|
||||
pnpm install --frozen-lockfile &&
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=aarch64-unknown-linux-musl
|
||||
- host: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
build: pnpm nx run-many --target=build-native -- --target=aarch64-pc-windows-msvc
|
||||
@@ -129,6 +136,7 @@ jobs:
|
||||
run: yarn config set supportedArchitectures.cpu "ia32"
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
run: pnpm install --frozen-lockfile
|
||||
timeout-minutes: 30
|
||||
- name: Setup node x86
|
||||
@@ -202,7 +210,7 @@ jobs:
|
||||
mkdir -p /Users/runner/work/_temp/_github_workflow
|
||||
echo "{}" > /Users/runner/work/_temp/_github_workflow/event.json
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
pnpm nx run-many --outputStyle stream --target=build-native -- --target=x86_64-unknown-freebsd
|
||||
pnpm nx run-many --verbose --outputStyle stream --target=build-native -- --target=x86_64-unknown-freebsd
|
||||
pnpm nx reset
|
||||
rm -rf node_modules
|
||||
rm -rf dist
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ packages/nx/src/plugins/js/lock-file/__fixtures__/**/*.*
|
||||
packages/**/schematics/**/files/**/*.html
|
||||
packages/**/generators/**/files/**/*.html
|
||||
packages/nx/src/native/**/*.rs
|
||||
packages/nx/src/native/index.js
|
||||
packages/nx/src/native/native-bindings.js
|
||||
packages/nx/src/native/index.d.ts
|
||||
nx-dev/nx-dev/.next/
|
||||
nx-dev/nx-dev/public/documentation
|
||||
|
||||
@@ -159,6 +159,12 @@ Type: `boolean`
|
||||
|
||||
Untracked changes
|
||||
|
||||
### verbose
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Prints additional information about the commands (e.g., stack traces)
|
||||
|
||||
### version
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
@@ -6,7 +6,7 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
|
||||
|
||||
### Properties
|
||||
|
||||
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): string[]
|
||||
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): readonly string[]
|
||||
- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration): NxJsonConfiguration<string[] | "\*">
|
||||
- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot): string
|
||||
|
||||
@@ -14,7 +14,7 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
|
||||
|
||||
### configFiles
|
||||
|
||||
• `Readonly` **configFiles**: `string`[]
|
||||
• `Readonly` **configFiles**: readonly `string`[]
|
||||
|
||||
The subset of configuration files which match the createNodes pattern
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ This will install the correct version of `@nx/eslint`.
|
||||
|
||||
### How @nx/eslint Infers Tasks
|
||||
|
||||
The `@nx/eslint` plugin will create a task for any project that has an ESLint configuration file present. Any of the following files will be recognized as an ESLint configuration file:
|
||||
The `@nx/eslint` plugin will create a task for any project that has an ESLint configuration file present and files to lint. Any of the following files will be recognized as an ESLint configuration file:
|
||||
|
||||
- `.eslintrc`
|
||||
- `.eslintrc.js`
|
||||
@@ -33,6 +33,8 @@ The `@nx/eslint` plugin will create a task for any project that has an ESLint co
|
||||
|
||||
Because ESLint applies configuration files to all subdirectories, the `@nx/eslint` plugin will also infer tasks for projects in subdirectories. So, if there is an ESLint configuration file in the root of the repository, every project will have an inferred ESLint task.
|
||||
|
||||
Even if a project has an ESLint configuration file, it will only have an inferred ESLint task if there are files to lint. Otherwise, the task will not be created. Therefore, if you don't want an ESLint task to be inferred for a particular project, make sure the project files are properly excluded from ESLint.
|
||||
|
||||
### View Inferred Tasks
|
||||
|
||||
To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project --web` in the command line.
|
||||
|
||||
@@ -159,6 +159,12 @@ Type: `boolean`
|
||||
|
||||
Untracked changes
|
||||
|
||||
### verbose
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Prints additional information about the commands (e.g., stack traces)
|
||||
|
||||
### version
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
@@ -121,6 +121,11 @@
|
||||
"items": { "type": "string" },
|
||||
"$default": { "$source": "unparsed" },
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"forwardAllArgs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether arguments should be forwarded when interpolation is not present.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
|
||||
@@ -19,12 +19,14 @@ You'll be entering it in the instructions below.
|
||||
|
||||
1. The Single Sign On URL needs to be:
|
||||
- If using the main-US cluster: `https://auth.nx.app/login/callback?connection=SAML-IDENTIFIER`
|
||||
- If using the EU cluster: `https://auth.eu.nx.app/login/callback?connection=SAML-IDENTIFIER`
|
||||
2. The Audience should be `urn:auth0:nrwl:SAML-IDENTIFIER`
|
||||
- If using the main-US cluster: `urn:auth0:nrwl:SAML-IDENTIFIER`
|
||||
- If using the EU cluster: `urn:auth0:nxcloud-eu:SAML-IDENTIFIER`
|
||||
|
||||

|
||||
{% callout type="note" title="EU Cluster" %}
|
||||
Contact your developer productivity engineer (DPE) to configure SAML auth in the EU cluster. The EU cluster is only available for enterprise customers.
|
||||
{% /callout %}
|
||||
|
||||

|
||||
|
||||
4. Scroll down to attribute statements and configure them as per below:
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ We offer multiple ways of running Nx Cloud for our Enterprise customers. The bel
|
||||
The quickest and easiest way to start using Nx Cloud is by utilizing our pre-existing secure, multi-tenant managed clusters:
|
||||
|
||||
- [https://nx.app/](https://nx.app/)
|
||||
- [https://eu.nx.app/](https://eu.nx.app/) if you have special restrictions and your data needs to be hosted in Europe.
|
||||
- Enterprise customers can contact their developer productivity engineer (DPE) to configure the EU hosted version of Nx Cloud.
|
||||
|
||||
You get the **same level of security**, **dedicated support**, **SSO/SAML auth** options and **predictable seat-based pricing** as all our other hosting options. But you won't have to manage the instance yourself.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ This will install the correct version of `@nx/eslint`.
|
||||
|
||||
### How @nx/eslint Infers Tasks
|
||||
|
||||
The `@nx/eslint` plugin will create a task for any project that has an ESLint configuration file present. Any of the following files will be recognized as an ESLint configuration file:
|
||||
The `@nx/eslint` plugin will create a task for any project that has an ESLint configuration file present and files to lint. Any of the following files will be recognized as an ESLint configuration file:
|
||||
|
||||
- `.eslintrc`
|
||||
- `.eslintrc.js`
|
||||
@@ -33,6 +33,8 @@ The `@nx/eslint` plugin will create a task for any project that has an ESLint co
|
||||
|
||||
Because ESLint applies configuration files to all subdirectories, the `@nx/eslint` plugin will also infer tasks for projects in subdirectories. So, if there is an ESLint configuration file in the root of the repository, every project will have an inferred ESLint task.
|
||||
|
||||
Even if a project has an ESLint configuration file, it will only have an inferred ESLint task if there are files to lint. Otherwise, the task will not be created. Therefore, if you don't want an ESLint task to be inferred for a particular project, make sure the project files are properly excluded from ESLint.
|
||||
|
||||
### View Inferred Tasks
|
||||
|
||||
To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project --web` in the command line.
|
||||
|
||||
@@ -64,6 +64,12 @@ dependencies {
|
||||
implementation(project(":app"))
|
||||
}`
|
||||
);
|
||||
updateFile(`app/build.gradle.kts`, (content) => {
|
||||
content += `\r\ntasks.register("task1"){
|
||||
println("REGISTER TASK1: This is executed during the configuration phase")
|
||||
}`;
|
||||
return content;
|
||||
});
|
||||
}
|
||||
updateFile(
|
||||
`settings.gradle${type === 'kotlin' ? '.kts' : ''}`,
|
||||
|
||||
@@ -217,6 +217,23 @@ module.exports = {
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it("should exclude 'test' target from e2e project that uses jest", async () => {
|
||||
const appName = uniq('nodeapp');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${appName} --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
|
||||
const nxJson = JSON.parse(readFile('nx.json'));
|
||||
expect(nxJson.plugins).toBeDefined();
|
||||
|
||||
const jestPlugin = nxJson.plugins.find(
|
||||
(p) => p.plugin === '@nx/jest/plugin'
|
||||
);
|
||||
expect(jestPlugin).toBeDefined();
|
||||
expect(jestPlugin.exclude).toContain(`${appName}-e2e/**/*`);
|
||||
});
|
||||
|
||||
it('should be able to generate an express application', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const originalEnvPort = process.env.PORT;
|
||||
|
||||
@@ -71,6 +71,9 @@ describe('file-server', () => {
|
||||
config.targets['build'] = {
|
||||
command: `node copy-index.js`,
|
||||
outputs: [`{workspaceRoot}/dist/foobar`],
|
||||
options: {
|
||||
cwd: '{projectRoot}',
|
||||
},
|
||||
};
|
||||
config.targets['serve'] = {
|
||||
executor: '@nx/web:file-server',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
|
||||
const htmlEl = document.documentElement;
|
||||
export const localStorageThemeKey = 'nx-dep-graph-theme';
|
||||
export type Theme = 'light' | 'dark' | 'system';
|
||||
@@ -48,6 +50,10 @@ export function getSystemTheme(): 'light' | 'dark' {
|
||||
if (isVSCodeDark || isVSCodeLight) {
|
||||
return isVSCodeDark ? 'dark' : 'light';
|
||||
}
|
||||
// we don't want to use system theme in nx-console because it might conflict with the IDE theme
|
||||
if (getEnvironmentConfig().environment === 'nx-console') {
|
||||
return 'light';
|
||||
}
|
||||
const isDarkMedia = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return isDarkMedia || isVSCodeDark ? 'dark' : 'light';
|
||||
}
|
||||
@@ -65,8 +71,9 @@ export function themeResolver(theme: Theme) {
|
||||
currentTheme = theme;
|
||||
} else {
|
||||
const resolver = getSystemTheme();
|
||||
|
||||
darkMedia.addEventListener('change', mediaListener);
|
||||
if (getEnvironmentConfig().environment !== 'nx-console') {
|
||||
darkMedia.addEventListener('change', mediaListener);
|
||||
}
|
||||
vscodeDarkOberserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
NEXT_PUBLIC_SEARCH_INDEX=nx-canary-production
|
||||
NEXT_PUBLIC_SEARCH_API_KEY=07340b85db83fd49c1f904f883cc3ef8
|
||||
NEXT_PUBLIC_SEARCH_APP_ID=PCTGM1JTQL
|
||||
NEXT_PUBLIC_NO_INDEX=false
|
||||
@@ -46,6 +46,9 @@ export default function CustomApp({
|
||||
site: '@nxdevtools',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
dangerouslySetAllPagesToNoIndex={
|
||||
process.env.NEXT_PUBLIC_NO_INDEX === 'true'
|
||||
}
|
||||
/>
|
||||
<Head>
|
||||
<meta name="apple-mobile-web-app-title" content="Nx" />
|
||||
@@ -71,7 +74,7 @@ export default function CustomApp({
|
||||
id="skip-to-content-link"
|
||||
href="#main"
|
||||
tabIndex={0}
|
||||
className="absolute top-3 left-8 -translate-y-24 rounded-md bg-green-400 px-4 py-2 text-white transition focus:translate-y-0"
|
||||
className="absolute left-8 top-3 -translate-y-24 rounded-md bg-green-400 px-4 py-2 text-white transition focus:translate-y-0"
|
||||
>
|
||||
Skip to content
|
||||
</Link>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { colors } from 'ng-packagr/lib/utils/color';
|
||||
import { getTailwindConfigPath } from './tailwindcss';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
import type { PostcssConfiguration } from 'ng-packagr/lib/styles/postcss-configuration';
|
||||
import { gte } from 'semver';
|
||||
import { gt } from 'semver';
|
||||
import { getInstalledPackageVersionInfo } from '../angular-version-utils';
|
||||
|
||||
const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];
|
||||
@@ -94,7 +94,7 @@ export class StylesheetProcessor {
|
||||
const { version: ngPackagrVersion } =
|
||||
getInstalledPackageVersionInfo('ng-packagr');
|
||||
let postcssConfiguration: PostcssConfiguration | undefined;
|
||||
if (gte(ngPackagrVersion, '17.3.0')) {
|
||||
if (gt(ngPackagrVersion, '17.2.0')) {
|
||||
const {
|
||||
loadPostcssConfiguration,
|
||||
} = require('ng-packagr/lib/styles/postcss-configuration');
|
||||
|
||||
@@ -10,7 +10,7 @@ import { addPlugin, generateCombinations } from './add-plugin';
|
||||
|
||||
describe('addPlugin', () => {
|
||||
let tree: Tree;
|
||||
let createNodes: CreateNodes;
|
||||
let createNodes: CreateNodes<{ targetName: string }>;
|
||||
let graph: ProjectGraph;
|
||||
let fs: TempFs;
|
||||
|
||||
@@ -30,9 +30,27 @@ describe('addPlugin', () => {
|
||||
targets: {},
|
||||
},
|
||||
},
|
||||
app2: {
|
||||
name: 'app2',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app2',
|
||||
targets: {},
|
||||
},
|
||||
},
|
||||
app3: {
|
||||
name: 'app3',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app3',
|
||||
targets: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
app1: [],
|
||||
app2: [],
|
||||
app3: [],
|
||||
},
|
||||
};
|
||||
createNodes = [
|
||||
@@ -45,12 +63,19 @@ describe('addPlugin', () => {
|
||||
[targetName]: { command: 'next build' },
|
||||
},
|
||||
},
|
||||
app2: {
|
||||
name: 'app2',
|
||||
targets: {
|
||||
[targetName]: { command: 'next build' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
await fs.createFiles({
|
||||
'app1/next.config.js': '',
|
||||
'app2/next.config.js': '',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +86,10 @@ describe('addPlugin', () => {
|
||||
describe('adding the plugin', () => {
|
||||
it('should not conflicting with the existing graph', async () => {
|
||||
graph.nodes.app1.data.targets.build = {};
|
||||
graph.nodes.app2.data.targets.build1 = {};
|
||||
// app 3 doesn't have a next config, so it having this
|
||||
// target should not affect the plugin options
|
||||
graph.nodes.app3.data.targets.build2 = {};
|
||||
|
||||
await addPlugin(
|
||||
tree,
|
||||
@@ -69,7 +98,7 @@ describe('addPlugin', () => {
|
||||
createNodes,
|
||||
|
||||
{
|
||||
targetName: ['build', '_build'],
|
||||
targetName: ['build', 'build1', 'build2'],
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -77,7 +106,7 @@ describe('addPlugin', () => {
|
||||
expect(readJson(tree, 'nx.json').plugins).toContainEqual({
|
||||
plugin: '@nx/next/plugin',
|
||||
options: {
|
||||
targetName: '_build',
|
||||
targetName: 'build2',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -372,6 +401,7 @@ describe('addPlugin', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCombinations', () => {
|
||||
it('should return all combinations for a 2x2 array of strings', () => {
|
||||
const input = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { dirname, extname, join } from 'path';
|
||||
import { dirname, extname, join, sep } from 'path';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { requireNx } from '../../nx';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const { workspaceRoot, registerTsProject } = requireNx();
|
||||
|
||||
@@ -53,15 +54,26 @@ export function getRootTsConfigFileName(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageInstallationDirectories = [
|
||||
`${sep}node_modules${sep}`,
|
||||
`${sep}.yarn${sep}`,
|
||||
];
|
||||
|
||||
export function clearRequireCache(): void {
|
||||
for (const k of Object.keys(require.cache)) {
|
||||
if (!packageInstallationDirectories.some((dir) => k.includes(dir))) {
|
||||
delete require.cache[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the module after ensuring that the require cache is cleared.
|
||||
*/
|
||||
async function load(path: string): Promise<any> {
|
||||
// Clear cache if the path is in the cache
|
||||
if (require.cache[path]) {
|
||||
for (const k of Object.keys(require.cache)) {
|
||||
delete require.cache[k];
|
||||
}
|
||||
clearRequireCache();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -70,8 +82,9 @@ async function load(path: string): Promise<any> {
|
||||
return require(path);
|
||||
} catch (e: any) {
|
||||
if (e.code === 'ERR_REQUIRE_ESM') {
|
||||
// If `require` fails to load ESM, try dynamic `import()`.
|
||||
return await dynamicImport(`${path}?t=${Date.now()}`);
|
||||
// If `require` fails to load ESM, try dynamic `import()`. ESM requires file url protocol for handling absolute paths.
|
||||
const pathAsFileUrl = pathToFileURL(path).pathname;
|
||||
return await dynamicImport(`${pathAsFileUrl}?t=${Date.now()}`);
|
||||
}
|
||||
|
||||
// Re-throw all other errors
|
||||
|
||||
@@ -3,11 +3,14 @@ import type { Executor, ExecutorContext } from 'nx/src/config/misc-interfaces';
|
||||
import type { ProjectsConfigurations } from 'nx/src/devkit-exports';
|
||||
|
||||
import { requireNx } from '../../nx';
|
||||
import { NX_VERSION } from './package-json';
|
||||
import { lt } from 'semver';
|
||||
|
||||
const {
|
||||
Workspaces,
|
||||
readNxJsonFromDisk,
|
||||
retrieveProjectConfigurationsWithAngularProjects,
|
||||
readProjectConfigurationsFromRootMap,
|
||||
} = requireNx();
|
||||
|
||||
/**
|
||||
@@ -32,7 +35,24 @@ export function convertNxExecutor(executor: Executor) {
|
||||
projects: await retrieveProjectConfigurationsWithAngularProjects(
|
||||
builderContext.workspaceRoot,
|
||||
nxJsonConfiguration
|
||||
).then((p) => (p as any).projectNodes ?? p.projects),
|
||||
).then((p) => {
|
||||
if ((p as any).projectNodes) {
|
||||
return (p as any).projectNodes;
|
||||
}
|
||||
// v18.3.4 changed projects to be keyed by root
|
||||
// rather than project name
|
||||
if (lt(NX_VERSION, '18.3.4')) {
|
||||
return p.projects;
|
||||
}
|
||||
|
||||
if (readProjectConfigurationsFromRootMap) {
|
||||
return readProjectConfigurationsFromRootMap(p.projects);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Unable to successfully map Nx executor -> Angular Builder'
|
||||
);
|
||||
}),
|
||||
}
|
||||
: // TODO(v19): remove retrieveProjectConfigurations. This is to be backwards compatible with Nx 16.5 and below.
|
||||
(workspaces as any).readProjectsConfigurations({
|
||||
|
||||
@@ -80,4 +80,118 @@ describe('getExtraDependencies', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should include npm dependencies of circular dependency child libs', () => {
|
||||
const result = getExtraDependencies('parent', {
|
||||
nodes: {
|
||||
parent: {
|
||||
type: 'app',
|
||||
name: 'parent',
|
||||
data: {},
|
||||
},
|
||||
child1: {
|
||||
type: 'lib',
|
||||
name: 'child1',
|
||||
data: {},
|
||||
},
|
||||
child2: {
|
||||
type: 'lib',
|
||||
name: 'child2',
|
||||
data: {},
|
||||
},
|
||||
child3: {
|
||||
type: 'lib',
|
||||
name: 'child3',
|
||||
data: {},
|
||||
},
|
||||
child4: {
|
||||
type: 'lib',
|
||||
name: 'child4',
|
||||
data: {},
|
||||
},
|
||||
} as any,
|
||||
externalNodes: {
|
||||
'npm:react': {
|
||||
type: 'npm',
|
||||
name: 'npm:react',
|
||||
data: { packageName: 'react', version: '18.0.0' },
|
||||
},
|
||||
'npm:axios': {
|
||||
type: 'npm',
|
||||
name: 'npm:axios',
|
||||
data: { packageName: 'axios', version: '1.0.0' },
|
||||
},
|
||||
'npm:dayjs': {
|
||||
type: 'npm',
|
||||
name: 'npm:dayjs',
|
||||
data: { packageName: 'dayjs', version: '1.11.0' },
|
||||
},
|
||||
'npm:date-fns': {
|
||||
type: 'npm',
|
||||
name: 'npm:date-fns',
|
||||
data: { packageName: 'date-fns', version: '2.0.0' },
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
parent: [
|
||||
{ source: 'parent', target: 'child1', type: 'static' },
|
||||
{ source: 'parent', target: 'npm:react', type: 'static' },
|
||||
],
|
||||
child1: [
|
||||
{ source: 'child1', target: 'child2', type: 'static' },
|
||||
{ source: 'child1', target: 'child3', type: 'static' },
|
||||
{ source: 'child1', target: 'npm:axios', type: 'static' },
|
||||
{ source: 'child1', target: 'child4', type: 'static' },
|
||||
],
|
||||
child2: [
|
||||
{ source: 'child2', target: 'npm:dayjs', type: 'static' },
|
||||
{ source: 'child2', target: 'child3', type: 'static' },
|
||||
],
|
||||
child3: [{ source: 'child3', target: 'child4', type: 'static' }],
|
||||
child4: [
|
||||
{ source: 'child4', target: 'child3', type: 'static' },
|
||||
{ source: 'child4', target: 'npm:date-fns', type: 'static' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'npm:react',
|
||||
outputs: [],
|
||||
node: {
|
||||
type: 'npm',
|
||||
name: 'npm:react',
|
||||
data: { packageName: 'react', version: '18.0.0' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'npm:axios',
|
||||
outputs: [],
|
||||
node: {
|
||||
type: 'npm',
|
||||
name: 'npm:axios',
|
||||
data: { packageName: 'axios', version: '1.0.0' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'npm:dayjs',
|
||||
outputs: [],
|
||||
node: {
|
||||
type: 'npm',
|
||||
name: 'npm:dayjs',
|
||||
data: { packageName: 'dayjs', version: '1.11.0' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'npm:date-fns',
|
||||
outputs: [],
|
||||
node: {
|
||||
type: 'npm',
|
||||
name: 'npm:date-fns',
|
||||
data: { packageName: 'date-fns', version: '2.0.0' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,14 @@ export function getExtraDependencies(
|
||||
graph: ProjectGraph
|
||||
): DependentBuildableProjectNode[] {
|
||||
const deps = new Map<string, DependentBuildableProjectNode>();
|
||||
const visited = new Set<string>(); // Track visited projects
|
||||
|
||||
recur(projectName);
|
||||
|
||||
function recur(currProjectName) {
|
||||
if (visited.has(currProjectName)) return; // Check if project already visited
|
||||
visited.add(currProjectName); // Mark project as visited
|
||||
|
||||
const allDeps = graph.dependencies[currProjectName];
|
||||
const externalDeps = allDeps.reduce((acc, node) => {
|
||||
const found = graph.externalNodes[node.target];
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import 'nx/src/internal-testing-utils/mock-fs';
|
||||
|
||||
jest.mock(
|
||||
'nx/src/utils/workspace-context',
|
||||
(): Partial<typeof import('nx/src/utils/workspace-context')> => {
|
||||
const glob = require('fast-glob');
|
||||
return {
|
||||
globWithWorkspaceContext(workspaceRoot: string, patterns: string[]) {
|
||||
// This glob will operate on memfs thanks to 'nx/src/internal-testing-utils/mock-fs'
|
||||
return glob.sync(patterns, { cwd: workspaceRoot });
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
import { CreateNodesContext } from '@nx/devkit';
|
||||
import { vol } from 'memfs';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
|
||||
import { createNodes } from './plugin';
|
||||
|
||||
describe('@nx/eslint/plugin', () => {
|
||||
let context: CreateNodesContext;
|
||||
let tempFs: TempFs;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempFs = new TempFs('eslint-plugin');
|
||||
context = {
|
||||
nxJsonConfiguration: {
|
||||
// These defaults should be overridden by the plugin
|
||||
@@ -36,24 +23,22 @@ describe('@nx/eslint/plugin', () => {
|
||||
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||
},
|
||||
},
|
||||
workspaceRoot: '',
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
jest.resetModules();
|
||||
tempFs.cleanup();
|
||||
tempFs = null;
|
||||
});
|
||||
|
||||
it('should not create any nodes when there are no eslint configs', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'package.json': `{}`,
|
||||
'project.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'package.json': `{}`,
|
||||
'project.json': `{}`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -64,13 +49,10 @@ describe('@nx/eslint/plugin', () => {
|
||||
|
||||
describe('root eslint config only', () => {
|
||||
it('should not create any nodes for just a package.json and root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -79,14 +61,14 @@ describe('@nx/eslint/plugin', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create a node for a root level eslint config when accompanied by a project.json, if no src directory is present', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'eslint.config.js': `module.exports = {};`,
|
||||
'project.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
// TODO(leo): dynamic import of the flat config fails with jest:
|
||||
// "TypeError: A dynamic import callback was invoked without --experimental-vm-modules"
|
||||
// mocking the "eslint.config.js" file import is not working, figure out if there's a way
|
||||
it.skip('should not create a node for a root level eslint config when accompanied by a project.json, if no src directory is present', async () => {
|
||||
createFiles({
|
||||
'eslint.config.js': `module.exports = {};`,
|
||||
'project.json': `{}`,
|
||||
});
|
||||
// NOTE: It should set ESLINT_USE_FLAT_CONFIG to true because of the use of eslint.config.js
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -98,14 +80,11 @@ describe('@nx/eslint/plugin', () => {
|
||||
|
||||
// Standalone Nx workspace style setup
|
||||
it('should create a node for just a package.json and root level eslint config if accompanied by a src directory', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
// NOTE: The command is specifically targeting the src directory in the case of a standalone Nx workspace
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -138,16 +117,44 @@ describe('@nx/eslint/plugin', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create a node for a nested project (with a project.json and any lintable file) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
it('should not create a node for just a package.json and root level eslint config if accompanied by a src directory when all files are ignored (.eslintignore)', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'.eslintignore': `**/*`,
|
||||
'package.json': `{}`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
// NOTE: The command is specifically targeting the src directory in the case of a standalone Nx workspace
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create a node for just a package.json and root level eslint config if accompanied by a src directory when all files are ignored (ignorePatterns in .eslintrc.json)', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
|
||||
'package.json': `{}`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
// NOTE: The command is specifically targeting the src directory in the case of a standalone Nx workspace
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create a node for a nested project (with a project.json and any lintable file) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -180,15 +187,12 @@ describe('@nx/eslint/plugin', () => {
|
||||
});
|
||||
|
||||
it('should create a node for a nested project (with a package.json and any lintable file) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -221,19 +225,16 @@ describe('@nx/eslint/plugin', () => {
|
||||
});
|
||||
|
||||
it('should not create a node for a nested project (with a package.json and no lintable files) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -243,19 +244,46 @@ describe('@nx/eslint/plugin', () => {
|
||||
});
|
||||
|
||||
it('should not create a node for a nested project (with a project.json and no lintable files) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
},
|
||||
context
|
||||
);
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create a node for a nested project (with a project.json and all files ignored) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create a node for a nested project (with a package.json and all files ignored) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -267,17 +295,14 @@ describe('@nx/eslint/plugin', () => {
|
||||
|
||||
describe('nested eslint configs only', () => {
|
||||
it('should create appropriate nodes for nested projects without a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
@@ -330,23 +355,56 @@ describe('@nx/eslint/plugin', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create nodes for nested projects without a root level eslint config when all files are ignored (.eslintignore)', async () => {
|
||||
createFiles({
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/.eslintignore': `**/*`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/.eslintignore': `**/*`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create nodes for nested projects without a root level eslint config when all files are ignored (ignorePatterns in .eslintrc.json)', async () => {
|
||||
createFiles({
|
||||
'apps/my-app/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('root eslint config and nested eslint configs', () => {
|
||||
it('should create appropriate nodes for just a package.json and root level eslint config combined with nested eslint configs', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
});
|
||||
// NOTE: The nested projects have the root level config as an input to their lint targets
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -404,15 +462,12 @@ describe('@nx/eslint/plugin', () => {
|
||||
});
|
||||
|
||||
it('should create appropriate nodes for a nested project without its own eslint config but with an orphaned eslint config in its parent hierarchy', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': '{}',
|
||||
'apps/.eslintrc.json': '{}',
|
||||
'apps/myapp/project.json': '{}',
|
||||
'apps/myapp/index.ts': 'console.log("hello world")',
|
||||
},
|
||||
context
|
||||
);
|
||||
createFiles({
|
||||
'.eslintrc.json': '{}',
|
||||
'apps/.eslintrc.json': '{}',
|
||||
'apps/myapp/project.json': '{}',
|
||||
'apps/myapp/index.ts': 'console.log("hello world")',
|
||||
});
|
||||
// NOTE: The nested projects have the root level config as an input to their lint targets
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -445,7 +500,58 @@ describe('@nx/eslint/plugin', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle multiple levels of nesting and ignored files correctly', async () => {
|
||||
createFiles({
|
||||
'.eslintrc.json': '{ "root": true, "ignorePatterns": ["**/*"] }',
|
||||
'apps/myapp/.eslintrc.json': '{ "extends": "../../.eslintrc.json" }', // no lintable files, don't create task
|
||||
'apps/myapp/project.json': '{}',
|
||||
'apps/myapp/index.ts': 'console.log("hello world")',
|
||||
'apps/myapp/nested/mylib/.eslintrc.json': JSON.stringify({
|
||||
extends: '../../../../.eslintrc.json',
|
||||
ignorePatterns: ['!**/*'], // include all files, create task
|
||||
}),
|
||||
'apps/myapp/nested/mylib/project.json': '{}',
|
||||
'apps/myapp/nested/mylib/index.ts': 'console.log("hello world")',
|
||||
});
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"apps/myapp/nested/mylib": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{projectRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/myapp/nested/mylib",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
function createFiles(fileSys: Record<string, string>) {
|
||||
tempFs.createFilesSync(fileSys);
|
||||
// @ts-expect-error update otherwise readonly property for testing
|
||||
context.configFiles = getMatchingFiles(Object.keys(fileSys));
|
||||
}
|
||||
});
|
||||
|
||||
function getMatchingFiles(allConfigFiles: string[]): string[] {
|
||||
@@ -454,15 +560,6 @@ function getMatchingFiles(allConfigFiles: string[]): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function applyFilesToVolAndContext(
|
||||
fileSys: Record<string, string>,
|
||||
context: CreateNodesContext
|
||||
) {
|
||||
vol.fromJSON(fileSys, '');
|
||||
// @ts-expect-error update otherwise readonly property for testing
|
||||
context.configFiles = getMatchingFiles(Object.keys(fileSys));
|
||||
}
|
||||
|
||||
async function invokeCreateNodesOnMatchingFiles(
|
||||
context: CreateNodesContext,
|
||||
targetName: string
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
CreateNodesResult,
|
||||
TargetConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import type { ESLint } from 'eslint';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { dirname, join, normalize, sep } from 'node:path';
|
||||
import { combineGlobPatterns } from 'nx/src/utils/globs';
|
||||
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
||||
import {
|
||||
@@ -28,48 +29,73 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
|
||||
baseEsLintConfigFile,
|
||||
baseEsLintFlatConfigFile,
|
||||
]),
|
||||
(configFilePath, options, context) => {
|
||||
async (configFilePath, options, context) => {
|
||||
options = normalizeOptions(options);
|
||||
const configDir = dirname(configFilePath);
|
||||
|
||||
// Ensure that configFiles are set, e2e-run fails due to them being undefined in CI (does not occur locally)
|
||||
// TODO(JamesHenry): Further troubleshoot this in CI
|
||||
(context as any).configFiles = context.configFiles ?? [];
|
||||
|
||||
// Create a Set of all the directories containing eslint configs
|
||||
const eslintRoots = new Set(context.configFiles.map(dirname));
|
||||
const configDir = dirname(configFilePath);
|
||||
// Create a Set of all the directories containing eslint configs, and a
|
||||
// list of globs to exclude from child projects
|
||||
const eslintRoots = new Set();
|
||||
const nestedEslintRootPatterns: string[] = [];
|
||||
for (const configFile of context.configFiles) {
|
||||
const eslintRootDir = dirname(configFile);
|
||||
eslintRoots.add(eslintRootDir);
|
||||
|
||||
const childProjectRoots = globWithWorkspaceContext(
|
||||
if (eslintRootDir !== configDir && isSubDir(configDir, eslintRootDir)) {
|
||||
nestedEslintRootPatterns.push(`${eslintRootDir}/**/*`);
|
||||
}
|
||||
}
|
||||
|
||||
const projectFiles = globWithWorkspaceContext(
|
||||
context.workspaceRoot,
|
||||
[
|
||||
'project.json',
|
||||
'package.json',
|
||||
'**/project.json',
|
||||
'**/package.json',
|
||||
].map((f) => join(configDir, f))
|
||||
)
|
||||
.map((f) => dirname(f))
|
||||
.filter((childProjectRoot) => {
|
||||
// Filter out projects under other eslint configs
|
||||
let root = childProjectRoot;
|
||||
// Traverse up from the childProjectRoot to either the workspaceRoot or the dir of this config file
|
||||
while (root !== dirname(root) && root !== dirname(configFilePath)) {
|
||||
if (eslintRoots.has(root)) {
|
||||
return false;
|
||||
}
|
||||
root = dirname(root);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.filter((dir) => {
|
||||
// Ignore project roots where the project does not contain any lintable files
|
||||
const lintableFiles = globWithWorkspaceContext(context.workspaceRoot, [
|
||||
join(dir, `**/*.{${options.extensions.join(',')}}`),
|
||||
]);
|
||||
return lintableFiles.length > 0;
|
||||
});
|
||||
].map((f) => join(configDir, f)),
|
||||
nestedEslintRootPatterns.length ? nestedEslintRootPatterns : undefined
|
||||
);
|
||||
// dedupe and sort project roots by depth for more efficient traversal
|
||||
const dedupedProjectRoots = Array.from(
|
||||
new Set(projectFiles.map((f) => dirname(f)))
|
||||
).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1));
|
||||
const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`);
|
||||
|
||||
const uniqueChildProjectRoots = Array.from(new Set(childProjectRoots));
|
||||
const ESLint = resolveESLintClass(isFlatConfig(configFilePath));
|
||||
const childProjectRoots = new Set<string>();
|
||||
|
||||
await Promise.all(
|
||||
dedupedProjectRoots.map(async (childProjectRoot, index) => {
|
||||
// anything after is either a nested project or a sibling project, can be excluded
|
||||
const nestedProjectRootPatterns = excludePatterns.slice(index + 1);
|
||||
|
||||
// Ignore project roots where the project does not contain any lintable files
|
||||
const lintableFiles = globWithWorkspaceContext(
|
||||
context.workspaceRoot,
|
||||
[join(childProjectRoot, `**/*.{${options.extensions.join(',')}}`)],
|
||||
// exclude nested eslint roots and nested project roots
|
||||
[...nestedEslintRootPatterns, ...nestedProjectRootPatterns]
|
||||
);
|
||||
const eslint = new ESLint({
|
||||
cwd: join(context.workspaceRoot, childProjectRoot),
|
||||
});
|
||||
for (const file of lintableFiles) {
|
||||
if (
|
||||
!(await eslint.isPathIgnored(join(context.workspaceRoot, file)))
|
||||
) {
|
||||
childProjectRoots.add(childProjectRoot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const uniqueChildProjectRoots = Array.from(childProjectRoots);
|
||||
|
||||
return {
|
||||
projects: getProjectsUsingESLintConfig(
|
||||
@@ -118,6 +144,7 @@ function getProjectsUsingESLintConfig(
|
||||
targets: buildEslintTargets(
|
||||
eslintConfigs,
|
||||
projectRoot,
|
||||
context.workspaceRoot,
|
||||
options,
|
||||
isStandaloneWorkspace
|
||||
),
|
||||
@@ -130,6 +157,7 @@ function getProjectsUsingESLintConfig(
|
||||
function buildEslintTargets(
|
||||
eslintConfigs: string[],
|
||||
projectRoot: string,
|
||||
workspaceRoot: string,
|
||||
options: EslintPluginOptions,
|
||||
isStandaloneWorkspace = false
|
||||
) {
|
||||
@@ -153,6 +181,9 @@ function buildEslintTargets(
|
||||
isRootProject ? '{projectRoot}/' : '{projectRoot}'
|
||||
)
|
||||
),
|
||||
...(existsSync(join(workspaceRoot, projectRoot, '.eslintignore'))
|
||||
? ['{projectRoot}/.eslintignore']
|
||||
: []),
|
||||
'{workspaceRoot}/tools/eslint-rules/**/*',
|
||||
{ externalDependencies: ['eslint'] },
|
||||
],
|
||||
@@ -181,3 +212,35 @@ function normalizeOptions(options: EslintPluginOptions): EslintPluginOptions {
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function resolveESLintClass(useFlatConfig = false): typeof ESLint {
|
||||
try {
|
||||
if (!useFlatConfig) {
|
||||
return require('eslint').ESLint;
|
||||
}
|
||||
|
||||
return require('eslint/use-at-your-own-risk').FlatESLint;
|
||||
} catch {
|
||||
throw new Error('Unable to find ESLint. Ensure ESLint is installed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `child` is a subdirectory of `parent`. This is a simplified
|
||||
* version that takes into account that paths are always relative to the
|
||||
* workspace root.
|
||||
*/
|
||||
function isSubDir(parent: string, child: string): boolean {
|
||||
if (parent === '.') {
|
||||
return true;
|
||||
}
|
||||
|
||||
parent = normalize(parent);
|
||||
child = normalize(child);
|
||||
|
||||
if (!parent.endsWith(sep)) {
|
||||
parent += sep;
|
||||
}
|
||||
|
||||
return child.startsWith(parent);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
|
||||
|
||||
export interface ExpoPluginOptions {
|
||||
startTargetName?: string;
|
||||
@@ -59,7 +60,7 @@ export const createDependencies: CreateDependencies = () => {
|
||||
|
||||
export const createNodes: CreateNodes<ExpoPluginOptions> = [
|
||||
'**/app.{json,config.js}',
|
||||
(configFilePath, options, context) => {
|
||||
async (configFilePath, options, context) => {
|
||||
options = normalizeOptions(options);
|
||||
const projectRoot = dirname(configFilePath);
|
||||
|
||||
@@ -71,7 +72,7 @@ export const createNodes: CreateNodes<ExpoPluginOptions> = [
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
const appConfig = getAppConfig(configFilePath, context);
|
||||
const appConfig = await getAppConfig(configFilePath, context);
|
||||
// if appConfig.expo is not defined
|
||||
if (!appConfig.expo) {
|
||||
return {};
|
||||
@@ -152,11 +153,10 @@ function buildExpoTargets(
|
||||
function getAppConfig(
|
||||
configFilePath: string,
|
||||
context: CreateNodesContext
|
||||
): any {
|
||||
): Promise<any> {
|
||||
const resolvedPath = join(context.workspaceRoot, configFilePath);
|
||||
|
||||
let module = load(resolvedPath);
|
||||
return module.default ?? module;
|
||||
return loadConfigFile(resolvedPath);
|
||||
}
|
||||
|
||||
function getInputs(
|
||||
@@ -180,21 +180,6 @@ function getOutputs(projectRoot: string, dir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the module after ensuring that the require cache is cleared.
|
||||
*/
|
||||
function load(path: string): any {
|
||||
// Clear cache if the path is in the cache
|
||||
if (require.cache[path]) {
|
||||
for (const k of Object.keys(require.cache)) {
|
||||
delete require.cache[k];
|
||||
}
|
||||
}
|
||||
|
||||
// Then require
|
||||
return require(path);
|
||||
}
|
||||
|
||||
function normalizeOptions(options: ExpoPluginOptions): ExpoPluginOptions {
|
||||
options ??= {};
|
||||
options.startTargetName ??= 'start';
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { CreateNodesContext } from '@nx/devkit';
|
||||
|
||||
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
|
||||
import type { GradleReport } from '../utils/get-gradle-report';
|
||||
|
||||
let gradleReport: GradleReport;
|
||||
jest.mock('../utils/get-gradle-report.ts', () => {
|
||||
return {
|
||||
getGradleReport: jest.fn().mockImplementation(() => gradleReport),
|
||||
};
|
||||
});
|
||||
|
||||
import { createNodes } from './nodes';
|
||||
|
||||
describe('@nx/gradle/plugin', () => {
|
||||
let createNodesFunction = createNodes[1];
|
||||
let context: CreateNodesContext;
|
||||
let tempFs: TempFs;
|
||||
let cwd: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempFs = new TempFs('test');
|
||||
gradleReport = {
|
||||
gradleFileToGradleProjectMap: new Map<string, string>([
|
||||
['proj/gradle.build', 'proj'],
|
||||
]),
|
||||
buildFileToDepsMap: new Map<string, string>(),
|
||||
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
|
||||
['proj/gradle.build', new Map([['build', 'build']])],
|
||||
]),
|
||||
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
|
||||
['proj', new Map([['test', 'Test']])],
|
||||
]),
|
||||
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
|
||||
};
|
||||
cwd = process.cwd();
|
||||
process.chdir(tempFs.tempDir);
|
||||
context = {
|
||||
nxJsonConfiguration: {
|
||||
namedInputs: {
|
||||
default: ['{projectRoot}/**/*'],
|
||||
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
await tempFs.createFiles({
|
||||
'proj/gradle.build': ``,
|
||||
gradlew: '',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
process.chdir(cwd);
|
||||
});
|
||||
|
||||
it('should create nodes based on gradle', async () => {
|
||||
const nodes = await createNodesFunction(
|
||||
'proj/gradle.build',
|
||||
{
|
||||
buildTargetName: 'build',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(nodes.projects.proj).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"technologies": [
|
||||
"gradle",
|
||||
],
|
||||
},
|
||||
"name": "proj",
|
||||
"targets": {
|
||||
"test": {
|
||||
"cache": false,
|
||||
"command": "../gradlew test",
|
||||
"dependsOn": [
|
||||
"classes",
|
||||
],
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production",
|
||||
],
|
||||
"options": {
|
||||
"cwd": "proj",
|
||||
},
|
||||
"outputs": undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create nodes based on gradle for nested project root', async () => {
|
||||
gradleReport = {
|
||||
gradleFileToGradleProjectMap: new Map<string, string>([
|
||||
['nested/nested/proj/gradle.build', 'proj'],
|
||||
]),
|
||||
buildFileToDepsMap: new Map<string, string>(),
|
||||
gradleFileToOutputDirsMap: new Map<string, Map<string, string>>([
|
||||
['nested/nested/proj/gradle.build', new Map([['build', 'build']])],
|
||||
]),
|
||||
gradleProjectToTasksTypeMap: new Map<string, Map<string, string>>([
|
||||
['proj', new Map([['test', 'Test']])],
|
||||
]),
|
||||
gradleProjectToProjectName: new Map<string, string>([['proj', 'proj']]),
|
||||
};
|
||||
await tempFs.createFiles({
|
||||
'nested/nested/proj/gradle.build': ``,
|
||||
});
|
||||
|
||||
const nodes = await createNodesFunction(
|
||||
'nested/nested/proj/gradle.build',
|
||||
{
|
||||
buildTargetName: 'build',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(nodes.projects['nested/nested/proj']).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"technologies": [
|
||||
"gradle",
|
||||
],
|
||||
},
|
||||
"name": "proj",
|
||||
"targets": {
|
||||
"test": {
|
||||
"cache": false,
|
||||
"command": "../../../gradlew test",
|
||||
"dependsOn": [
|
||||
"classes",
|
||||
],
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production",
|
||||
],
|
||||
"options": {
|
||||
"cwd": "nested/nested/proj",
|
||||
},
|
||||
"outputs": undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
ProjectConfiguration,
|
||||
TargetConfiguration,
|
||||
readJsonFile,
|
||||
workspaceRoot,
|
||||
writeJsonFile,
|
||||
} from '@nx/devkit';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||
|
||||
import { getGradleBinaryPath } from '../utils/exec-gradle';
|
||||
@@ -180,8 +181,12 @@ function createGradleTargets(
|
||||
const targetName = options?.[`${task.name}TargetName`] ?? task.name;
|
||||
|
||||
const outputs = outputDirs.get(task.name);
|
||||
const path = relative(
|
||||
join(context.workspaceRoot, projectRoot),
|
||||
getGradleBinaryPath()
|
||||
);
|
||||
targets[targetName] = {
|
||||
command: `${getGradleBinaryPath()} ${task.name}`,
|
||||
command: `${path} ${task.name}`,
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
> Task :dependencyReport
|
||||
See the report at: file:///tmp/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :htmlDependencyReport
|
||||
See the report at: file:///tmp/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :propertyReport
|
||||
See the report at: file:///tmp/build/reports/project/properties.txt
|
||||
|
||||
> Task :taskReport
|
||||
See the report at: file:///tmp/build/reports/project/tasks.txt
|
||||
|
||||
> Task :app:dependencyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :app:htmlDependencyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :app:propertyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/properties.txt
|
||||
|
||||
> Task :app:taskReport
|
||||
REGISTER TASK1: This is executed during the configuration phase
|
||||
NAMED TASK1: This is executed during the configuration phase
|
||||
See the report at: file:///tmp/app/build/reports/project/tasks.txt
|
||||
|
||||
> Task :list:dependencyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :list:htmlDependencyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :list:propertyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/properties.txt
|
||||
|
||||
> Task :list:taskReport
|
||||
See the report at: file:///tmp/list/build/reports/project/tasks.txt
|
||||
|
||||
> Task :utilities:dependencyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :utilities:htmlDependencyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :utilities:propertyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/properties.txt
|
||||
|
||||
> Task :utilities:taskReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/tasks.txt
|
||||
@@ -0,0 +1,47 @@
|
||||
> Task :dependencyReport
|
||||
See the report at: file:///tmp/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :htmlDependencyReport
|
||||
See the report at: file:///tmp/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :propertyReport
|
||||
See the report at: file:///tmp/build/reports/project/properties.txt
|
||||
|
||||
> Task :taskReport
|
||||
See the report at: file:///tmp/build/reports/project/tasks.txt
|
||||
|
||||
> Task :app:dependencyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :app:htmlDependencyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :app:propertyReport
|
||||
See the report at: file:///tmp/app/build/reports/project/properties.txt
|
||||
|
||||
> Task :app:taskReport
|
||||
See the report at: file:///tmp/app/build/reports/project/tasks.txt
|
||||
|
||||
> Task :list:dependencyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :list:htmlDependencyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :list:propertyReport
|
||||
See the report at: file:///tmp/list/build/reports/project/properties.txt
|
||||
|
||||
> Task :list:taskReport
|
||||
See the report at: file:///tmp/list/build/reports/project/tasks.txt
|
||||
|
||||
> Task :utilities:dependencyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/dependencies.txt
|
||||
|
||||
> Task :utilities:htmlDependencyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/dependencies/index.html
|
||||
|
||||
> Task :utilities:propertyReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/properties.txt
|
||||
|
||||
> Task :utilities:taskReport
|
||||
See the report at: file:///tmp/utilities/build/reports/project/tasks.txt
|
||||
@@ -10,14 +10,14 @@ import { join } from 'node:path';
|
||||
|
||||
export function execGradle(
|
||||
args: string[],
|
||||
execOptions: ExecFileSyncOptionsWithBufferEncoding
|
||||
execOptions?: ExecFileSyncOptionsWithBufferEncoding
|
||||
) {
|
||||
const gradleBinaryPath = getGradleBinaryPath();
|
||||
|
||||
return execFileSync(gradleBinaryPath, args, execOptions);
|
||||
}
|
||||
|
||||
export function getGradleBinaryPath() {
|
||||
export function getGradleBinaryPath(): string {
|
||||
const gradleFile = process.platform.startsWith('win')
|
||||
? 'gradlew.bat'
|
||||
: 'gradlew';
|
||||
@@ -31,12 +31,9 @@ export function getGradleBinaryPath() {
|
||||
|
||||
export function execGradleAsync(
|
||||
args: ReadonlyArray<string>,
|
||||
execOptions: ExecFileOptions
|
||||
execOptions?: ExecFileOptions
|
||||
) {
|
||||
const gradleBinaryPath = getGradleBinaryPath();
|
||||
if (!existsSync(gradleBinaryPath)) {
|
||||
throw new Error('Gradle is not setup. Run "gradle init"');
|
||||
}
|
||||
|
||||
return new Promise<Buffer>((res, rej) => {
|
||||
const cp = execFile(gradleBinaryPath, args, execOptions);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { processProjectReports, fileSeparator } from './get-gradle-report';
|
||||
|
||||
describe('processProjectReports', () => {
|
||||
it('should process project reports', () => {
|
||||
const projectReportLines = readFileSync(
|
||||
join(__dirname, '__mocks__/gradle-project-report.txt'),
|
||||
'utf-8'
|
||||
).split('\n');
|
||||
const report = processProjectReports(projectReportLines);
|
||||
expect(
|
||||
Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap))
|
||||
).toEqual(['', ':app', ':list', ':utilities']);
|
||||
});
|
||||
|
||||
it('should process project reports with println', () => {
|
||||
const projectReportLines = readFileSync(
|
||||
join(__dirname, '__mocks__/gradle-project-report-println.txt'),
|
||||
'utf-8'
|
||||
).split('\n');
|
||||
const report = processProjectReports(projectReportLines);
|
||||
expect(
|
||||
Object.keys(Object.fromEntries(report.gradleProjectToTasksTypeMap))
|
||||
).toEqual(['', ':app', ':list', ':utilities']);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
|
||||
import { normalizePath, workspaceRoot } from '@nx/devkit';
|
||||
|
||||
import { execGradle } from './exec-gradle';
|
||||
|
||||
const fileSeparator = process.platform.startsWith('win')
|
||||
export const fileSeparator = process.platform.startsWith('win')
|
||||
? 'file:///'
|
||||
: 'file://';
|
||||
|
||||
const newLineSeparator = process.platform.startsWith('win') ? '\r\n' : '\n';
|
||||
|
||||
interface GradleReport {
|
||||
export interface GradleReport {
|
||||
gradleFileToGradleProjectMap: Map<string, string>;
|
||||
buildFileToDepsMap: Map<string, string>;
|
||||
gradleFileToOutputDirsMap: Map<string, Map<string, string>>;
|
||||
@@ -48,7 +48,9 @@ export function getGradleReport(): GradleReport {
|
||||
return gradleReportCache;
|
||||
}
|
||||
|
||||
function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
export function processProjectReports(
|
||||
projectReportLines: string[]
|
||||
): GradleReport {
|
||||
/**
|
||||
* Map of Gradle File path to Gradle Project Name
|
||||
*/
|
||||
@@ -73,15 +75,22 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
*/
|
||||
const gradleFileToOutputDirsMap = new Map<string, Map<string, string>>();
|
||||
|
||||
projectReportLines.forEach((line, index) => {
|
||||
let index = 0;
|
||||
while (index < projectReportLines.length) {
|
||||
const line = projectReportLines[index].trim();
|
||||
if (line.startsWith('> Task ')) {
|
||||
const nextLine = projectReportLines[index + 1];
|
||||
if (line.endsWith(':dependencyReport')) {
|
||||
const gradleProject = line.substring(
|
||||
'> Task '.length,
|
||||
line.length - ':dependencyReport'.length
|
||||
);
|
||||
const [_, file] = nextLine.split(fileSeparator);
|
||||
while (
|
||||
index < projectReportLines.length &&
|
||||
!projectReportLines[index].includes(fileSeparator)
|
||||
) {
|
||||
index++;
|
||||
}
|
||||
const [_, file] = projectReportLines[index].split(fileSeparator);
|
||||
dependenciesMap.set(gradleProject, file);
|
||||
}
|
||||
if (line.endsWith('propertyReport')) {
|
||||
@@ -89,10 +98,16 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
'> Task '.length,
|
||||
line.length - ':propertyReport'.length
|
||||
);
|
||||
const [_, file] = nextLine.split(fileSeparator);
|
||||
const propertyReportLines = readFileSync(file)
|
||||
.toString()
|
||||
.split(newLineSeparator);
|
||||
while (
|
||||
index < projectReportLines.length &&
|
||||
!projectReportLines[index].includes(fileSeparator)
|
||||
) {
|
||||
index++;
|
||||
}
|
||||
const [_, file] = projectReportLines[index].split(fileSeparator);
|
||||
const propertyReportLines = existsSync(file)
|
||||
? readFileSync(file).toString().split(newLineSeparator)
|
||||
: [];
|
||||
|
||||
let projectName: string,
|
||||
absBuildFilePath: string,
|
||||
@@ -119,7 +134,7 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
}
|
||||
|
||||
if (!projectName || !absBuildFilePath || !absBuildDirPath) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
const buildFile = normalizePath(
|
||||
relative(workspaceRoot, absBuildFilePath)
|
||||
@@ -146,11 +161,17 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
'> Task '.length,
|
||||
line.length - ':taskReport'.length
|
||||
);
|
||||
const [_, file] = nextLine.split(fileSeparator);
|
||||
while (
|
||||
index < projectReportLines.length &&
|
||||
!projectReportLines[index].includes(fileSeparator)
|
||||
) {
|
||||
index++;
|
||||
}
|
||||
const [_, file] = projectReportLines[index].split(fileSeparator);
|
||||
const taskTypeMap = new Map<string, string>();
|
||||
const tasksFileLines = readFileSync(file)
|
||||
.toString()
|
||||
.split(newLineSeparator);
|
||||
const tasksFileLines = existsSync(file)
|
||||
? readFileSync(file).toString().split(newLineSeparator)
|
||||
: [];
|
||||
|
||||
let i = 0;
|
||||
while (i < tasksFileLines.length) {
|
||||
@@ -172,7 +193,8 @@ function processProjectReports(projectReportLines: string[]): GradleReport {
|
||||
gradleProjectToTasksTypeMap.set(gradleProject, taskTypeMap);
|
||||
}
|
||||
}
|
||||
});
|
||||
index++;
|
||||
}
|
||||
|
||||
return {
|
||||
gradleFileToGradleProjectMap,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { existsSync, readdirSync, readFileSync } from 'fs';
|
||||
import { readConfig } from 'jest-config';
|
||||
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { clearRequireCache } from '@nx/devkit/src/utils/config-utils';
|
||||
import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json-workspaces';
|
||||
import { combineGlobPatterns } from 'nx/src/utils/globs';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -115,12 +116,18 @@ async function buildJestTargets(
|
||||
options: JestPluginOptions,
|
||||
context: CreateNodesContext
|
||||
): Promise<Pick<ProjectConfiguration, 'targets' | 'metadata'>> {
|
||||
const absConfigFilePath = resolve(context.workspaceRoot, configFilePath);
|
||||
|
||||
if (require.cache[absConfigFilePath]) {
|
||||
clearRequireCache();
|
||||
}
|
||||
|
||||
const config = await readConfig(
|
||||
{
|
||||
_: [],
|
||||
$0: undefined,
|
||||
},
|
||||
resolve(context.workspaceRoot, configFilePath)
|
||||
absConfigFilePath
|
||||
);
|
||||
|
||||
const namedInputs = getNamedInputs(projectRoot, context);
|
||||
@@ -153,12 +160,12 @@ async function buildJestTargets(
|
||||
// nx-ignore-next-line
|
||||
})) as typeof import('jest-runtime');
|
||||
|
||||
const context = await Runtime.createContext(config.projectConfig, {
|
||||
const jestContext = await Runtime.createContext(config.projectConfig, {
|
||||
maxWorkers: 1,
|
||||
watchman: false,
|
||||
});
|
||||
|
||||
const source = new jest.SearchSource(context);
|
||||
const source = new jest.SearchSource(jestContext);
|
||||
|
||||
const specs = await source.getTestPaths(config.globalConfig);
|
||||
|
||||
@@ -188,7 +195,9 @@ async function buildJestTargets(
|
||||
targetGroup.push(options.ciTargetName);
|
||||
|
||||
for (const testPath of testPaths) {
|
||||
const relativePath = normalize(relative(projectRoot, testPath));
|
||||
const relativePath = normalize(
|
||||
relative(join(context.workspaceRoot, projectRoot), testPath)
|
||||
);
|
||||
const targetName = `${options.ciTargetName}--${relativePath}`;
|
||||
dependsOn.push(targetName);
|
||||
targets[targetName] = {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { existsSync, readdirSync } from 'fs';
|
||||
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { getLockFileName } from '@nx/js';
|
||||
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
|
||||
|
||||
export interface NextPluginOptions {
|
||||
buildTargetName?: string;
|
||||
@@ -194,19 +195,13 @@ async function getOutputs(projectRoot, nextConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextConfig(
|
||||
function getNextConfig(
|
||||
configFilePath: string,
|
||||
context: CreateNodesContext
|
||||
): Promise<any> {
|
||||
const resolvedPath = join(context.workspaceRoot, configFilePath);
|
||||
|
||||
let module;
|
||||
if (extname(configFilePath) === '.mjs') {
|
||||
module = await loadEsmModule(resolvedPath);
|
||||
} else {
|
||||
module = load(resolvedPath);
|
||||
}
|
||||
return module.default ?? module;
|
||||
return loadConfigFile(resolvedPath);
|
||||
}
|
||||
|
||||
function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
|
||||
@@ -230,36 +225,3 @@ function getInputs(
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
||||
/**
|
||||
* Load the module after ensuring that the require cache is cleared.
|
||||
*/
|
||||
function load(path: string): any {
|
||||
// Clear cache if the path is in the cache
|
||||
if (require.cache[path]) {
|
||||
for (const key of Object.keys(require.cache)) {
|
||||
if (!packageInstallationDirectories.some((dir) => key.includes(dir))) {
|
||||
delete require.cache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then require
|
||||
return require(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily compiled dynamic import loader function.
|
||||
*/
|
||||
let dynamicLoad: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
|
||||
|
||||
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
|
||||
const modulePathWithCacheBust = `${modulePath}?version=${Date.now()}`;
|
||||
dynamicLoad ??= new Function(
|
||||
'modulePath',
|
||||
`return import(modulePath);`
|
||||
) as Exclude<typeof dynamicLoad, undefined>;
|
||||
|
||||
return dynamicLoad(modulePathWithCacheBust);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||
import { Linter, lintProjectGenerator } from '@nx/eslint';
|
||||
@@ -57,9 +58,37 @@ export async function e2eProjectGeneratorInternal(
|
||||
jestConfig: `${options.e2eProjectRoot}/jest.config.ts`,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
dependsOn: [`${options.project}:build`],
|
||||
},
|
||||
},
|
||||
});
|
||||
// TODO(@nicholas): Find a better way to get build target
|
||||
|
||||
// We remove the 'test' target from the e2e project because it is not needed
|
||||
// The 'e2e' target is the one that should run the tests for the e2e project
|
||||
const nxJson = readNxJson(host);
|
||||
const hasPlugin = nxJson.plugins?.some((p) => {
|
||||
if (typeof p !== 'string' && p.plugin === '@nx/jest/plugin') {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasPlugin) {
|
||||
updateJson(host, 'nx.json', (json) => {
|
||||
return {
|
||||
...json,
|
||||
plugins: json.plugins?.map((p) => {
|
||||
if (typeof p !== 'string' && p.plugin === '@nx/jest/plugin') {
|
||||
return {
|
||||
...p,
|
||||
exclude: [...(p.exclude || []), `${options.e2eProjectRoot}/**/*`],
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (options.projectType === 'server') {
|
||||
generateFiles(
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
{
|
||||
"group": ["nx/*"],
|
||||
"message": "Circular import in 'nx' found. Use relative path."
|
||||
},
|
||||
{
|
||||
"group": ["**/native-bindings", "**/native-bindings.js"],
|
||||
"message": "Direct imports from native-bindings.js are not allowed. Import from index.js instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+8
-20
@@ -4,8 +4,7 @@ import {
|
||||
WorkspaceTypeAndRoot,
|
||||
} from '../src/utils/find-workspace-root';
|
||||
import * as chalk from 'chalk';
|
||||
import { config as loadDotEnvFile } from 'dotenv';
|
||||
import { expand } from 'dotenv-expand';
|
||||
import { loadRootEnvFiles } from '../src/utils/dotenv';
|
||||
import { initLocal } from './init-local';
|
||||
import { output } from '../src/utils/output';
|
||||
import {
|
||||
@@ -26,15 +25,20 @@ function main() {
|
||||
if (
|
||||
process.argv[2] !== 'report' &&
|
||||
process.argv[2] !== '--version' &&
|
||||
process.argv[2] !== '--help'
|
||||
process.argv[2] !== '--help' &&
|
||||
process.argv[2] !== 'reset'
|
||||
) {
|
||||
assertSupportedPlatform();
|
||||
}
|
||||
|
||||
require('nx/src/utils/perf-logging');
|
||||
|
||||
const workspace = findWorkspaceRoot(process.cwd());
|
||||
|
||||
performance.mark('loading dotenv files:start');
|
||||
loadDotEnvFiles();
|
||||
if (workspace) {
|
||||
loadRootEnvFiles(workspace.dir);
|
||||
}
|
||||
performance.mark('loading dotenv files:end');
|
||||
performance.measure(
|
||||
'loading dotenv files',
|
||||
@@ -42,7 +46,6 @@ function main() {
|
||||
'loading dotenv files:end'
|
||||
);
|
||||
|
||||
const workspace = findWorkspaceRoot(process.cwd());
|
||||
// new is a special case because there is no local workspace to load
|
||||
if (
|
||||
process.argv[2] === 'new' ||
|
||||
@@ -105,21 +108,6 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This loads dotenv files from:
|
||||
* - .env
|
||||
* - .local.env
|
||||
* - .env.local
|
||||
*/
|
||||
function loadDotEnvFiles() {
|
||||
for (const file of ['.local.env', '.env.local', '.env']) {
|
||||
const myEnv = loadDotEnvFile({
|
||||
path: file,
|
||||
});
|
||||
expand(myEnv);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNoWorkspace(globalNxVersion?: string) {
|
||||
output.log({
|
||||
title: `The current directory isn't part of an Nx workspace.`,
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
"cli": "nx",
|
||||
"version": "16.8.0-beta.3",
|
||||
"description": "Escape $ in env variables",
|
||||
"implementation": "./src/migrations/update-16-8-0/escape-dollar-sign-env-variables"
|
||||
"implementation": "./src/migrations/update-16-8-0/escape-dollar-sign-env-variables",
|
||||
"x-repair-skip": true
|
||||
},
|
||||
"17.0.0-move-cache-directory": {
|
||||
"cli": "nx",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"executor": "@monodon/rust:napi",
|
||||
"options": {
|
||||
"dist": "packages/nx/src/native",
|
||||
"jsFile": "packages/nx/src/native/index.js",
|
||||
"jsFile": "packages/nx/src/native/native-bindings.js",
|
||||
"release": true
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@@ -119,6 +119,10 @@ async function initializePlugin(
|
||||
await runNxAsync(
|
||||
`g ${pkgName}:${initGenerator} --keepExistingVersions${
|
||||
updatePackageScripts ? ' --updatePackageScripts' : ''
|
||||
}${
|
||||
options.__overrides_unparsed__.length
|
||||
? ' ' + options.__overrides_unparsed__.join(' ')
|
||||
: ''
|
||||
}`,
|
||||
{
|
||||
silent: !options.verbose,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { CommandModule } from 'yargs';
|
||||
import { withOverrides } from '../yargs-utils/shared-options';
|
||||
|
||||
export interface AddOptions {
|
||||
packageSpecifier: string;
|
||||
updatePackageScripts?: boolean;
|
||||
verbose?: boolean;
|
||||
__overrides_unparsed__: string[];
|
||||
}
|
||||
|
||||
export const yargsAddCommand: CommandModule<
|
||||
@@ -14,6 +16,10 @@ export const yargsAddCommand: CommandModule<
|
||||
describe: 'Install a plugin and initialize it.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.parserConfiguration({
|
||||
'strip-dashed': true,
|
||||
'unknown-options-as-args': true,
|
||||
})
|
||||
.positional('packageSpecifier', {
|
||||
type: 'string',
|
||||
description:
|
||||
@@ -41,5 +47,6 @@ export const yargsAddCommand: CommandModule<
|
||||
'$0 add @nx/react@17.0.0',
|
||||
'Install version `17.0.0` of the `@nx/react` package and run its `@nx/react:init` generator'
|
||||
) as any,
|
||||
handler: (args) => import('./add').then((m) => m.addHandler(args)),
|
||||
handler: (args) =>
|
||||
import('./add').then((m) => m.addHandler(withOverrides(args))),
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { linkToNxDevAndExamples } from '../yargs-utils/documentation';
|
||||
import {
|
||||
withAffectedOptions,
|
||||
withDepGraphOptions,
|
||||
withVerbose,
|
||||
} from '../yargs-utils/shared-options';
|
||||
|
||||
export const yargsDepGraphCommand: CommandModule = {
|
||||
@@ -11,7 +12,7 @@ export const yargsDepGraphCommand: CommandModule = {
|
||||
aliases: ['dep-graph'],
|
||||
builder: (yargs) =>
|
||||
linkToNxDevAndExamples(
|
||||
withAffectedOptions(withDepGraphOptions(yargs)),
|
||||
withVerbose(withAffectedOptions(withDepGraphOptions(yargs))),
|
||||
'dep-graph'
|
||||
)
|
||||
.option('affected', {
|
||||
|
||||
+8
-3
@@ -1,12 +1,17 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export function checkForUncommittedChanges() {
|
||||
const gitResult = execSync(`git status --porcelain`);
|
||||
if (gitResult.length > 0) {
|
||||
const gitResult = execSync('git status --porcelain').toString();
|
||||
|
||||
const filteredResults = gitResult
|
||||
.split('\n')
|
||||
.filter((line) => !line.includes('.nx') && line.trim().length > 0);
|
||||
|
||||
if (filteredResults.length > 0) {
|
||||
console.log('❗️ Careful!');
|
||||
console.log('You have uncommitted changes in your repository.');
|
||||
console.log('');
|
||||
console.log(gitResult.toString());
|
||||
console.log(filteredResults.join('\n').toString());
|
||||
console.log('Please commit your changes before running the migrator!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { ProjectGraphProjectNode } from '../../config/project-graph';
|
||||
import { CommandModule, showHelp } from 'yargs';
|
||||
import { parseCSV, withAffectedOptions } from '../yargs-utils/shared-options';
|
||||
import {
|
||||
parseCSV,
|
||||
withAffectedOptions,
|
||||
withVerbose,
|
||||
} from '../yargs-utils/shared-options';
|
||||
import { handleErrors } from '../../utils/params';
|
||||
|
||||
export interface NxShowArgs {
|
||||
@@ -64,7 +68,7 @@ const showProjectsCommand: CommandModule<NxShowArgs, ShowProjectsOptions> = {
|
||||
command: 'projects',
|
||||
describe: 'Show a list of projects in the workspace',
|
||||
builder: (yargs) =>
|
||||
withAffectedOptions(yargs)
|
||||
withVerbose(withAffectedOptions(yargs))
|
||||
.option('affected', {
|
||||
type: 'boolean',
|
||||
description: 'Show only affected projects',
|
||||
@@ -86,11 +90,6 @@ const showProjectsCommand: CommandModule<NxShowArgs, ShowProjectsOptions> = {
|
||||
description: 'Select only projects of the given type',
|
||||
choices: ['app', 'lib', 'e2e'],
|
||||
})
|
||||
.option('verbose', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Prints additional information about the commands (e.g., stack traces)',
|
||||
})
|
||||
.implies('untracked', 'affected')
|
||||
.implies('uncommitted', 'affected')
|
||||
.implies('files', 'affected')
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface RunOptions {
|
||||
}
|
||||
|
||||
export function withRunOptions<T>(yargs: Argv<T>): Argv<T & RunOptions> {
|
||||
return withExcludeOption(yargs)
|
||||
return withVerbose(withExcludeOption(yargs))
|
||||
.option('parallel', {
|
||||
describe: 'Max number of parallel processes [default is 3]',
|
||||
type: 'string',
|
||||
@@ -63,11 +63,6 @@ export function withRunOptions<T>(yargs: Argv<T>): Argv<T & RunOptions> {
|
||||
? false
|
||||
: value,
|
||||
})
|
||||
.option('verbose', {
|
||||
type: 'boolean',
|
||||
describe:
|
||||
'Prints additional information about the commands (e.g., stack traces)',
|
||||
})
|
||||
.option('nxBail', {
|
||||
describe: 'Stop command execution after the first failed task',
|
||||
type: 'boolean',
|
||||
@@ -123,6 +118,20 @@ export function withConfiguration(yargs: Argv) {
|
||||
});
|
||||
}
|
||||
|
||||
export function withVerbose(yargs: Argv) {
|
||||
return yargs
|
||||
.option('verbose', {
|
||||
describe:
|
||||
'Prints additional information about the commands (e.g., stack traces)',
|
||||
type: 'boolean',
|
||||
})
|
||||
.middleware((args) => {
|
||||
if (args.verbose) {
|
||||
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function withBatch(yargs: Argv) {
|
||||
return yargs.options('batch', {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('Workspaces', () => {
|
||||
}
|
||||
);
|
||||
console.log(projects);
|
||||
expect(projects['my-package']).toEqual({
|
||||
expect(projects['packages/my-package']).toEqual({
|
||||
name: 'my-package',
|
||||
root: 'packages/my-package',
|
||||
sourceRoot: 'packages/my-package',
|
||||
|
||||
@@ -43,6 +43,25 @@ export async function safelyCleanUpExistingProcess(): Promise<void> {
|
||||
if (daemonProcessJson && daemonProcessJson.processId) {
|
||||
try {
|
||||
process.kill(daemonProcessJson.processId);
|
||||
// we wait for the process to actually shut down before returning
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
try {
|
||||
// sending a signal 0 to a process checks if the process is running instead of actually killing it
|
||||
process.kill(daemonProcessJson.processId, 0);
|
||||
} catch (e) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
if ((count += 1) > 200) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
`Daemon process ${daemonProcessJson.processId} didn't exit after 2 seconds.`
|
||||
);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
deleteDaemonJsonProcessCache();
|
||||
|
||||
@@ -25,8 +25,10 @@ import { safelyCleanUpExistingProcess } from '../cache';
|
||||
import { Hash } from '../../hasher/task-hasher';
|
||||
import { Task, TaskGraph } from '../../config/task-graph';
|
||||
import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils';
|
||||
import { DaemonProjectGraphError } from '../daemon-project-graph-error';
|
||||
import { ProjectGraphError } from '../../project-graph/project-graph';
|
||||
import {
|
||||
DaemonProjectGraphError,
|
||||
ProjectGraphError,
|
||||
} from '../../project-graph/error-types';
|
||||
|
||||
const DAEMON_ENV_SETTINGS = {
|
||||
NX_PROJECT_GLOB_CACHE: 'false',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ProjectGraph } from '../config/project-graph';
|
||||
import { ConfigurationSourceMaps } from '../project-graph/utils/project-configuration-utils';
|
||||
|
||||
export class DaemonProjectGraphError extends Error {
|
||||
constructor(
|
||||
public errors: any[],
|
||||
readonly projectGraph: ProjectGraph,
|
||||
readonly sourceMaps: ConfigurationSourceMaps
|
||||
) {
|
||||
super(
|
||||
`The Daemon Process threw an error while calculating the project graph. Convert this error to a ProjectGraphError to get more information.`
|
||||
);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Task, TaskGraph } from '../../config/task-graph';
|
||||
import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation';
|
||||
import { InProcessTaskHasher } from '../../hasher/task-hasher';
|
||||
import { readNxJson } from '../../config/configuration';
|
||||
import { DaemonProjectGraphError } from '../daemon-project-graph-error';
|
||||
import { DaemonProjectGraphError } from '../../project-graph/error-types';
|
||||
|
||||
/**
|
||||
* We use this not to recreated hasher for every hash operation
|
||||
|
||||
@@ -33,10 +33,12 @@ import { notifyFileWatcherSockets } from './file-watching/file-watcher-sockets';
|
||||
import { serverLogger } from './logger';
|
||||
import { NxWorkspaceFilesExternals } from '../../native';
|
||||
import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils';
|
||||
import { DaemonProjectGraphError } from '../daemon-project-graph-error';
|
||||
import { LoadedNxPlugin } from '../../project-graph/plugins/internal-api';
|
||||
import { getPlugins } from './plugins';
|
||||
import { ProjectConfigurationsError } from '../../project-graph/error-types';
|
||||
import {
|
||||
DaemonProjectGraphError,
|
||||
ProjectConfigurationsError,
|
||||
} from '../../project-graph/error-types';
|
||||
|
||||
interface SerializedProjectGraph {
|
||||
error: Error | null;
|
||||
@@ -232,29 +234,29 @@ async function processFilesAndCreateAndSerializeProjectGraph(
|
||||
const nxJson = readNxJson(workspaceRoot);
|
||||
global.NX_GRAPH_CREATION = true;
|
||||
|
||||
let graphNodes: ConfigurationResult;
|
||||
let projectConfigurationsResult: ConfigurationResult;
|
||||
let projectConfigurationsError;
|
||||
|
||||
try {
|
||||
graphNodes = await retrieveProjectConfigurations(
|
||||
projectConfigurationsResult = await retrieveProjectConfigurations(
|
||||
plugins,
|
||||
workspaceRoot,
|
||||
nxJson
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ProjectConfigurationsError) {
|
||||
graphNodes = e.partialProjectConfigurationsResult;
|
||||
projectConfigurationsResult = e.partialProjectConfigurationsResult;
|
||||
projectConfigurationsError = e;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await processCollectedUpdatedAndDeletedFiles(
|
||||
graphNodes,
|
||||
projectConfigurationsResult,
|
||||
updatedFileHashes,
|
||||
deletedFiles
|
||||
);
|
||||
const g = await createAndSerializeProjectGraph(graphNodes);
|
||||
const g = await createAndSerializeProjectGraph(projectConfigurationsResult);
|
||||
|
||||
delete global.NX_GRAPH_CREATION;
|
||||
|
||||
@@ -282,7 +284,7 @@ async function processFilesAndCreateAndSerializeProjectGraph(
|
||||
error: new DaemonProjectGraphError(
|
||||
errors,
|
||||
g.projectGraph,
|
||||
graphNodes.sourceMaps
|
||||
projectConfigurationsResult.sourceMaps
|
||||
),
|
||||
projectGraph: null,
|
||||
projectFileMapCache: null,
|
||||
|
||||
@@ -112,10 +112,11 @@ async function handleMessage(socket, data: string) {
|
||||
}
|
||||
|
||||
if (daemonIsOutdated()) {
|
||||
await respondWithErrorAndExit(socket, `Lock files changed`, {
|
||||
name: '',
|
||||
message: 'LOCK-FILES-CHANGED',
|
||||
});
|
||||
await respondWithErrorAndExit(
|
||||
socket,
|
||||
`Lock files changed`,
|
||||
new Error('LOCK-FILES-CHANGED')
|
||||
);
|
||||
}
|
||||
|
||||
resetInactivityTimeout(handleInactivityTimeout);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlinkSync } from 'fs';
|
||||
import { platform } from 'os';
|
||||
import { join, resolve } from 'path';
|
||||
import { DAEMON_SOCKET_PATH, socketDir } from './tmp-dir';
|
||||
import { DaemonProjectGraphError } from './daemon-project-graph-error';
|
||||
import { createSerializableError } from '../utils/serializable-error';
|
||||
|
||||
export const isWindows = platform() === 'win32';
|
||||
|
||||
@@ -27,21 +27,6 @@ export function killSocketOrPath(): void {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Include the original stack trace within the serialized error so that the client can show it to the user.
|
||||
function serializeError(error: Error | null): string | null {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error instanceof DaemonProjectGraphError) {
|
||||
error.errors = error.errors.map((e) => JSON.parse(serializeError(e)));
|
||||
}
|
||||
|
||||
return `{${Object.getOwnPropertyNames(error)
|
||||
.map((k) => `"${k}": ${JSON.stringify(error[k])}`)
|
||||
.join(',')}}`;
|
||||
}
|
||||
|
||||
// Prepare a serialized project graph result for sending over IPC from the server to the client
|
||||
export function serializeResult(
|
||||
error: Error | null,
|
||||
@@ -49,7 +34,7 @@ export function serializeResult(
|
||||
serializedSourceMaps: string | null
|
||||
): string | null {
|
||||
// We do not want to repeat work `JSON.stringify`ing an object containing the potentially large project graph so merge as strings
|
||||
return `{ "error": ${serializeError(
|
||||
error
|
||||
return `{ "error": ${JSON.stringify(
|
||||
error ? createSerializableError(error) : error
|
||||
)}, "projectGraph": ${serializedProjectGraph}, "sourceMaps": ${serializedSourceMaps} }`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export { getExecutorInformation } from './command-line/run/executor-utils';
|
||||
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
|
||||
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
|
||||
export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files';
|
||||
export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils';
|
||||
export { splitTarget } from './utils/split-target';
|
||||
export { combineOptionsForExecutor } from './utils/params';
|
||||
export { sortObjectByKeys } from './utils/object-sort';
|
||||
|
||||
@@ -37,19 +37,61 @@ describe('Run Commands', () => {
|
||||
});
|
||||
|
||||
it('should not pass --args into underlying command', async () => {
|
||||
const f = fileSync().name;
|
||||
const result = await runCommands(
|
||||
{
|
||||
command: `echo`,
|
||||
__unparsed__: ['--args=--key=123'],
|
||||
args: '--key=123',
|
||||
unparsedCommandArgs: { args: '--key=123' },
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(result.terminalOutput.trim()).not.toContain('--args=--key=123');
|
||||
});
|
||||
|
||||
it('should not foward any args to underlying command if forwardAllArgs is false', async () => {
|
||||
let result = await runCommands(
|
||||
{
|
||||
command: `echo`,
|
||||
key: 123,
|
||||
__unparsed__: [],
|
||||
forwardAllArgs: false,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(result.terminalOutput.trim()).not.toContain('--key=123');
|
||||
|
||||
result = await runCommands(
|
||||
{
|
||||
command: `echo`,
|
||||
key: 123,
|
||||
__unparsed__: [],
|
||||
forwardAllArgs: true,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(result.terminalOutput.trim()).toContain('--key=123');
|
||||
|
||||
result = await runCommands(
|
||||
{
|
||||
commands: [
|
||||
{
|
||||
command: `echo 1`,
|
||||
forwardAllArgs: true,
|
||||
},
|
||||
{
|
||||
command: `echo 2`,
|
||||
},
|
||||
],
|
||||
__unparsed__: ['--args=--key=123'],
|
||||
args: '--key=123',
|
||||
forwardAllArgs: false,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(result.terminalOutput).toContain('1 --key=123');
|
||||
expect(result.terminalOutput).not.toContain('2 --key=123');
|
||||
});
|
||||
|
||||
it('should interpolate all unknown args as if they were --args', async () => {
|
||||
const f = fileSync().name;
|
||||
const result = await runCommands(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { exec } from 'child_process';
|
||||
import { ChildProcess, exec, Serializable } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as yargsParser from 'yargs-parser';
|
||||
import { env as appendLocalEnv } from 'npm-run-path';
|
||||
@@ -7,10 +7,13 @@ import * as chalk from 'chalk';
|
||||
import {
|
||||
getPseudoTerminal,
|
||||
PseudoTerminal,
|
||||
PseudoTtyProcess,
|
||||
} from '../../tasks-runner/pseudo-terminal';
|
||||
import { output } from '../../utils/output';
|
||||
import { signalToCode } from '../../utils/exit-codes';
|
||||
|
||||
export const LARGE_BUFFER = 1024 * 1000000;
|
||||
let pseudoTerminal: PseudoTerminal | null;
|
||||
const childProcesses = new Set<ChildProcess | PseudoTtyProcess>();
|
||||
|
||||
async function loadEnvVars(path?: string) {
|
||||
if (path) {
|
||||
@@ -51,6 +54,7 @@ export interface RunCommandsOptions extends Json {
|
||||
readyWhen?: string;
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
forwardAllArgs?: boolean; // default is true
|
||||
args?: string | string[];
|
||||
envFile?: string;
|
||||
__unparsed__: string[];
|
||||
@@ -72,6 +76,7 @@ const propKeys = [
|
||||
'usePty',
|
||||
'streamOutput',
|
||||
'verbose',
|
||||
'forwardAllArgs',
|
||||
];
|
||||
|
||||
export interface NormalizedRunCommandsOptions extends RunCommandsOptions {
|
||||
@@ -98,6 +103,7 @@ export default async function (
|
||||
success: boolean;
|
||||
terminalOutput: string;
|
||||
}> {
|
||||
registerProcessListener();
|
||||
await loadEnvVars(options.envFile);
|
||||
const normalized = normalizeOptions(options);
|
||||
|
||||
@@ -240,7 +246,7 @@ function normalizeOptions(
|
||||
c.command = interpolateArgsIntoCommand(
|
||||
c.command,
|
||||
options as NormalizedRunCommandsOptions,
|
||||
c.forwardAllArgs ?? true
|
||||
c.forwardAllArgs ?? options.forwardAllArgs ?? true
|
||||
);
|
||||
});
|
||||
return options as NormalizedRunCommandsOptions;
|
||||
@@ -250,9 +256,7 @@ async function runSerially(
|
||||
options: NormalizedRunCommandsOptions,
|
||||
context: ExecutorContext
|
||||
): Promise<{ success: boolean; terminalOutput: string }> {
|
||||
const pseudoTerminal = PseudoTerminal.isSupported()
|
||||
? getPseudoTerminal()
|
||||
: null;
|
||||
pseudoTerminal ??= PseudoTerminal.isSupported() ? getPseudoTerminal() : null;
|
||||
let terminalOutput = '';
|
||||
for (const c of options.commands) {
|
||||
const result: { success: boolean; terminalOutput: string } =
|
||||
@@ -317,6 +321,8 @@ async function createProcess(
|
||||
quiet: !streamOutput,
|
||||
});
|
||||
|
||||
childProcesses.add(cp);
|
||||
|
||||
return new Promise((res) => {
|
||||
cp.onOutput((output) => {
|
||||
terminalOutput += output;
|
||||
@@ -360,16 +366,8 @@ function nodeProcess(
|
||||
env,
|
||||
cwd,
|
||||
});
|
||||
/**
|
||||
* Ensure the child process is killed when the parent exits
|
||||
*/
|
||||
const processExitListener = (signal?: number | NodeJS.Signals) =>
|
||||
childProcess.kill(signal);
|
||||
|
||||
process.on('exit', processExitListener);
|
||||
process.on('SIGTERM', processExitListener);
|
||||
process.on('SIGINT', processExitListener);
|
||||
process.on('SIGQUIT', processExitListener);
|
||||
childProcesses.add(childProcess);
|
||||
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
const output = addColorAndPrefix(data, commandConfig);
|
||||
@@ -400,6 +398,7 @@ function nodeProcess(
|
||||
res({ success: false, terminalOutput });
|
||||
});
|
||||
childProcess.on('exit', (code) => {
|
||||
childProcesses.delete(childProcess);
|
||||
if (!readyWhen) {
|
||||
res({ success: code === 0, terminalOutput });
|
||||
}
|
||||
@@ -561,3 +560,62 @@ function filterPropKeysFromUnParsedOptions(
|
||||
}
|
||||
return parsedOptions;
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
function registerProcessListener() {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
registered = true;
|
||||
// When the nx process gets a message, it will be sent into the task's process
|
||||
process.on('message', (message: Serializable) => {
|
||||
// this.publisher.publish(message.toString());
|
||||
if (pseudoTerminal) {
|
||||
pseudoTerminal.sendMessageToChildren(message);
|
||||
}
|
||||
|
||||
childProcesses.forEach((p) => {
|
||||
if ('connected' in p && p.connected) {
|
||||
p.send(message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Terminate any task processes on exit
|
||||
process.on('exit', () => {
|
||||
childProcesses.forEach((p) => {
|
||||
if ('connected' in p ? p.connected : p.isAlive) {
|
||||
p.kill();
|
||||
}
|
||||
});
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
childProcesses.forEach((p) => {
|
||||
if ('connected' in p ? p.connected : p.isAlive) {
|
||||
p.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
// we exit here because we don't need to write anything to cache.
|
||||
process.exit(signalToCode('SIGINT'));
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
childProcesses.forEach((p) => {
|
||||
if ('connected' in p ? p.connected : p.isAlive) {
|
||||
p.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
// no exit here because we expect child processes to terminate which
|
||||
// will store results to the cache and will terminate this process
|
||||
});
|
||||
process.on('SIGHUP', () => {
|
||||
childProcesses.forEach((p) => {
|
||||
if ('connected' in p ? p.connected : p.isAlive) {
|
||||
p.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
// no exit here because we expect child processes to terminate which
|
||||
// will store results to the cache and will terminate this process
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,6 +135,11 @@
|
||||
"$source": "unparsed"
|
||||
},
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"forwardAllArgs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether arguments should be forwarded when interpolation is not present.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Observable } from 'rxjs';
|
||||
import { readNxJson } from '../../config/nx-json';
|
||||
import { Executor, ExecutorContext } from '../../config/misc-interfaces';
|
||||
import { retrieveProjectConfigurations } from '../../project-graph/utils/retrieve-workspace-files';
|
||||
import { readProjectConfigurationsFromRootMap } from '../../project-graph/utils/project-configuration-utils';
|
||||
import { ProjectsConfigurations } from '../../config/workspace-json-project-json';
|
||||
import { loadNxPlugins } from '../../project-graph/plugins/internal-api';
|
||||
|
||||
@@ -25,13 +26,15 @@ export function convertNxExecutor(executor: Executor) {
|
||||
);
|
||||
const projectsConfigurations: ProjectsConfigurations = {
|
||||
version: 2,
|
||||
projects: (
|
||||
await retrieveProjectConfigurations(
|
||||
plugins,
|
||||
builderContext.workspaceRoot,
|
||||
nxJsonConfiguration
|
||||
)
|
||||
).projects,
|
||||
projects: readProjectConfigurationsFromRootMap(
|
||||
(
|
||||
await retrieveProjectConfigurations(
|
||||
plugins,
|
||||
builderContext.workspaceRoot,
|
||||
nxJsonConfiguration
|
||||
)
|
||||
).projects
|
||||
),
|
||||
};
|
||||
cleanup();
|
||||
const context: ExecutorContext = {
|
||||
|
||||
@@ -211,7 +211,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
|
||||
(r) => deletedFiles.indexOf(r) === -1
|
||||
);
|
||||
|
||||
const rootMap: Map<string, ProjectConfiguration> = new Map();
|
||||
const rootMap: Record<string, ProjectConfiguration> = {};
|
||||
for (const projectFile of projectFiles) {
|
||||
if (basename(projectFile) === 'project.json') {
|
||||
const json = readJson(tree, projectFile);
|
||||
@@ -230,7 +230,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
|
||||
projectFile,
|
||||
readNxJson(tree)
|
||||
);
|
||||
if (!rootMap.has(config.root)) {
|
||||
if (!rootMap[config.root]) {
|
||||
mergeProjectConfigurationIntoRootMap(
|
||||
rootMap,
|
||||
// Inferred targets, tags, etc don't show up when running generators
|
||||
|
||||
@@ -26,8 +26,10 @@ pub fn hash_file_path<P: AsRef<Path>>(path: P) -> Option<String> {
|
||||
trace!("Failed to read file: {:?}", path);
|
||||
return None;
|
||||
};
|
||||
let hash = hash(&content);
|
||||
trace!("Hashed file {:?} - {:?}", path, hash);
|
||||
|
||||
Some(hash(&content))
|
||||
Some(hash)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Vendored
+1
-4
@@ -29,11 +29,8 @@ export function findImports(projectFileMap: Record<string, Array<string>>): Arra
|
||||
* This wont be needed once the project graph is created in Rust
|
||||
*/
|
||||
export function transferProjectGraph(projectGraph: ProjectGraph): ExternalObject<ProjectGraph>
|
||||
export interface ExternalNodeData {
|
||||
version: string
|
||||
hash?: string
|
||||
}
|
||||
export interface ExternalNode {
|
||||
packageName?: string
|
||||
version: string
|
||||
hash?: string
|
||||
}
|
||||
|
||||
+68
-260
@@ -1,268 +1,76 @@
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
const { join, basename } = require('path');
|
||||
const { copyFileSync, existsSync, mkdirSync } = require('fs');
|
||||
const Module = require('module');
|
||||
const { nxVersion } = require('../utils/versions');
|
||||
const { cacheDir } = require('../utils/cache-directory');
|
||||
|
||||
const { platform, arch } = process
|
||||
const nxPackages = new Set([
|
||||
'@nx/nx-android-arm64',
|
||||
'@nx/nx-android-arm-eabi',
|
||||
'@nx/nx-win32-x64-msvc',
|
||||
'@nx/nx-win32-ia32-msvc',
|
||||
'@nx/nx-win32-arm64-msvc',
|
||||
'@nx/nx-darwin-universal',
|
||||
'@nx/nx-darwin-x64',
|
||||
'@nx/nx-darwin-arm64',
|
||||
'@nx/nx-freebsd-x64',
|
||||
'@nx/nx-linux-x64-musl',
|
||||
'@nx/nx-linux-x64-gnu',
|
||||
'@nx/nx-linux-arm64-musl',
|
||||
'@nx/nx-linux-arm64-gnu',
|
||||
'@nx/nx-linux-arm-gnueabihf',
|
||||
]);
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
const localNodeFiles = [
|
||||
'nx.android-arm64.node',
|
||||
'nx.android-arm-eabi.node',
|
||||
'nx.win32-x64-msvc.node',
|
||||
'nx.win32-ia32-msvc.node',
|
||||
'nx.win32-arm64-msvc.node',
|
||||
'nx.darwin-universal.node',
|
||||
'nx.darwin-x64.node',
|
||||
'nx.darwin-arm64.node',
|
||||
'nx.freebsd-x64.node',
|
||||
'nx.linux-x64-musl.node',
|
||||
'nx.linux-x64-gnu.node',
|
||||
'nx.linux-arm64-musl.node',
|
||||
'nx.linux-arm64-gnu.node',
|
||||
'nx.linux-arm-gnueabihf.node',
|
||||
];
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
const lddPath = require('child_process').execSync('which ldd').toString().trim();
|
||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
const originalLoad = Module._load;
|
||||
|
||||
// We override the _load function so that when a native file is required,
|
||||
// we copy it to a cache directory and require it from there.
|
||||
// This prevents the file being loaded from node_modules and causing file locking issues.
|
||||
// Will only be called once because the require cache takes over afterwards.
|
||||
Module._load = function (request, parent, isMain) {
|
||||
const modulePath = request;
|
||||
if (
|
||||
nxPackages.has(modulePath) ||
|
||||
localNodeFiles.some((f) => modulePath.endsWith(f))
|
||||
) {
|
||||
const nativeLocation = require.resolve(modulePath);
|
||||
const fileName = basename(nativeLocation);
|
||||
// we copy the file to the cache directory (.nx/cache by default) and prefix with nxVersion to avoid stale files being loaded
|
||||
const tmpFile = join(cacheDir, nxVersion + '-' + fileName);
|
||||
if (existsSync(tmpFile)) {
|
||||
return originalLoad.apply(this, [tmpFile, parent, isMain]);
|
||||
}
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
copyFileSync(nativeLocation, tmpFile);
|
||||
return originalLoad.apply(this, [tmpFile, parent, isMain]);
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
// call the original _load function for everything else
|
||||
return originalLoad.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.darwin-universal.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-universal.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-universal')
|
||||
}
|
||||
break
|
||||
} catch {}
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-x64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-x64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-x64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-x64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-x64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-x64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
}
|
||||
const indexModulePath = require.resolve('./native-bindings.js');
|
||||
delete require.cache[indexModulePath];
|
||||
const indexModule = require('./native-bindings.js');
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { expandOutputs, getFilesForOutputs, remove, copy, hashArray, hashFile, ImportResult, findImports, transferProjectGraph, ChildProcess, RustPseudoTerminal, HashPlanner, TaskHasher, EventType, Watcher, WorkspaceContext, WorkspaceErrors, testOnlyTransferFileMap } = nativeBinding
|
||||
|
||||
module.exports.expandOutputs = expandOutputs
|
||||
module.exports.getFilesForOutputs = getFilesForOutputs
|
||||
module.exports.remove = remove
|
||||
module.exports.copy = copy
|
||||
module.exports.hashArray = hashArray
|
||||
module.exports.hashFile = hashFile
|
||||
module.exports.ImportResult = ImportResult
|
||||
module.exports.findImports = findImports
|
||||
module.exports.transferProjectGraph = transferProjectGraph
|
||||
module.exports.ChildProcess = ChildProcess
|
||||
module.exports.RustPseudoTerminal = RustPseudoTerminal
|
||||
module.exports.HashPlanner = HashPlanner
|
||||
module.exports.TaskHasher = TaskHasher
|
||||
module.exports.EventType = EventType
|
||||
module.exports.Watcher = Watcher
|
||||
module.exports.WorkspaceContext = WorkspaceContext
|
||||
module.exports.WorkspaceErrors = WorkspaceErrors
|
||||
module.exports.testOnlyTransferFileMap = testOnlyTransferFileMap
|
||||
module.exports = indexModule;
|
||||
Module._load = originalLoad;
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
|
||||
const { platform, arch } = process
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
const lddPath = require('child_process').execSync('which ldd').toString().trim();
|
||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
}
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.darwin-universal.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-universal.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-universal')
|
||||
}
|
||||
break
|
||||
} catch {}
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'nx.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-x64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-x64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-x64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-x64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-x64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-x64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'nx.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./nx.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@nx/nx-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { expandOutputs, getFilesForOutputs, remove, copy, hashArray, hashFile, ImportResult, findImports, transferProjectGraph, ChildProcess, RustPseudoTerminal, HashPlanner, TaskHasher, EventType, Watcher, WorkspaceContext, WorkspaceErrors, testOnlyTransferFileMap } = nativeBinding
|
||||
|
||||
module.exports.expandOutputs = expandOutputs
|
||||
module.exports.getFilesForOutputs = getFilesForOutputs
|
||||
module.exports.remove = remove
|
||||
module.exports.copy = copy
|
||||
module.exports.hashArray = hashArray
|
||||
module.exports.hashFile = hashFile
|
||||
module.exports.ImportResult = ImportResult
|
||||
module.exports.findImports = findImports
|
||||
module.exports.transferProjectGraph = transferProjectGraph
|
||||
module.exports.ChildProcess = ChildProcess
|
||||
module.exports.RustPseudoTerminal = RustPseudoTerminal
|
||||
module.exports.HashPlanner = HashPlanner
|
||||
module.exports.TaskHasher = TaskHasher
|
||||
module.exports.EventType = EventType
|
||||
module.exports.Watcher = Watcher
|
||||
module.exports.WorkspaceContext = WorkspaceContext
|
||||
module.exports.WorkspaceErrors = WorkspaceErrors
|
||||
module.exports.testOnlyTransferFileMap = testOnlyTransferFileMap
|
||||
@@ -1,14 +1,9 @@
|
||||
use crate::native::types::JsInputs;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ExternalNodeData {
|
||||
pub version: String,
|
||||
pub hash: Option<String>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ExternalNode {
|
||||
pub package_name: Option<String>,
|
||||
pub version: String,
|
||||
pub hash: Option<String>,
|
||||
}
|
||||
|
||||
@@ -110,20 +110,13 @@ impl HashPlanner {
|
||||
project_name: &str,
|
||||
target_name: &str,
|
||||
self_inputs: &[Input],
|
||||
external_deps_map: &hashbrown::HashMap<&str, Vec<&'a str>>,
|
||||
external_deps_map: &hashbrown::HashMap<&String, Vec<&'a String>>,
|
||||
) -> anyhow::Result<Option<Vec<HashInstruction>>> {
|
||||
let project = &self.project_graph.nodes[project_name];
|
||||
let Some(target) = project.targets.get(target_name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let external_nodes_keys: Vec<&str> = self
|
||||
.project_graph
|
||||
.external_nodes
|
||||
.keys()
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
|
||||
// we can only vouch for @nx packages's executor dependencies
|
||||
// if it's "run commands" or third-party we skip traversing since we have no info what this command depends on
|
||||
if target
|
||||
@@ -139,29 +132,29 @@ impl HashPlanner {
|
||||
.next()
|
||||
.expect("Executors should always have a ':'");
|
||||
let Some(existing_package) =
|
||||
find_external_dependency_node_name(executor_package, &external_nodes_keys)
|
||||
find_external_dependency_node_name(executor_package, &self.project_graph)
|
||||
else {
|
||||
// this usually happens because the executor was a local plugin.
|
||||
// todo)) @Cammisuli: we need to gather the project's inputs and its dep inputs similar to how we do it in `self_and_deps_inputs`
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(vec![HashInstruction::External(
|
||||
existing_package.to_string(),
|
||||
existing_package.to_owned(),
|
||||
)]))
|
||||
} else {
|
||||
let mut external_deps: Vec<&str> = vec![];
|
||||
let mut external_deps: Vec<&'a String> = vec![];
|
||||
for input in self_inputs {
|
||||
match input {
|
||||
Input::ExternalDependency(deps) => {
|
||||
for dep in deps.iter() {
|
||||
let external_node_name: Option<&str> =
|
||||
find_external_dependency_node_name(dep, &external_nodes_keys);
|
||||
let external_node_name =
|
||||
find_external_dependency_node_name(dep, &self.project_graph);
|
||||
let Some(external_node_name) = external_node_name else {
|
||||
anyhow::bail!("The externalDependency '{dep}' for '{project_name}:{target_name}' could not be found")
|
||||
};
|
||||
|
||||
external_deps.push(external_node_name);
|
||||
external_deps.extend(&external_deps_map[external_node_name]);
|
||||
external_deps.push(&external_node_name);
|
||||
external_deps.extend(&external_deps_map[&external_node_name]);
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
@@ -186,12 +179,11 @@ impl HashPlanner {
|
||||
task: &Task,
|
||||
inputs: &SplitInputs,
|
||||
task_graph: &TaskGraph,
|
||||
external_deps_mapped: &hashbrown::HashMap<&str, Vec<&str>>,
|
||||
external_deps_mapped: &hashbrown::HashMap<&String, Vec<&String>>,
|
||||
visited: &mut Box<hashbrown::HashSet<String>>,
|
||||
) -> anyhow::Result<Vec<HashInstruction>> {
|
||||
let project_deps = &self.project_graph.dependencies[project_name]
|
||||
.iter()
|
||||
.map(|d| d.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let self_inputs = self.gather_self_inputs(project_name, &inputs.self_inputs);
|
||||
let deps_inputs = self.gather_dependency_inputs(
|
||||
@@ -215,15 +207,13 @@ impl HashPlanner {
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn setup_external_deps(&self) -> hashbrown::HashMap<&str, Vec<&str>> {
|
||||
fn setup_external_deps(&self) -> hashbrown::HashMap<&String, Vec<&String>> {
|
||||
self.project_graph
|
||||
.external_nodes
|
||||
.keys()
|
||||
.collect::<Vec<_>>()
|
||||
.par_iter()
|
||||
.map(|external_node| {
|
||||
(
|
||||
external_node.as_str(),
|
||||
external_node,
|
||||
utils::find_all_project_node_dependencies(
|
||||
external_node,
|
||||
&self.project_graph,
|
||||
@@ -240,8 +230,8 @@ impl HashPlanner {
|
||||
task: &Task,
|
||||
inputs: &[Input],
|
||||
task_graph: &TaskGraph,
|
||||
project_deps: &[&'a str],
|
||||
external_deps_mapped: &hashbrown::HashMap<&str, Vec<&'a str>>,
|
||||
project_deps: &[&'a String],
|
||||
external_deps_mapped: &hashbrown::HashMap<&String, Vec<&'a String>>,
|
||||
visited: &mut Box<hashbrown::HashSet<String>>,
|
||||
) -> anyhow::Result<Vec<HashInstruction>> {
|
||||
let mut deps_inputs: Vec<HashInstruction> = vec![];
|
||||
@@ -377,12 +367,23 @@ impl HashPlanner {
|
||||
|
||||
fn find_external_dependency_node_name<'a>(
|
||||
package_name: &str,
|
||||
external_nodes: &[&'a str],
|
||||
) -> Option<&'a str> {
|
||||
external_nodes
|
||||
.iter()
|
||||
.find(|n| **n == package_name || n.ends_with(package_name))
|
||||
.copied()
|
||||
project_graph: &'a ProjectGraph,
|
||||
) -> Option<&'a String> {
|
||||
let npm_name = format!("npm:{}", &package_name);
|
||||
if let Some((key, _)) = project_graph.external_nodes.get_key_value(package_name) {
|
||||
Some(key)
|
||||
} else if let Some((key, _)) = project_graph.external_nodes.get_key_value(&npm_name) {
|
||||
Some(key)
|
||||
} else {
|
||||
for (node_name, node) in project_graph.external_nodes.iter() {
|
||||
if let Some(pkg_name) = &node.package_name {
|
||||
if pkg_name.as_str() == package_name {
|
||||
return Some(node_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn project_file_set_inputs(project_name: &str, file_sets: Vec<&str>) -> Vec<HashInstruction> {
|
||||
|
||||
@@ -54,6 +54,7 @@ mod test {
|
||||
(
|
||||
"my_external".to_string(),
|
||||
ExternalNode {
|
||||
package_name: Some("my_external".into()),
|
||||
version: "0.0.1".into(),
|
||||
hash: None,
|
||||
},
|
||||
@@ -61,6 +62,7 @@ mod test {
|
||||
(
|
||||
"my_external_with_hash".to_string(),
|
||||
ExternalNode {
|
||||
package_name: Some("my_external_with_hash".into()),
|
||||
version: "0.0.1".into(),
|
||||
hash: Some("hashvalue".into()),
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ pub(super) fn find_all_project_node_dependencies<'a>(
|
||||
parent_node_name: &str,
|
||||
project_graph: &'a ProjectGraph,
|
||||
exclude_external_dependencies: bool,
|
||||
) -> Vec<&'a str> {
|
||||
) -> Vec<&'a String> {
|
||||
let mut dependent_node_names = HashSet::new();
|
||||
collect_dependent_project_node_names(
|
||||
project_graph,
|
||||
@@ -19,28 +19,27 @@ pub(super) fn find_all_project_node_dependencies<'a>(
|
||||
fn collect_dependent_project_node_names<'a>(
|
||||
project_graph: &'a ProjectGraph,
|
||||
parent_node_name: &str,
|
||||
dependent_node_names: &mut HashSet<&'a str>,
|
||||
dependent_node_names: &mut HashSet<&'a String>,
|
||||
exclude_external_dependencies: bool,
|
||||
) {
|
||||
let Some( dependencies ) = project_graph.dependencies.get(parent_node_name) else {
|
||||
let Some(dependencies) = project_graph.dependencies.get(parent_node_name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for dependency in dependencies {
|
||||
let dep = dependency.as_str();
|
||||
// skip dependencies already added (avoid circular dependencies)
|
||||
if dependent_node_names.contains(dep) {
|
||||
if dependent_node_names.contains(dependency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if exclude_external_dependencies && project_graph.external_nodes.contains_key(dep) {
|
||||
if exclude_external_dependencies && project_graph.external_nodes.contains_key(dependency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dependent_node_names.insert(dep);
|
||||
dependent_node_names.insert(dependency);
|
||||
collect_dependent_project_node_names(
|
||||
project_graph,
|
||||
dependency.as_str(),
|
||||
dependency,
|
||||
dependent_node_names,
|
||||
exclude_external_dependencies,
|
||||
);
|
||||
|
||||
@@ -187,6 +187,52 @@ describe('workspace files', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
describe('globbing', () => {
|
||||
let context: WorkspaceContext;
|
||||
let fs: TempFs;
|
||||
|
||||
beforeEach(async () => {
|
||||
fs = new TempFs('workspace-files');
|
||||
|
||||
await fs.createFiles({
|
||||
'file.txt': '',
|
||||
'file.css': '',
|
||||
'file.js': '',
|
||||
});
|
||||
|
||||
context = new WorkspaceContext(
|
||||
fs.tempDir,
|
||||
cacheDirectoryForWorkspace(fs.tempDir)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
context = null;
|
||||
fs.reset();
|
||||
});
|
||||
|
||||
it('should glob', () => {
|
||||
const results = context.glob(['**/*.txt']);
|
||||
expect(results).toContain('file.txt');
|
||||
expect(results).not.toContain('file.css');
|
||||
expect(results).not.toContain('file.js');
|
||||
});
|
||||
|
||||
it('should glob and exclude patterns', () => {
|
||||
const results = context.glob(['**/*'], ['**/*.txt']);
|
||||
expect(results).not.toContain('file.txt');
|
||||
expect(results).toContain('file.css');
|
||||
expect(results).toContain('file.js');
|
||||
});
|
||||
|
||||
it('should glob and not exclude if exclude is empty', () => {
|
||||
const results = context.glob(['**/*'], []);
|
||||
expect(results).toContain('file.txt');
|
||||
expect(results).toContain('file.css');
|
||||
expect(results).toContain('file.js');
|
||||
});
|
||||
});
|
||||
|
||||
// describe('errors', () => {
|
||||
// it('it should infer names of configuration files without a name', async () => {
|
||||
// const fs = new TempFs('workspace-files');
|
||||
|
||||
@@ -42,6 +42,7 @@ export function transformProjectGraphForRust(
|
||||
graph.externalNodes ?? {}
|
||||
)) {
|
||||
externalNodes[projectName] = {
|
||||
packageName: externalNode.data.packageName,
|
||||
hash: externalNode.data.hash,
|
||||
version: externalNode.data.version,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use napi::bindgen_prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use crate::native::walker::nx_walker_sync;
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
use ignore::Match;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::trace;
|
||||
use watchexec_events::filekind::CreateKind;
|
||||
use watchexec_events::filekind::FileEventKind;
|
||||
use watchexec_events::filekind::ModifyKind::Name;
|
||||
use watchexec_events::filekind::RenameMode;
|
||||
use watchexec_events::{Event, Tag};
|
||||
|
||||
use crate::native::watch::utils::transform_event;
|
||||
@@ -28,7 +35,7 @@ impl From<&WatchEventInternal> for WatchEvent {
|
||||
fn from(value: &WatchEventInternal) -> Self {
|
||||
let path = value
|
||||
.path
|
||||
.strip_prefix(value.origin.as_ref().expect("origin is available"))
|
||||
.strip_prefix(&value.origin)
|
||||
.unwrap_or(&value.path)
|
||||
.display()
|
||||
.to_string();
|
||||
@@ -47,75 +54,130 @@ impl From<&WatchEventInternal> for WatchEvent {
|
||||
pub(super) struct WatchEventInternal {
|
||||
pub path: PathBuf,
|
||||
pub r#type: EventType,
|
||||
pub origin: Option<String>,
|
||||
pub origin: String,
|
||||
}
|
||||
|
||||
impl From<&Event> for WatchEventInternal {
|
||||
fn from(value: &Event) -> Self {
|
||||
let transformed = transform_event(value);
|
||||
let value = transformed.as_ref().unwrap_or(value);
|
||||
pub fn transform_event_to_watch_events(
|
||||
value: &Event,
|
||||
origin: &str,
|
||||
) -> anyhow::Result<Vec<WatchEventInternal>> {
|
||||
let transformed = transform_event(value);
|
||||
let value = transformed.as_ref().unwrap_or(value);
|
||||
|
||||
let path = value.paths().next().expect("there should always be a path");
|
||||
let Some(path) = value.paths().next() else {
|
||||
let error_msg = "unable to get path from the event";
|
||||
trace!(?value, error_msg);
|
||||
anyhow::bail!(error_msg)
|
||||
};
|
||||
|
||||
let event_kind = value
|
||||
.tags
|
||||
.iter()
|
||||
.find_map(|t| match t {
|
||||
Tag::FileEventKind(event_kind) => Some(event_kind),
|
||||
_ => None,
|
||||
})
|
||||
.expect("there should always be a file event kind");
|
||||
let Some(event_kind) = value.tags.iter().find_map(|t| match t {
|
||||
Tag::FileEventKind(event_kind) => Some(event_kind),
|
||||
_ => None,
|
||||
}) else {
|
||||
let error_msg = "unable to get the file event kind";
|
||||
trace!(?value, error_msg);
|
||||
anyhow::bail!(error_msg)
|
||||
};
|
||||
|
||||
let path_ref = path.0;
|
||||
let event_type = if path.1.is_none() && !path_ref.exists() {
|
||||
EventType::delete
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::fs;
|
||||
use std::os::macos::fs::MetadataExt;
|
||||
let path_ref = path.0;
|
||||
if path.1.is_none() && !path_ref.exists() {
|
||||
Ok(vec![WatchEventInternal {
|
||||
path: path_ref.into(),
|
||||
r#type: EventType::delete,
|
||||
origin: origin.to_owned(),
|
||||
}])
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::fs;
|
||||
use std::os::macos::fs::MetadataExt;
|
||||
|
||||
let t = fs::metadata(path_ref);
|
||||
match t {
|
||||
Err(_) => EventType::delete,
|
||||
Ok(t) => {
|
||||
let modified_time = t.st_mtime();
|
||||
let birth_time = t.st_birthtime();
|
||||
let origin = origin.to_owned();
|
||||
let t = fs::metadata(path_ref);
|
||||
let event_type = match t {
|
||||
Err(_) => EventType::delete,
|
||||
Ok(t) => {
|
||||
let modified_time = t.st_mtime();
|
||||
let birth_time = t.st_birthtime();
|
||||
|
||||
// if a file is created and updated near the same time, we always get a create event
|
||||
// so we need to check the timestamps to see if it was created or updated
|
||||
// if the modified time is the same as birth_time then it was created
|
||||
if modified_time == birth_time {
|
||||
EventType::create
|
||||
} else {
|
||||
EventType::update
|
||||
}
|
||||
// if a file is created and updated near the same time, we always get a create event
|
||||
// so we need to check the timestamps to see if it was created or updated
|
||||
// if the modified time is the same as birth_time then it was created
|
||||
if modified_time == birth_time {
|
||||
EventType::create
|
||||
} else {
|
||||
EventType::update
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
use watchexec_events::filekind::FileEventKind;
|
||||
use watchexec_events::filekind::ModifyKind::Name;
|
||||
use watchexec_events::filekind::RenameMode;
|
||||
Ok(vec![WatchEventInternal {
|
||||
path: path_ref.into(),
|
||||
r#type: event_type,
|
||||
origin,
|
||||
}])
|
||||
}
|
||||
|
||||
match event_kind {
|
||||
FileEventKind::Create(_) => EventType::create,
|
||||
FileEventKind::Modify(Name(RenameMode::To)) => EventType::create,
|
||||
FileEventKind::Modify(Name(RenameMode::From)) => EventType::delete,
|
||||
FileEventKind::Modify(_) => EventType::update,
|
||||
_ => EventType::update,
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Ok(create_watch_event_internal(origin, event_kind, path_ref))
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
|
||||
{
|
||||
if matches!(event_kind, FileEventKind::Create(CreateKind::Folder)) {
|
||||
let mut result = vec![];
|
||||
|
||||
let mut gitignore_builder = GitignoreBuilder::new(origin);
|
||||
let origin_path: &Path = origin.as_ref();
|
||||
gitignore_builder.add(origin_path.join(".nxignore"));
|
||||
let ignore = gitignore_builder.build()?;
|
||||
|
||||
for path in nx_walker_sync(path_ref, None) {
|
||||
let path = path_ref.join(path);
|
||||
let is_dir = path.is_dir();
|
||||
if is_dir
|
||||
|| matches!(
|
||||
ignore.matched_path_or_any_parents(&path, is_dir),
|
||||
Match::Ignore(_)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(WatchEventInternal {
|
||||
path,
|
||||
r#type: EventType::create,
|
||||
origin: origin.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
Ok(create_watch_event_internal(origin, event_kind, path_ref))
|
||||
}
|
||||
};
|
||||
|
||||
trace!(?path, ?event_kind, ?event_type, "event kind -> event type");
|
||||
|
||||
WatchEventInternal {
|
||||
path: path.0.into(),
|
||||
r#type: event_type,
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn create_watch_event_internal(
|
||||
origin: &str,
|
||||
event_kind: &FileEventKind,
|
||||
path_ref: &Path,
|
||||
) -> Vec<WatchEventInternal> {
|
||||
let event_kind = match event_kind {
|
||||
FileEventKind::Create(CreateKind::File) => EventType::create,
|
||||
FileEventKind::Modify(Name(RenameMode::To)) => EventType::create,
|
||||
FileEventKind::Modify(Name(RenameMode::From)) => EventType::delete,
|
||||
FileEventKind::Modify(_) => EventType::update,
|
||||
_ => EventType::update,
|
||||
};
|
||||
|
||||
vec![WatchEventInternal {
|
||||
path: path_ref.into(),
|
||||
r#type: event_kind,
|
||||
origin: origin.to_owned(),
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ impl Filterer for WatchFilterer {
|
||||
FileEventKind::Create(CreateKind::File) => continue,
|
||||
FileEventKind::Remove(RemoveKind::File) => continue,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
FileEventKind::Create(CreateKind::Folder) => continue,
|
||||
|
||||
#[cfg(windows)]
|
||||
FileEventKind::Modify(ModifyKind::Any) => continue,
|
||||
#[cfg(windows)]
|
||||
@@ -92,6 +95,13 @@ impl Filterer for WatchFilterer {
|
||||
path,
|
||||
file_type: Some(FileType::File) | None,
|
||||
} if !path.display().to_string().ends_with('~') => continue,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
Tag::Path {
|
||||
path: _,
|
||||
file_type: Some(FileType::Dir),
|
||||
} => continue,
|
||||
|
||||
Tag::Source(Source::Filesystem) => continue,
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::collections::HashMap;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::native::watch::types::{EventType, WatchEvent, WatchEventInternal};
|
||||
use crate::native::watch::types::{
|
||||
transform_event_to_watch_events, EventType, WatchEvent, WatchEventInternal,
|
||||
};
|
||||
use crate::native::watch::watch_filterer;
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi::threadsafe_function::{
|
||||
@@ -118,11 +120,8 @@ impl Watcher {
|
||||
let events = action
|
||||
.events
|
||||
.par_iter()
|
||||
.map(|ev| {
|
||||
let mut watch_event: WatchEventInternal = ev.into();
|
||||
watch_event.origin = Some(origin_path.clone());
|
||||
watch_event
|
||||
})
|
||||
.filter_map(|ev| transform_event_to_watch_events(ev, &origin_path).ok())
|
||||
.flatten()
|
||||
.collect::<Vec<WatchEventInternal>>();
|
||||
|
||||
let mut group_events: HashMap<String, WatchEventInternal> = HashMap::new();
|
||||
|
||||
@@ -11,9 +11,16 @@ pub(super) fn glob_files(
|
||||
) -> napi::Result<impl ParallelIterator<Item = &FileData>> {
|
||||
let globs = build_glob_set(&globs)?;
|
||||
|
||||
let exclude_glob_set = exclude
|
||||
.map(|exclude| build_glob_set(&exclude))
|
||||
.transpose()?;
|
||||
let exclude_glob_set = match exclude {
|
||||
Some(exclude) => {
|
||||
if exclude.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(build_glob_set(&exclude)?)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(files.par_iter().filter(move |file_data| {
|
||||
let path = &file_data.file;
|
||||
@@ -25,7 +32,7 @@ pub(super) fn glob_files(
|
||||
|
||||
exclude_glob_set
|
||||
.as_ref()
|
||||
.map(|exclude_glob_set| exclude_glob_set.is_match(path))
|
||||
.map(|exclude_glob_set| !exclude_glob_set.is_match(path))
|
||||
.unwrap_or(is_match)
|
||||
}))
|
||||
}
|
||||
|
||||
+4
@@ -18,15 +18,19 @@ describe('explicit package json dependencies', () => {
|
||||
projects: {
|
||||
proj: {
|
||||
root: 'libs/proj',
|
||||
name: 'proj',
|
||||
},
|
||||
proj2: {
|
||||
root: 'libs/proj2',
|
||||
name: 'proj2',
|
||||
},
|
||||
proj3: {
|
||||
root: 'libs/proj3',
|
||||
name: 'proj3',
|
||||
},
|
||||
proj4: {
|
||||
root: 'libs/proj4',
|
||||
name: 'proj4',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+3
-1
@@ -580,7 +580,9 @@ async function createContext(
|
||||
|
||||
return {
|
||||
externalNodes: builder.getUpdatedProjectGraph().externalNodes,
|
||||
projects: projects,
|
||||
projects: Object.fromEntries(
|
||||
Object.entries(projects).map(([root, config]) => [config.name, config])
|
||||
),
|
||||
nxJsonConfiguration: nxJson,
|
||||
filesToProcess: fileMap,
|
||||
fileMap: fileMap,
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('getImplicitlyTouchedProjects', () => {
|
||||
root: 'a',
|
||||
targets: {
|
||||
build: {
|
||||
inputs: ['files'],
|
||||
inputs: ['files', '{workspaceRoot}/b.txt'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -151,6 +151,10 @@ describe('getImplicitlyTouchedProjects', () => {
|
||||
expect(getImplicitlyTouchedProjects(fileChanges, graph, nxJson)).toEqual([
|
||||
'a',
|
||||
]);
|
||||
fileChanges = getFileChanges(['b.txt']);
|
||||
expect(getImplicitlyTouchedProjects(fileChanges, graph, nxJson)).toEqual([
|
||||
'a',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not return projects which have touched files inputs which are not used by its targets', () => {
|
||||
|
||||
@@ -87,7 +87,9 @@ function extractFilesFromInputs(
|
||||
const globalFiles = [];
|
||||
for (const input of inputs) {
|
||||
if (typeof input === 'string' && input in namedInputs) {
|
||||
return extractFilesFromInputs(namedInputs[input], namedInputs);
|
||||
globalFiles.push(
|
||||
...extractFilesFromInputs(namedInputs[input], namedInputs)
|
||||
);
|
||||
} else if (
|
||||
typeof input === 'string' &&
|
||||
input.startsWith('{workspaceRoot}/')
|
||||
|
||||
@@ -60,7 +60,7 @@ export function getFileMap(): {
|
||||
}
|
||||
|
||||
export async function buildProjectGraphUsingProjectFileMap(
|
||||
projects: Record<string, ProjectConfiguration>,
|
||||
projectRootMap: Record<string, ProjectConfiguration>,
|
||||
externalNodes: Record<string, ProjectGraphExternalNode>,
|
||||
fileMap: FileMap,
|
||||
allWorkspaceFiles: FileData[],
|
||||
@@ -75,6 +75,12 @@ export async function buildProjectGraphUsingProjectFileMap(
|
||||
storedAllWorkspaceFiles = allWorkspaceFiles;
|
||||
storedRustReferences = rustReferences;
|
||||
|
||||
const projects: Record<string, ProjectConfiguration> = {};
|
||||
for (const root in projectRootMap) {
|
||||
const project = projectRootMap[root];
|
||||
projects[project.name] = project;
|
||||
}
|
||||
|
||||
const nxJson = readNxJson();
|
||||
const projectGraphVersion = '6.0';
|
||||
assertWorkspaceValidity(projects, nxJson);
|
||||
@@ -233,15 +239,9 @@ function createContext(
|
||||
fileMap: FileMap,
|
||||
filesToProcess: FileMap
|
||||
): CreateDependenciesContext {
|
||||
const clonedProjects = Object.keys(projects).reduce((map, projectName) => {
|
||||
map[projectName] = {
|
||||
...projects[projectName],
|
||||
};
|
||||
return map;
|
||||
}, {} as Record<string, ProjectConfiguration>);
|
||||
return {
|
||||
nxJsonConfiguration: nxJson,
|
||||
projects: clonedProjects,
|
||||
projects,
|
||||
externalNodes,
|
||||
workspaceRoot,
|
||||
fileMap,
|
||||
|
||||
@@ -1,6 +1,75 @@
|
||||
import { CreateNodesResultWithContext } from './plugins/internal-api';
|
||||
import { ConfigurationResult } from './utils/project-configuration-utils';
|
||||
import {
|
||||
ConfigurationResult,
|
||||
ConfigurationSourceMaps,
|
||||
} from './utils/project-configuration-utils';
|
||||
import { ProjectConfiguration } from '../config/workspace-json-project-json';
|
||||
import {
|
||||
ProcessDependenciesError,
|
||||
ProcessProjectGraphError,
|
||||
} from './build-project-graph';
|
||||
import { ProjectGraph } from '../config/project-graph';
|
||||
|
||||
export class ProjectGraphError extends Error {
|
||||
readonly #errors: Array<
|
||||
| CreateNodesError
|
||||
| MergeNodesError
|
||||
| ProjectsWithNoNameError
|
||||
| ProjectsWithConflictingNamesError
|
||||
| ProcessDependenciesError
|
||||
| ProcessProjectGraphError
|
||||
>;
|
||||
readonly #partialProjectGraph: ProjectGraph;
|
||||
readonly #partialSourceMaps: ConfigurationSourceMaps;
|
||||
|
||||
constructor(
|
||||
errors: Array<
|
||||
| CreateNodesError
|
||||
| MergeNodesError
|
||||
| ProjectsWithNoNameError
|
||||
| ProjectsWithConflictingNamesError
|
||||
| ProcessDependenciesError
|
||||
| ProcessProjectGraphError
|
||||
>,
|
||||
partialProjectGraph: ProjectGraph,
|
||||
partialSourceMaps: ConfigurationSourceMaps
|
||||
) {
|
||||
super(`Failed to process project graph.`);
|
||||
this.name = this.constructor.name;
|
||||
this.#errors = errors;
|
||||
this.#partialProjectGraph = partialProjectGraph;
|
||||
this.#partialSourceMaps = partialSourceMaps;
|
||||
this.stack = `${this.message}\n ${errors
|
||||
.map((error) => error.stack.split('\n').join('\n '))
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon cannot throw errors which contain methods as they are not serializable.
|
||||
*
|
||||
* This method creates a new {@link ProjectGraphError} from a {@link DaemonProjectGraphError} with the methods based on the same serialized data.
|
||||
*/
|
||||
static fromDaemonProjectGraphError(e: DaemonProjectGraphError) {
|
||||
return new ProjectGraphError(e.errors, e.projectGraph, e.sourceMaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets the partial project graph despite the errors which occured.
|
||||
* This partial project graph may be missing nodes, properties of nodes, or dependencies.
|
||||
* This is useful mostly for visualization/debugging. It should not be used for running tasks.
|
||||
*/
|
||||
getPartialProjectGraph() {
|
||||
return this.#partialProjectGraph;
|
||||
}
|
||||
|
||||
getPartialSourcemaps() {
|
||||
return this.#partialSourceMaps;
|
||||
}
|
||||
|
||||
getErrors() {
|
||||
return this.#errors;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectsWithConflictingNamesError extends Error {
|
||||
constructor(
|
||||
@@ -153,3 +222,25 @@ export function isMergeNodesError(e: unknown): e is MergeNodesError {
|
||||
(typeof e === 'object' && 'name' in e && e?.name === MergeNodesError.name)
|
||||
);
|
||||
}
|
||||
|
||||
export class DaemonProjectGraphError extends Error {
|
||||
constructor(
|
||||
public errors: any[],
|
||||
readonly projectGraph: ProjectGraph,
|
||||
readonly sourceMaps: ConfigurationSourceMaps
|
||||
) {
|
||||
super(
|
||||
`The Daemon Process threw an error while calculating the project graph. Convert this error to a ProjectGraphError to get more information.`
|
||||
);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadPluginError extends Error {
|
||||
constructor(public plugin: string, cause: Error) {
|
||||
super(`Could not load plugin ${plugin}`, {
|
||||
cause,
|
||||
});
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,19 @@ describe('fileMapUtils', () => {
|
||||
version: 2,
|
||||
projects: {
|
||||
demo: {
|
||||
name: 'demo',
|
||||
root: 'apps/demo',
|
||||
sourceRoot: 'apps/demo/src',
|
||||
projectType: 'application' as ProjectType,
|
||||
},
|
||||
'demo-e2e': {
|
||||
name: 'demo-e2e',
|
||||
root: 'apps/demo-e2e',
|
||||
sourceRoot: 'apps/demo-e2e/src',
|
||||
projectType: 'application' as ProjectType,
|
||||
},
|
||||
ui: {
|
||||
name: 'ui',
|
||||
root: 'libs/ui',
|
||||
sourceRoot: 'libs/ui/src',
|
||||
projectType: 'library' as ProjectType,
|
||||
|
||||
@@ -192,7 +192,7 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) {
|
||||
...getDefaultPluginsSync(root),
|
||||
];
|
||||
|
||||
const projectRootMap: Map<string, ProjectConfiguration> = new Map();
|
||||
const projectRootMap: Record<string, ProjectConfiguration> = {};
|
||||
|
||||
// We iterate over plugins first - this ensures that plugins specified first take precedence.
|
||||
for (const plugin of plugins) {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { PluginConfiguration } from '../../../config/nx-json';
|
||||
import { LoadedNxPlugin } from '../internal-api';
|
||||
import { loadRemoteNxPlugin } from './plugin-pool';
|
||||
|
||||
const remotePluginCache = new Map<
|
||||
string,
|
||||
[Promise<LoadedNxPlugin>, () => void]
|
||||
>();
|
||||
/**
|
||||
* Used to ensure 1 plugin : 1 worker
|
||||
*/
|
||||
const remotePluginCache = new Map<string, Promise<LoadedNxPlugin>>();
|
||||
|
||||
export function loadNxPluginInIsolation(
|
||||
plugin: PluginConfiguration,
|
||||
@@ -15,10 +15,11 @@ export function loadNxPluginInIsolation(
|
||||
const cacheKey = JSON.stringify(plugin);
|
||||
|
||||
if (remotePluginCache.has(cacheKey)) {
|
||||
return remotePluginCache.get(cacheKey);
|
||||
return [remotePluginCache.get(cacheKey), () => {}];
|
||||
}
|
||||
|
||||
const [loadingPlugin, cleanup] = loadRemoteNxPlugin(plugin, root);
|
||||
remotePluginCache.set(cacheKey, [loadingPlugin, cleanup]);
|
||||
return [loadingPlugin, cleanup];
|
||||
const loadingPlugin = loadRemoteNxPlugin(plugin, root);
|
||||
remotePluginCache.set(cacheKey, loadingPlugin);
|
||||
// We clean up plugin workers when Nx process completes.
|
||||
return [loadingPlugin, () => {}];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { PluginConfiguration } from '../../../config/nx-json';
|
||||
import { CreateDependenciesContext, CreateNodesContext } from '../public-api';
|
||||
import { LoadedNxPlugin } from '../internal-api';
|
||||
import { Serializable } from 'child_process';
|
||||
|
||||
export interface PluginWorkerLoadMessage {
|
||||
type: 'load';
|
||||
@@ -26,7 +27,7 @@ export interface PluginWorkerLoadResult {
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
error: Error;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ export interface PluginWorkerCreateNodesResult {
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
error: Error;
|
||||
tx: string;
|
||||
};
|
||||
}
|
||||
@@ -72,7 +73,7 @@ export interface PluginCreateDependenciesResult {
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
error: Error;
|
||||
tx: string;
|
||||
};
|
||||
}
|
||||
@@ -96,7 +97,7 @@ export interface PluginWorkerProcessProjectGraphResult {
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
error: Error;
|
||||
tx: string;
|
||||
};
|
||||
}
|
||||
@@ -113,6 +114,38 @@ export type PluginWorkerResult =
|
||||
| PluginCreateDependenciesResult
|
||||
| PluginWorkerProcessProjectGraphResult;
|
||||
|
||||
export function isPluginWorkerMessage(
|
||||
message: Serializable
|
||||
): message is PluginWorkerMessage {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
'type' in message &&
|
||||
typeof message.type === 'string' &&
|
||||
[
|
||||
'load',
|
||||
'createNodes',
|
||||
'createDependencies',
|
||||
'processProjectGraph',
|
||||
].includes(message.type)
|
||||
);
|
||||
}
|
||||
|
||||
export function isPluginWorkerResult(
|
||||
message: Serializable
|
||||
): message is PluginWorkerResult {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
'type' in message &&
|
||||
typeof message.type === 'string' &&
|
||||
[
|
||||
'load-result',
|
||||
'createNodesResult',
|
||||
'createDependenciesResult',
|
||||
'processProjectGraphResult',
|
||||
].includes(message.type)
|
||||
);
|
||||
}
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
// The handler can return a message to be sent back to the process from which the message originated
|
||||
@@ -126,7 +159,7 @@ type MessageHandlerReturn<T extends PluginWorkerMessage | PluginWorkerResult> =
|
||||
export async function consumeMessage<
|
||||
T extends PluginWorkerMessage | PluginWorkerResult
|
||||
>(
|
||||
raw: string | T,
|
||||
raw: T,
|
||||
handlers: {
|
||||
[K in T['type']]: (
|
||||
// Extract restricts the type of payload to the payload of the message with the type K
|
||||
@@ -134,20 +167,12 @@ export async function consumeMessage<
|
||||
) => MessageHandlerReturn<T>;
|
||||
}
|
||||
) {
|
||||
const message: T = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
const message: T = raw;
|
||||
const handler = handlers[message.type];
|
||||
if (handler) {
|
||||
const response = await handler(message.payload);
|
||||
if (response) {
|
||||
process.send!(createMessage(response));
|
||||
process.send!(response);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unhandled message type: ${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function createMessage(
|
||||
message: PluginWorkerMessage | PluginWorkerResult
|
||||
): string {
|
||||
return JSON.stringify(message);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import { ChildProcess, Serializable, fork } from 'child_process';
|
||||
import path = require('path');
|
||||
|
||||
import { PluginConfiguration } from '../../../config/nx-json';
|
||||
@@ -7,7 +7,7 @@ import { PluginConfiguration } from '../../../config/nx-json';
|
||||
// import { logger } from '../../utils/logger';
|
||||
|
||||
import { LoadedNxPlugin, nxPluginCache } from '../internal-api';
|
||||
import { PluginWorkerResult, consumeMessage, createMessage } from './messaging';
|
||||
import { consumeMessage, isPluginWorkerResult } from './messaging';
|
||||
|
||||
const cleanupFunctions = new Set<() => void>();
|
||||
|
||||
@@ -22,7 +22,7 @@ interface PendingPromise {
|
||||
export function loadRemoteNxPlugin(
|
||||
plugin: PluginConfiguration,
|
||||
root: string
|
||||
): [Promise<LoadedNxPlugin>, () => void] {
|
||||
): Promise<LoadedNxPlugin> {
|
||||
// this should only really be true when running unit tests within
|
||||
// the Nx repo. We still need to start the worker in this case,
|
||||
// but its typescript.
|
||||
@@ -45,7 +45,7 @@ export function loadRemoteNxPlugin(
|
||||
...(isWorkerTypescript ? ['-r', 'ts-node/register'] : []),
|
||||
],
|
||||
});
|
||||
worker.send(createMessage({ type: 'load', payload: { plugin, root } }));
|
||||
worker.send({ type: 'load', payload: { plugin, root } });
|
||||
|
||||
// logger.verbose(`[plugin-worker] started worker: ${worker.pid}`);
|
||||
|
||||
@@ -60,19 +60,13 @@ export function loadRemoteNxPlugin(
|
||||
|
||||
cleanupFunctions.add(cleanupFunction);
|
||||
|
||||
return [
|
||||
new Promise<LoadedNxPlugin>((res, rej) => {
|
||||
worker.on(
|
||||
'message',
|
||||
createWorkerHandler(worker, pendingPromises, res, rej)
|
||||
);
|
||||
worker.on('exit', exitHandler);
|
||||
}),
|
||||
() => {
|
||||
cleanupFunction();
|
||||
cleanupFunctions.delete(cleanupFunction);
|
||||
},
|
||||
] as const;
|
||||
return new Promise<LoadedNxPlugin>((res, rej) => {
|
||||
worker.on(
|
||||
'message',
|
||||
createWorkerHandler(worker, pendingPromises, res, rej)
|
||||
);
|
||||
worker.on('exit', exitHandler);
|
||||
});
|
||||
}
|
||||
|
||||
async function shutdownPluginWorker(
|
||||
@@ -109,14 +103,11 @@ function createWorkerHandler(
|
||||
) {
|
||||
let pluginName: string;
|
||||
|
||||
return function (message: string) {
|
||||
const parsed = JSON.parse(message);
|
||||
// logger.verbose(
|
||||
// `[plugin-pool] received message: ${parsed.type} from ${
|
||||
// pluginName ?? worker.pid
|
||||
// }`
|
||||
// );
|
||||
consumeMessage<PluginWorkerResult>(parsed, {
|
||||
return function (message: Serializable) {
|
||||
if (!isPluginWorkerResult(message)) {
|
||||
return;
|
||||
}
|
||||
return consumeMessage(message, {
|
||||
'load-result': (result) => {
|
||||
if (result.success) {
|
||||
const { name, createNodesPattern } = result;
|
||||
@@ -130,12 +121,10 @@ function createWorkerHandler(
|
||||
(configFiles, ctx) => {
|
||||
const tx = pluginName + ':createNodes:' + performance.now();
|
||||
return registerPendingPromise(tx, pending, () => {
|
||||
worker.send(
|
||||
createMessage({
|
||||
type: 'createNodes',
|
||||
payload: { configFiles, context: ctx, tx },
|
||||
})
|
||||
);
|
||||
worker.send({
|
||||
type: 'createNodes',
|
||||
payload: { configFiles, context: ctx, tx },
|
||||
});
|
||||
});
|
||||
},
|
||||
]
|
||||
@@ -145,12 +134,10 @@ function createWorkerHandler(
|
||||
const tx =
|
||||
pluginName + ':createDependencies:' + performance.now();
|
||||
return registerPendingPromise(tx, pending, () => {
|
||||
worker.send(
|
||||
createMessage({
|
||||
type: 'createDependencies',
|
||||
payload: { context: ctx, tx },
|
||||
})
|
||||
);
|
||||
worker.send({
|
||||
type: 'createDependencies',
|
||||
payload: { context: ctx, tx },
|
||||
});
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
@@ -159,12 +146,10 @@ function createWorkerHandler(
|
||||
const tx =
|
||||
pluginName + ':processProjectGraph:' + performance.now();
|
||||
return registerPendingPromise(tx, pending, () => {
|
||||
worker.send(
|
||||
createMessage({
|
||||
type: 'processProjectGraph',
|
||||
payload: { graph, ctx, tx },
|
||||
})
|
||||
);
|
||||
worker.send({
|
||||
type: 'processProjectGraph',
|
||||
payload: { graph, ctx, tx },
|
||||
});
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { consumeMessage, PluginWorkerMessage } from './messaging';
|
||||
import { consumeMessage, isPluginWorkerMessage } from './messaging';
|
||||
import { LoadedNxPlugin } from '../internal-api';
|
||||
import { loadNxPlugin } from '../loader';
|
||||
import { runCreateNodesInParallel } from '../utils';
|
||||
import { Serializable } from 'child_process';
|
||||
import { createSerializableError } from '../../../utils/serializable-error';
|
||||
|
||||
global.NX_GRAPH_CREATION = true;
|
||||
|
||||
let plugin: LoadedNxPlugin;
|
||||
|
||||
process.on('message', async (message: string) => {
|
||||
consumeMessage<PluginWorkerMessage>(message, {
|
||||
process.on('message', async (message: Serializable) => {
|
||||
if (!isPluginWorkerMessage(message)) {
|
||||
return;
|
||||
}
|
||||
return consumeMessage(message, {
|
||||
load: async ({ plugin: pluginConfiguration, root }) => {
|
||||
process.chdir(root);
|
||||
try {
|
||||
@@ -31,9 +35,7 @@ process.on('message', async (message: string) => {
|
||||
type: 'load-result',
|
||||
payload: {
|
||||
success: false,
|
||||
error: `Could not load plugin ${plugin} \n ${
|
||||
e instanceof Error ? e.stack : ''
|
||||
}`,
|
||||
error: createSerializableError(e),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -48,7 +50,11 @@ process.on('message', async (message: string) => {
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'createNodesResult',
|
||||
payload: { success: false, error: e.stack, tx },
|
||||
payload: {
|
||||
success: false,
|
||||
error: createSerializableError(e),
|
||||
tx,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -62,7 +68,11 @@ process.on('message', async (message: string) => {
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'createDependenciesResult',
|
||||
payload: { success: false, error: e.stack, tx },
|
||||
payload: {
|
||||
success: false,
|
||||
error: createSerializableError(e),
|
||||
tx,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -76,7 +86,11 @@ process.on('message', async (message: string) => {
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'processProjectGraphResult',
|
||||
payload: { success: false, error: e.stack, tx },
|
||||
payload: {
|
||||
success: false,
|
||||
error: createSerializableError(e),
|
||||
tx,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
registerTsConfigPaths,
|
||||
} from '../../plugins/js/utils/register';
|
||||
import {
|
||||
ProjectRootMappings,
|
||||
createProjectRootMappingsFromProjectConfigurations,
|
||||
findProjectForPath,
|
||||
} from '../utils/find-project-for-path';
|
||||
@@ -26,14 +27,12 @@ import { logger } from '../../utils/logger';
|
||||
import type * as ts from 'typescript';
|
||||
import { extname } from 'node:path';
|
||||
import { NxPlugin } from './public-api';
|
||||
import path = require('node:path/posix');
|
||||
import {
|
||||
ExpandedPluginConfiguration,
|
||||
PluginConfiguration,
|
||||
} from '../../config/nx-json';
|
||||
import { PluginConfiguration } from '../../config/nx-json';
|
||||
import { retrieveProjectConfigurationsWithoutPluginInference } from '../utils/retrieve-workspace-files';
|
||||
import { normalizeNxPlugin } from './utils';
|
||||
import { LoadedNxPlugin } from './internal-api';
|
||||
import { LoadPluginError } from '../error-types';
|
||||
import path = require('node:path/posix');
|
||||
|
||||
export function readPluginPackageJson(
|
||||
pluginName: string,
|
||||
@@ -57,6 +56,9 @@ export function readPluginPackageJson(
|
||||
localPluginPath.path,
|
||||
'package.json'
|
||||
);
|
||||
if (!unregisterPluginTSTranspiler) {
|
||||
registerPluginTSTranspiler();
|
||||
}
|
||||
return {
|
||||
path: localPluginPackageJson,
|
||||
json: readJsonFile(localPluginPackageJson),
|
||||
@@ -113,12 +115,11 @@ function lookupLocalPlugin(
|
||||
projects: Record<string, ProjectConfiguration>,
|
||||
root = workspaceRoot
|
||||
) {
|
||||
const plugin = findNxProjectForImportPath(importPath, projects, root);
|
||||
if (!plugin) {
|
||||
const projectConfig = findNxProjectForImportPath(importPath, projects, root);
|
||||
if (!projectConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectConfig: ProjectConfiguration = projects[plugin];
|
||||
return { path: path.join(root, projectConfig.root), projectConfig };
|
||||
}
|
||||
|
||||
@@ -126,18 +127,23 @@ function findNxProjectForImportPath(
|
||||
importPath: string,
|
||||
projects: Record<string, ProjectConfiguration>,
|
||||
root = workspaceRoot
|
||||
): string | null {
|
||||
): ProjectConfiguration | null {
|
||||
const tsConfigPaths: Record<string, string[]> = readTsConfigPaths(root);
|
||||
const possiblePaths = tsConfigPaths[importPath]?.map((p) =>
|
||||
normalizePath(path.relative(root, path.join(root, p)))
|
||||
);
|
||||
if (possiblePaths?.length) {
|
||||
const projectRootMappings =
|
||||
createProjectRootMappingsFromProjectConfigurations(projects);
|
||||
const projectRootMappings: ProjectRootMappings = new Map();
|
||||
const projectNameMap = new Map<string, ProjectConfiguration>();
|
||||
for (const projectRoot in projects) {
|
||||
const project = projects[projectRoot];
|
||||
projectRootMappings.set(project.root, project.name);
|
||||
projectNameMap.set(project.name, project);
|
||||
}
|
||||
for (const tsConfigPath of possiblePaths) {
|
||||
const nxProject = findProjectForPath(tsConfigPath, projectRootMappings);
|
||||
if (nxProject) {
|
||||
return nxProject;
|
||||
return projectNameMap.get(nxProject);
|
||||
}
|
||||
}
|
||||
logger.verbose(
|
||||
@@ -247,39 +253,38 @@ export async function loadNxPluginAsync(
|
||||
paths: string[],
|
||||
root: string
|
||||
): Promise<LoadedNxPlugin> {
|
||||
try {
|
||||
require.resolve(
|
||||
typeof pluginConfiguration === 'string'
|
||||
? pluginConfiguration
|
||||
: pluginConfiguration.plugin
|
||||
);
|
||||
} catch {
|
||||
// If a plugin cannot be resolved, we will need projects to resolve it
|
||||
projectsWithoutInference ??=
|
||||
await retrieveProjectConfigurationsWithoutPluginInference(root);
|
||||
}
|
||||
|
||||
const moduleName =
|
||||
typeof pluginConfiguration === 'string'
|
||||
? pluginConfiguration
|
||||
: pluginConfiguration.plugin;
|
||||
try {
|
||||
try {
|
||||
require.resolve(moduleName);
|
||||
} catch {
|
||||
// If a plugin cannot be resolved, we will need projects to resolve it
|
||||
projectsWithoutInference ??=
|
||||
await retrieveProjectConfigurationsWithoutPluginInference(root);
|
||||
}
|
||||
|
||||
performance.mark(`Load Nx Plugin: ${moduleName} - start`);
|
||||
let { pluginPath, name } = await getPluginPathAndName(
|
||||
moduleName,
|
||||
paths,
|
||||
projectsWithoutInference,
|
||||
root
|
||||
);
|
||||
const plugin = normalizeNxPlugin(await importPluginModule(pluginPath));
|
||||
plugin.name ??= name;
|
||||
performance.mark(`Load Nx Plugin: ${moduleName} - end`);
|
||||
performance.measure(
|
||||
`Load Nx Plugin: ${moduleName}`,
|
||||
`Load Nx Plugin: ${moduleName} - start`,
|
||||
`Load Nx Plugin: ${moduleName} - end`
|
||||
);
|
||||
return new LoadedNxPlugin(plugin, pluginConfiguration);
|
||||
performance.mark(`Load Nx Plugin: ${moduleName} - start`);
|
||||
let { pluginPath, name } = await getPluginPathAndName(
|
||||
moduleName,
|
||||
paths,
|
||||
projectsWithoutInference,
|
||||
root
|
||||
);
|
||||
const plugin = normalizeNxPlugin(await importPluginModule(pluginPath));
|
||||
plugin.name ??= name;
|
||||
performance.mark(`Load Nx Plugin: ${moduleName} - end`);
|
||||
performance.measure(
|
||||
`Load Nx Plugin: ${moduleName}`,
|
||||
`Load Nx Plugin: ${moduleName} - start`,
|
||||
`Load Nx Plugin: ${moduleName} - end`
|
||||
);
|
||||
return new LoadedNxPlugin(plugin, pluginConfiguration);
|
||||
} catch (e) {
|
||||
throw new LoadPluginError(moduleName, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function importPluginModule(pluginPath: string): Promise<NxPlugin> {
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface CreateNodesContext {
|
||||
/**
|
||||
* The subset of configuration files which match the createNodes pattern
|
||||
*/
|
||||
readonly configFiles: string[];
|
||||
readonly configFiles: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { runCreateNodesInParallel } from './utils';
|
||||
|
||||
const configFiles = ['file1', 'file2'] as const;
|
||||
|
||||
const context = {
|
||||
file: 'file1',
|
||||
nxJsonConfiguration: {},
|
||||
workspaceRoot: '',
|
||||
configFiles,
|
||||
} as const;
|
||||
|
||||
describe('createNodesInParallel', () => {
|
||||
it('should return results with context', async () => {
|
||||
const plugin = {
|
||||
name: 'test',
|
||||
createNodes: [
|
||||
'*/**/*',
|
||||
async (file: string) => {
|
||||
return {
|
||||
projects: {
|
||||
[file]: {
|
||||
root: file,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
const options = {};
|
||||
|
||||
const results = await runCreateNodesInParallel(
|
||||
configFiles,
|
||||
plugin,
|
||||
options,
|
||||
context
|
||||
);
|
||||
|
||||
expect(results).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "file1",
|
||||
"pluginName": "test",
|
||||
"projects": {
|
||||
"file1": {
|
||||
"root": "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"file": "file2",
|
||||
"pluginName": "test",
|
||||
"projects": {
|
||||
"file2": {
|
||||
"root": "file2",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle async errors', async () => {
|
||||
const plugin = {
|
||||
name: 'test',
|
||||
createNodes: [
|
||||
'*/**/*',
|
||||
async () => {
|
||||
throw new Error('Async Error');
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
const options = {};
|
||||
|
||||
const error = await runCreateNodesInParallel(
|
||||
configFiles,
|
||||
plugin,
|
||||
options,
|
||||
context
|
||||
).catch((e) => e);
|
||||
|
||||
expect(error).toMatchInlineSnapshot(
|
||||
`[AggregateCreateNodesError: Failed to create nodes]`
|
||||
);
|
||||
|
||||
expect(error.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[CreateNodesError: The "test" plugin threw an error while creating nodes from file1:],
|
||||
[CreateNodesError: The "test" plugin threw an error while creating nodes from file2:],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle sync errors', async () => {
|
||||
const plugin = {
|
||||
name: 'test',
|
||||
createNodes: [
|
||||
'*/**/*',
|
||||
() => {
|
||||
throw new Error('Sync Error');
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
const options = {};
|
||||
|
||||
const error = await runCreateNodesInParallel(
|
||||
configFiles,
|
||||
plugin,
|
||||
options,
|
||||
context
|
||||
).catch((e) => e);
|
||||
|
||||
expect(error).toMatchInlineSnapshot(
|
||||
`[AggregateCreateNodesError: Failed to create nodes]`
|
||||
);
|
||||
|
||||
expect(error.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[CreateNodesError: The "test" plugin threw an error while creating nodes from file1:],
|
||||
[CreateNodesError: The "test" plugin threw an error while creating nodes from file2:],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,12 @@ import type {
|
||||
LoadedNxPlugin,
|
||||
NormalizedPlugin,
|
||||
} from './internal-api';
|
||||
import type { CreateNodesContext, NxPlugin, NxPluginV2 } from './public-api';
|
||||
import {
|
||||
CreateNodesResult,
|
||||
type CreateNodesContext,
|
||||
type NxPlugin,
|
||||
type NxPluginV2,
|
||||
} from './public-api';
|
||||
import { AggregateCreateNodesError, CreateNodesError } from '../error-types';
|
||||
|
||||
export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 {
|
||||
@@ -49,7 +54,7 @@ export function normalizeNxPlugin(plugin: NxPlugin): NormalizedPlugin {
|
||||
}
|
||||
|
||||
export async function runCreateNodesInParallel(
|
||||
configFiles: string[],
|
||||
configFiles: readonly string[],
|
||||
plugin: NormalizedPlugin,
|
||||
options: unknown,
|
||||
context: CreateNodesContext
|
||||
@@ -59,39 +64,33 @@ export async function runCreateNodesInParallel(
|
||||
const errors: CreateNodesError[] = [];
|
||||
const results: CreateNodesResultWithContext[] = [];
|
||||
|
||||
const promises: Array<Promise<void>> = configFiles.map((file) => {
|
||||
const promises: Array<Promise<void>> = configFiles.map(async (file) => {
|
||||
performance.mark(`${plugin.name}:createNodes:${file} - start`);
|
||||
// Result is either static or a promise, using Promise.resolve lets us
|
||||
// handle both cases with same logic
|
||||
const value = Promise.resolve(
|
||||
plugin.createNodes[1](file, options, context)
|
||||
);
|
||||
return value
|
||||
.catch((e) => {
|
||||
performance.mark(`${plugin.name}:createNodes:${file} - end`);
|
||||
errors.push(
|
||||
new CreateNodesError({
|
||||
error: e,
|
||||
pluginName: plugin.name,
|
||||
file,
|
||||
})
|
||||
);
|
||||
return null;
|
||||
})
|
||||
.then((r) => {
|
||||
performance.mark(`${plugin.name}:createNodes:${file} - end`);
|
||||
performance.measure(
|
||||
`${plugin.name}:createNodes:${file}`,
|
||||
`${plugin.name}:createNodes:${file} - start`,
|
||||
`${plugin.name}:createNodes:${file} - end`
|
||||
);
|
||||
|
||||
// Existing behavior is to ignore null results of
|
||||
// createNodes function.
|
||||
if (r) {
|
||||
results.push({ ...r, file, pluginName: plugin.name });
|
||||
}
|
||||
});
|
||||
try {
|
||||
const value = await plugin.createNodes[1](file, options, context);
|
||||
if (value) {
|
||||
results.push({
|
||||
...value,
|
||||
file,
|
||||
pluginName: plugin.name,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(
|
||||
new CreateNodesError({
|
||||
error: e,
|
||||
pluginName: plugin.name,
|
||||
file,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
performance.mark(`${plugin.name}:createNodes:${file} - end`);
|
||||
performance.measure(
|
||||
`${plugin.name}:createNodes:${file}`,
|
||||
`${plugin.name}:createNodes:${file} - start`,
|
||||
`${plugin.name}:createNodes:${file} - end`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises).then(() => {
|
||||
|
||||
@@ -1,44 +1,33 @@
|
||||
import {
|
||||
readFileMapCache,
|
||||
readProjectGraphCache,
|
||||
writeCache,
|
||||
} from './nx-deps-cache';
|
||||
import {
|
||||
CreateDependenciesError,
|
||||
ProcessDependenciesError,
|
||||
ProcessProjectGraphError,
|
||||
buildProjectGraphUsingProjectFileMap,
|
||||
} from './build-project-graph';
|
||||
import { output } from '../utils/output';
|
||||
import { markDaemonAsDisabled, writeDaemonLogs } from '../daemon/tmp-dir';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { readNxJson } from '../config/nx-json';
|
||||
import { ProjectGraph } from '../config/project-graph';
|
||||
import { stripIndents } from '../utils/strip-indents';
|
||||
import {
|
||||
ProjectConfiguration,
|
||||
ProjectsConfigurations,
|
||||
} from '../config/workspace-json-project-json';
|
||||
import { daemonClient } from '../daemon/client/client';
|
||||
import { markDaemonAsDisabled, writeDaemonLogs } from '../daemon/tmp-dir';
|
||||
import { fileExists } from '../utils/fileutils';
|
||||
import { output } from '../utils/output';
|
||||
import { stripIndents } from '../utils/strip-indents';
|
||||
import { workspaceRoot } from '../utils/workspace-root';
|
||||
import { performance } from 'perf_hooks';
|
||||
import {
|
||||
CreateDependenciesError,
|
||||
buildProjectGraphUsingProjectFileMap,
|
||||
} from './build-project-graph';
|
||||
import {
|
||||
readFileMapCache,
|
||||
readProjectGraphCache,
|
||||
writeCache,
|
||||
} from './nx-deps-cache';
|
||||
|
||||
import { ProjectConfigurationsError, ProjectGraphError } from './error-types';
|
||||
import { loadNxPlugins } from './plugins/internal-api';
|
||||
import { ConfigurationResult } from './utils/project-configuration-utils';
|
||||
import {
|
||||
retrieveProjectConfigurations,
|
||||
retrieveWorkspaceFiles,
|
||||
} from './utils/retrieve-workspace-files';
|
||||
import { readNxJson } from '../config/nx-json';
|
||||
import {
|
||||
ConfigurationResult,
|
||||
ConfigurationSourceMaps,
|
||||
} from './utils/project-configuration-utils';
|
||||
import {
|
||||
CreateNodesError,
|
||||
MergeNodesError,
|
||||
ProjectConfigurationsError,
|
||||
ProjectsWithNoNameError,
|
||||
ProjectsWithConflictingNamesError,
|
||||
} from './error-types';
|
||||
import { DaemonProjectGraphError } from '../daemon/daemon-project-graph-error';
|
||||
import { loadNxPlugins, LoadedNxPlugin } from './plugins/internal-api';
|
||||
|
||||
/**
|
||||
* Synchronously reads the latest cached copy of the workspace's ProjectGraph.
|
||||
@@ -179,67 +168,6 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectGraphError extends Error {
|
||||
readonly #errors: Array<
|
||||
| CreateNodesError
|
||||
| MergeNodesError
|
||||
| ProjectsWithNoNameError
|
||||
| ProjectsWithConflictingNamesError
|
||||
| ProcessDependenciesError
|
||||
| ProcessProjectGraphError
|
||||
>;
|
||||
readonly #partialProjectGraph: ProjectGraph;
|
||||
readonly #partialSourceMaps: ConfigurationSourceMaps;
|
||||
|
||||
constructor(
|
||||
errors: Array<
|
||||
| CreateNodesError
|
||||
| MergeNodesError
|
||||
| ProjectsWithNoNameError
|
||||
| ProjectsWithConflictingNamesError
|
||||
| ProcessDependenciesError
|
||||
| ProcessProjectGraphError
|
||||
>,
|
||||
partialProjectGraph: ProjectGraph,
|
||||
partialSourceMaps: ConfigurationSourceMaps
|
||||
) {
|
||||
super(`Failed to process project graph.`);
|
||||
this.name = this.constructor.name;
|
||||
this.#errors = errors;
|
||||
this.#partialProjectGraph = partialProjectGraph;
|
||||
this.#partialSourceMaps = partialSourceMaps;
|
||||
this.stack = `${this.message}\n ${errors
|
||||
.map((error) => error.stack.split('\n').join('\n '))
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon cannot throw errors which contain methods as they are not serializable.
|
||||
*
|
||||
* This method creates a new {@link ProjectGraphError} from a {@link DaemonProjectGraphError} with the methods based on the same serialized data.
|
||||
*/
|
||||
static fromDaemonProjectGraphError(e: DaemonProjectGraphError) {
|
||||
return new ProjectGraphError(e.errors, e.projectGraph, e.sourceMaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets the partial project graph despite the errors which occured.
|
||||
* This partial project graph may be missing nodes, properties of nodes, or dependencies.
|
||||
* This is useful mostly for visualization/debugging. It should not be used for running tasks.
|
||||
*/
|
||||
getPartialProjectGraph() {
|
||||
return this.#partialProjectGraph;
|
||||
}
|
||||
|
||||
getPartialSourcemaps() {
|
||||
return this.#partialSourceMaps;
|
||||
}
|
||||
|
||||
getErrors() {
|
||||
return this.#errors;
|
||||
}
|
||||
}
|
||||
|
||||
function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
|
||||
if (opts.exitOnError) {
|
||||
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
|
||||
|
||||
@@ -13,9 +13,8 @@ export function createProjectRootMappingsFromProjectConfigurations(
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
) {
|
||||
const projectRootMappings: ProjectRootMappings = new Map();
|
||||
for (const projectName of Object.keys(projects)) {
|
||||
const root = projects[projectName].root;
|
||||
projectRootMappings.set(normalizeProjectRoot(root), projectName);
|
||||
for (const { name, root } of Object.values(projects)) {
|
||||
projectRootMappings.set(normalizeProjectRoot(root), name);
|
||||
}
|
||||
return projectRootMappings;
|
||||
}
|
||||
|
||||
@@ -5,34 +5,41 @@ import {
|
||||
TargetConfiguration,
|
||||
} from '../../config/workspace-json-project-json';
|
||||
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||
import { resolveNxTokensInOptions } from '../utils/project-configuration-utils';
|
||||
import {
|
||||
readProjectConfigurationsFromRootMap,
|
||||
resolveNxTokensInOptions,
|
||||
} from '../utils/project-configuration-utils';
|
||||
import { CreateDependenciesContext } from '../plugins';
|
||||
|
||||
export async function normalizeProjectNodes(
|
||||
ctx: CreateDependenciesContext,
|
||||
{ projects }: CreateDependenciesContext,
|
||||
builder: ProjectGraphBuilder
|
||||
) {
|
||||
const toAdd = [];
|
||||
// Sorting projects by name to make sure that the order of projects in the graph is deterministic.
|
||||
// This is important to ensure that expanded properties referencing projects (e.g. implicit dependencies)
|
||||
// are also deterministic, and thus don't cause the calculated project configuration hash to shift.
|
||||
const projects = Object.keys(ctx.projects).sort();
|
||||
const sortedProjectNames = Object.keys(projects).sort();
|
||||
|
||||
// Used for expanding implicit dependencies (e.g. `@proj/*` or `tag:foo`)
|
||||
const partialProjectGraphNodes = projects.reduce((graph, project) => {
|
||||
const projectConfiguration = ctx.projects[project];
|
||||
graph[project] = {
|
||||
name: project,
|
||||
type: projectConfiguration.projectType === 'library' ? 'lib' : 'app', // missing fallback to `e2e`
|
||||
data: {
|
||||
...projectConfiguration,
|
||||
},
|
||||
};
|
||||
return graph;
|
||||
}, {} as Record<string, ProjectGraphProjectNode>);
|
||||
const partialProjectGraphNodes = sortedProjectNames.reduce(
|
||||
(graph, project) => {
|
||||
const projectConfiguration = projects[project];
|
||||
graph[project] = {
|
||||
name: project,
|
||||
type: projectConfiguration.projectType === 'library' ? 'lib' : 'app', // missing fallback to `e2e`
|
||||
data: {
|
||||
...projectConfiguration,
|
||||
},
|
||||
};
|
||||
return graph;
|
||||
},
|
||||
{} as Record<string, ProjectGraphProjectNode>
|
||||
);
|
||||
|
||||
for (const key of projects) {
|
||||
const p = ctx.projects[key];
|
||||
const toAdd = [];
|
||||
|
||||
for (const key of sortedProjectNames) {
|
||||
const p = projects[key];
|
||||
|
||||
p.implicitDependencies = normalizeImplicitDependencies(
|
||||
key,
|
||||
@@ -49,7 +56,7 @@ export async function normalizeProjectNodes(
|
||||
? 'e2e'
|
||||
: 'app'
|
||||
: 'lib';
|
||||
const tags = ctx.projects?.[key]?.tags || [];
|
||||
const tags = p.tags || [];
|
||||
|
||||
toAdd.push({
|
||||
name: key,
|
||||
|
||||
@@ -477,7 +477,7 @@ describe('project-configuration-utils', () => {
|
||||
['dummy', 'dummy.ts']
|
||||
);
|
||||
|
||||
expect(rootMap.get('libs/lib-a').targets.build.metadata).toEqual({
|
||||
expect(rootMap['libs/lib-a'].targets.build.metadata).toEqual({
|
||||
description: 'do stuff',
|
||||
technologies: ['tech'],
|
||||
});
|
||||
@@ -530,7 +530,7 @@ describe('project-configuration-utils', () => {
|
||||
['dummy', 'dummy.ts']
|
||||
);
|
||||
|
||||
expect(rootMap.get('libs/lib-a').targets.build.metadata).toEqual({
|
||||
expect(rootMap['libs/lib-a'].targets.build.metadata).toEqual({
|
||||
description: 'do cool stuff',
|
||||
technologies: ['tech', 'tech2'],
|
||||
});
|
||||
@@ -566,7 +566,7 @@ describe('project-configuration-utils', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(rootMap.get('libs/lib-a')).toMatchInlineSnapshot(`
|
||||
expect(rootMap['libs/lib-a']).toMatchInlineSnapshot(`
|
||||
{
|
||||
"name": "lib-a",
|
||||
"root": "libs/lib-a",
|
||||
@@ -660,7 +660,7 @@ describe('project-configuration-utils', () => {
|
||||
newTarget: newTargetConfiguration,
|
||||
},
|
||||
});
|
||||
const merged = rootMap.get('libs/lib-a');
|
||||
const merged = rootMap['libs/lib-a'];
|
||||
expect(merged.targets['existingTarget']).toEqual(
|
||||
existingTargetConfiguration
|
||||
);
|
||||
@@ -719,7 +719,7 @@ describe('project-configuration-utils', () => {
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
const { targets } = rootMap.get('libs/lib-a');
|
||||
const { targets } = rootMap['libs/lib-a'];
|
||||
expect(targets.build).toBeUndefined();
|
||||
// cwd was merged in, and ONLY_MODIFIES_EXISTING_TARGET was removed
|
||||
expect(targets.echo).toMatchInlineSnapshot(`
|
||||
@@ -747,8 +747,8 @@ describe('project-configuration-utils', () => {
|
||||
tags: ['b', 'c'],
|
||||
implicitDependencies: ['lib-c', '!lib-b'],
|
||||
});
|
||||
expect(rootMap.get('libs/lib-a').tags).toEqual(['a', 'b', 'c']);
|
||||
expect(rootMap.get('libs/lib-a').implicitDependencies).toEqual([
|
||||
expect(rootMap['libs/lib-a'].tags).toEqual(['a', 'b', 'c']);
|
||||
expect(rootMap['libs/lib-a'].implicitDependencies).toEqual([
|
||||
'lib-b',
|
||||
'lib-c',
|
||||
'!lib-b',
|
||||
@@ -779,7 +779,7 @@ describe('project-configuration-utils', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(rootMap.get('libs/lib-a').generators).toMatchInlineSnapshot(`
|
||||
expect(rootMap['libs/lib-a'].generators).toMatchInlineSnapshot(`
|
||||
{
|
||||
"@nx/angular:component": {
|
||||
"flat": true,
|
||||
@@ -814,7 +814,7 @@ describe('project-configuration-utils', () => {
|
||||
production: ['{projectRoot}/**/*.prod.ts'],
|
||||
},
|
||||
});
|
||||
expect(rootMap.get('libs/lib-a').namedInputs).toMatchInlineSnapshot(`
|
||||
expect(rootMap['libs/lib-a'].namedInputs).toMatchInlineSnapshot(`
|
||||
{
|
||||
"another": [
|
||||
"{projectRoot}/**/*.ts",
|
||||
@@ -847,7 +847,7 @@ describe('project-configuration-utils', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(rootMap.get('libs/lib-a').release).toMatchInlineSnapshot(`
|
||||
expect(rootMap['libs/lib-a'].release).toMatchInlineSnapshot(`
|
||||
{
|
||||
"version": {
|
||||
"generatorOptions": {
|
||||
@@ -885,7 +885,7 @@ describe('project-configuration-utils', () => {
|
||||
['dummy', 'dummy.ts']
|
||||
);
|
||||
|
||||
expect(rootMap.get('libs/lib-a').metadata).toEqual({
|
||||
expect(rootMap['libs/lib-a'].metadata).toEqual({
|
||||
technologies: ['technology'],
|
||||
targetGroups: {
|
||||
group1: ['target1', 'target2'],
|
||||
@@ -927,7 +927,7 @@ describe('project-configuration-utils', () => {
|
||||
['dummy', 'dummy.ts']
|
||||
);
|
||||
|
||||
expect(rootMap.get('libs/lib-a').metadata).toEqual({
|
||||
expect(rootMap['libs/lib-a'].metadata).toEqual({
|
||||
technologies: ['technology1', 'technology2'],
|
||||
});
|
||||
expect(sourceMap['libs/lib-a']).toMatchObject({
|
||||
@@ -971,7 +971,7 @@ describe('project-configuration-utils', () => {
|
||||
['dummy', 'dummy.ts']
|
||||
);
|
||||
|
||||
expect(rootMap.get('libs/lib-a').metadata).toEqual({
|
||||
expect(rootMap['libs/lib-a'].metadata).toEqual({
|
||||
targetGroups: {
|
||||
group1: ['target1', 'target2'],
|
||||
},
|
||||
@@ -1110,7 +1110,7 @@ describe('project-configuration-utils', () => {
|
||||
});
|
||||
|
||||
it('should merge root level properties', () => {
|
||||
const rootMap = new Map();
|
||||
const rootMap: Record<string, ProjectConfiguration> = {};
|
||||
const sourceMap: ConfigurationSourceMaps = {
|
||||
'libs/lib-a': {},
|
||||
};
|
||||
@@ -1527,6 +1527,25 @@ describe('project-configuration-utils', () => {
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if one target specifies a command, and the other specifies commands', () => {
|
||||
expect(
|
||||
isCompatibleTarget(
|
||||
{
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'echo',
|
||||
},
|
||||
},
|
||||
{
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
commands: ['echo', 'other'],
|
||||
},
|
||||
}
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProjectConfigurations', () => {
|
||||
@@ -1563,12 +1582,12 @@ describe('project-configuration-utils', () => {
|
||||
);
|
||||
|
||||
expect(projectConfigurations.projects).toEqual({
|
||||
a: {
|
||||
'libs/a': {
|
||||
name: 'a',
|
||||
root: 'libs/a',
|
||||
tags: ['fake-lib'],
|
||||
},
|
||||
b: {
|
||||
'libs/b': {
|
||||
name: 'b',
|
||||
root: 'libs/b',
|
||||
tags: ['fake-lib'],
|
||||
@@ -1590,7 +1609,7 @@ describe('project-configuration-utils', () => {
|
||||
);
|
||||
|
||||
expect(projectConfigurations.projects).toEqual({
|
||||
a: {
|
||||
'libs/a': {
|
||||
name: 'a',
|
||||
root: 'libs/a',
|
||||
tags: ['fake-lib'],
|
||||
@@ -1612,7 +1631,7 @@ describe('project-configuration-utils', () => {
|
||||
);
|
||||
|
||||
expect(projectConfigurations.projects).toEqual({
|
||||
a: {
|
||||
'libs/a': {
|
||||
name: 'a',
|
||||
root: 'libs/a',
|
||||
tags: ['fake-lib'],
|
||||
@@ -1623,10 +1642,10 @@ describe('project-configuration-utils', () => {
|
||||
});
|
||||
|
||||
class RootMapBuilder {
|
||||
private rootMap: Map<string, ProjectConfiguration> = new Map();
|
||||
private rootMap: Record<string, ProjectConfiguration> = {};
|
||||
|
||||
addProject(p: ProjectConfiguration) {
|
||||
this.rootMap.set(p.root, p);
|
||||
this.rootMap[p.root] = p;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export type ConfigurationSourceMaps = Record<
|
||||
>;
|
||||
|
||||
export function mergeProjectConfigurationIntoRootMap(
|
||||
projectRootMap: Map<string, ProjectConfiguration>,
|
||||
projectRootMap: Record<string, ProjectConfiguration>,
|
||||
project: ProjectConfiguration & {
|
||||
targets?: Record<
|
||||
string,
|
||||
@@ -57,13 +57,13 @@ export function mergeProjectConfigurationIntoRootMap(
|
||||
}
|
||||
const sourceMap = configurationSourceMaps?.[project.root];
|
||||
|
||||
let matchingProject = projectRootMap.get(project.root);
|
||||
let matchingProject = projectRootMap[project.root];
|
||||
|
||||
if (!matchingProject) {
|
||||
projectRootMap.set(project.root, {
|
||||
projectRootMap[project.root] = {
|
||||
root: project.root,
|
||||
});
|
||||
matchingProject = projectRootMap.get(project.root);
|
||||
};
|
||||
matchingProject = projectRootMap[project.root];
|
||||
if (sourceMap) {
|
||||
sourceMap[`root`] = sourceInformation;
|
||||
}
|
||||
@@ -220,10 +220,8 @@ export function mergeProjectConfigurationIntoRootMap(
|
||||
}
|
||||
}
|
||||
|
||||
projectRootMap.set(
|
||||
updatedProjectConfiguration.root,
|
||||
updatedProjectConfiguration
|
||||
);
|
||||
projectRootMap[updatedProjectConfiguration.root] =
|
||||
updatedProjectConfiguration;
|
||||
}
|
||||
|
||||
function mergeMetadata<T = ProjectMetadata | TargetMetadata>(
|
||||
@@ -302,10 +300,29 @@ function mergeMetadata<T = ProjectMetadata | TargetMetadata>(
|
||||
}
|
||||
|
||||
export type ConfigurationResult = {
|
||||
projects: Record<string, ProjectConfiguration>;
|
||||
/**
|
||||
* A map of project configurations, keyed by project root.
|
||||
*/
|
||||
projects: {
|
||||
[projectRoot: string]: ProjectConfiguration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Node Name -> Node info
|
||||
*/
|
||||
externalNodes: Record<string, ProjectGraphExternalNode>;
|
||||
|
||||
/**
|
||||
* Project Root -> Project Name
|
||||
*/
|
||||
projectRootMap: Record<string, string>;
|
||||
|
||||
sourceMaps: ConfigurationSourceMaps;
|
||||
|
||||
/**
|
||||
* The list of files that were used to create project configurations
|
||||
*/
|
||||
matchingProjectFiles: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -388,7 +405,7 @@ export async function createProjectConfigurations(
|
||||
|
||||
return Promise.all(results).then((results) => {
|
||||
performance.mark('createNodes:merge - start');
|
||||
const projectRootMap: Map<string, ProjectConfiguration> = new Map();
|
||||
const projectRootMap: Record<string, ProjectConfiguration> = {};
|
||||
const externalNodes: Record<string, ProjectGraphExternalNode> = {};
|
||||
const configurationSourceMaps: Record<
|
||||
string,
|
||||
@@ -434,15 +451,18 @@ export async function createProjectConfigurations(
|
||||
Object.assign(externalNodes, pluginExternalNodes);
|
||||
}
|
||||
|
||||
let projects: Record<string, ProjectConfiguration>;
|
||||
try {
|
||||
projects = readProjectConfigurationsFromRootMap(projectRootMap);
|
||||
// We still call this just to assert that the root map
|
||||
// only contains valid project names. This is a safety check.
|
||||
//
|
||||
// The signature itself can't be changed as we need it to return
|
||||
// project configurations for use in devkit.
|
||||
readProjectConfigurationsFromRootMap(projectRootMap);
|
||||
} catch (e) {
|
||||
if (
|
||||
isProjectsWithNoNameError(e) ||
|
||||
isProjectsWithConflictingNamesError(e)
|
||||
) {
|
||||
projects = e.projects;
|
||||
errors.push(e);
|
||||
} else {
|
||||
throw e;
|
||||
@@ -467,24 +487,26 @@ export async function createProjectConfigurations(
|
||||
|
||||
if (errors.length === 0) {
|
||||
return {
|
||||
projects,
|
||||
projects: projectRootMap,
|
||||
externalNodes,
|
||||
projectRootMap: rootMap,
|
||||
sourceMaps: configurationSourceMaps,
|
||||
matchingProjectFiles: projectFiles,
|
||||
};
|
||||
} else {
|
||||
throw new ProjectConfigurationsError(errors, {
|
||||
projects,
|
||||
projects: projectRootMap,
|
||||
externalNodes,
|
||||
projectRootMap: rootMap,
|
||||
sourceMaps: configurationSourceMaps,
|
||||
matchingProjectFiles: projectFiles,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function readProjectConfigurationsFromRootMap(
|
||||
projectRootMap: Map<string, ProjectConfiguration>
|
||||
projectRootMap: Record<string, ProjectConfiguration>
|
||||
) {
|
||||
const projects: Record<string, ProjectConfiguration> = {};
|
||||
// If there are projects that have the same name, that is an error.
|
||||
@@ -493,7 +515,8 @@ export function readProjectConfigurationsFromRootMap(
|
||||
const conflicts = new Map<string, string[]>();
|
||||
const projectRootsWithNoName: string[] = [];
|
||||
|
||||
for (const [root, configuration] of projectRootMap.entries()) {
|
||||
for (const root in projectRootMap) {
|
||||
const configuration = projectRootMap[root];
|
||||
// We're setting `// targets` as a comment `targets` is empty due to Project Crystal.
|
||||
// Strip it before returning configuration for usage.
|
||||
if (configuration['// targets']) delete configuration['// targets'];
|
||||
@@ -554,7 +577,7 @@ export function mergeTargetConfigurations(
|
||||
|
||||
// Target is "compatible", e.g. executor is defined only once or is the same
|
||||
// in both places. This means that it is likely safe to merge
|
||||
const isCompatible = isCompatibleTarget(baseTargetProperties, target);
|
||||
const isCompatible = isCompatibleTarget(baseTarget ?? {}, target);
|
||||
|
||||
// If the targets are not compatible, we would normally overwrite the old target
|
||||
// with the new one. However, we have a special case for targets that have the
|
||||
@@ -779,9 +802,10 @@ export function readTargetDefaultsForTarget(
|
||||
}
|
||||
}
|
||||
|
||||
function createRootMap(projectRootMap: Map<string, ProjectConfiguration>) {
|
||||
function createRootMap(projectRootMap: Record<string, ProjectConfiguration>) {
|
||||
const map: Record<string, string> = {};
|
||||
for (const [projectRoot, { name: projectName }] of projectRootMap) {
|
||||
for (const projectRoot in projectRootMap) {
|
||||
const projectName = projectRootMap[projectRoot].name;
|
||||
map[projectRoot] = projectName;
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -7,9 +7,11 @@ import { invokeTasksRunner } from './run-command';
|
||||
import { InvokeRunnerTerminalOutputLifeCycle } from './life-cycles/invoke-runner-terminal-output-life-cycle';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { getOutputs } from './utils';
|
||||
import { loadRootEnvFiles } from '../utils/dotenv';
|
||||
|
||||
export async function initTasksRunner(nxArgs: NxArgs) {
|
||||
performance.mark('init-local');
|
||||
loadRootEnvFiles();
|
||||
workspaceConfigurationCheck();
|
||||
const nxJson = readNxJson();
|
||||
if (nxArgs.verbose) {
|
||||
|
||||
+4
@@ -36,6 +36,10 @@ export async function createRunManyDynamicOutputRenderer({
|
||||
overrides: Record<string, unknown>;
|
||||
}): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
|
||||
cliCursor.hide();
|
||||
// Show the cursor again after the process exits
|
||||
process.on('exit', () => {
|
||||
cliCursor.show();
|
||||
});
|
||||
let resolveRenderIsDonePromise: (value: void) => void;
|
||||
const renderIsDone = new Promise<void>(
|
||||
(resolve) => (resolveRenderIsDonePromise = resolve)
|
||||
|
||||
@@ -47,6 +47,10 @@ export async function createRunOneDynamicOutputRenderer({
|
||||
overrides: Record<string, unknown>;
|
||||
}): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
|
||||
cliCursor.hide();
|
||||
// Show the cursor again after the process exits
|
||||
process.on('exit', () => {
|
||||
cliCursor.show();
|
||||
});
|
||||
let resolveRenderIsDonePromise: (value: void) => void;
|
||||
const renderIsDone = new Promise<void>(
|
||||
(resolve) => (resolveRenderIsDonePromise = resolve)
|
||||
|
||||
@@ -196,6 +196,13 @@ function supportedPtyPlatform() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Re-enable Windows support when it's stable
|
||||
// Currently, there's an issue with control chars.
|
||||
// See: https://github.com/nrwl/nx/issues/22358
|
||||
if (process.env.NX_WINDOWS_PTY_SUPPORT !== 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let windowsVersion = os.release().split('.');
|
||||
let windowsBuild = windowsVersion[2];
|
||||
|
||||
|
||||
@@ -393,53 +393,69 @@ export class TaskOrchestrator {
|
||||
targetConfiguration.executor === 'nx:run-commands' &&
|
||||
!shouldPrefix
|
||||
) {
|
||||
const { schema } = getExecutorForTask(task, this.projectGraph);
|
||||
const isRunOne = this.initiatingProject != null;
|
||||
const combinedOptions = combineOptionsForExecutor(
|
||||
task.overrides,
|
||||
task.target.configuration ?? targetConfiguration.defaultConfiguration,
|
||||
targetConfiguration,
|
||||
schema,
|
||||
task.target.project,
|
||||
relative(task.projectRoot ?? workspaceRoot, process.cwd()),
|
||||
process.env.NX_VERBOSE_LOGGING === 'true'
|
||||
);
|
||||
if (combinedOptions.env) {
|
||||
env = {
|
||||
...env,
|
||||
...combinedOptions.env,
|
||||
};
|
||||
}
|
||||
if (streamOutput) {
|
||||
const args = getPrintableCommandArgsForTask(task);
|
||||
output.logCommand(args.join(' '));
|
||||
}
|
||||
const { success, terminalOutput } = await runCommandsImpl(
|
||||
{
|
||||
...combinedOptions,
|
||||
env,
|
||||
usePty: isRunOne && !this.tasksSchedule.hasTasks(),
|
||||
streamOutput,
|
||||
},
|
||||
{
|
||||
root: workspaceRoot, // only root is needed in runCommandsImpl
|
||||
} as any
|
||||
);
|
||||
try {
|
||||
const { schema } = getExecutorForTask(task, this.projectGraph);
|
||||
const isRunOne = this.initiatingProject != null;
|
||||
const combinedOptions = combineOptionsForExecutor(
|
||||
task.overrides,
|
||||
task.target.configuration ??
|
||||
targetConfiguration.defaultConfiguration,
|
||||
targetConfiguration,
|
||||
schema,
|
||||
task.target.project,
|
||||
relative(task.projectRoot ?? workspaceRoot, process.cwd()),
|
||||
process.env.NX_VERBOSE_LOGGING === 'true'
|
||||
);
|
||||
if (combinedOptions.env) {
|
||||
env = {
|
||||
...env,
|
||||
...combinedOptions.env,
|
||||
};
|
||||
}
|
||||
if (streamOutput) {
|
||||
const args = getPrintableCommandArgsForTask(task);
|
||||
output.logCommand(args.join(' '));
|
||||
}
|
||||
const { success, terminalOutput } = await runCommandsImpl(
|
||||
{
|
||||
...combinedOptions,
|
||||
env,
|
||||
usePty: isRunOne && !this.tasksSchedule.hasTasks(),
|
||||
streamOutput,
|
||||
},
|
||||
{
|
||||
root: workspaceRoot, // only root is needed in runCommandsImpl
|
||||
} as any
|
||||
);
|
||||
|
||||
const status = success ? 'success' : 'failure';
|
||||
if (!streamOutput) {
|
||||
this.options.lifeCycle.printTaskTerminalOutput(
|
||||
const status = success ? 'success' : 'failure';
|
||||
if (!streamOutput) {
|
||||
this.options.lifeCycle.printTaskTerminalOutput(
|
||||
task,
|
||||
status,
|
||||
terminalOutput
|
||||
);
|
||||
}
|
||||
writeFileSync(temporaryOutputPath, terminalOutput);
|
||||
results.push({
|
||||
task,
|
||||
status,
|
||||
terminalOutput
|
||||
);
|
||||
terminalOutput,
|
||||
});
|
||||
} catch (e) {
|
||||
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
||||
console.error(e);
|
||||
} else {
|
||||
console.error(e.message);
|
||||
}
|
||||
const terminalOutput = e.stack ?? e.message ?? '';
|
||||
writeFileSync(temporaryOutputPath, terminalOutput);
|
||||
results.push({
|
||||
task,
|
||||
status: 'failure',
|
||||
terminalOutput,
|
||||
});
|
||||
}
|
||||
writeFileSync(temporaryOutputPath, terminalOutput);
|
||||
results.push({
|
||||
task,
|
||||
status,
|
||||
terminalOutput,
|
||||
});
|
||||
} else {
|
||||
// cache prep
|
||||
const { code, terminalOutput } = await this.runTaskInForkedProcess(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user