Compare commits

...

43 Commits

Author SHA1 Message Date
Isaac Mann e81c5d3466 docs(core): contact dpe for eu cluster (#26753)
Adds note explaining that enterprise customers need to work with their
DPE to use the EU hosted version of Nx Cloud
2024-06-28 14:00:10 -04:00
Isaac Mann 8d9a62d9de feat(nx-dev): website-18 noindex (#26333)
Add No_NEXT_PUBLIC_NO_INDEX environment variable to turn off indexing

Co-authored-by: Nicholas Cunningham <ndcunningham@gmail.com>
2024-06-03 13:27:10 -04:00
Jason Jean 91eae77a51 fix(core): fix affected detection for inputs after named inputs (#23354)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

## Current Behavior
<!-- This is the behavior we have today -->

Inputs after named inputs are not considered for affected.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

Inputs after named inputs are considered for affected...

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

(cherry picked from commit 722b2d0fac)
2024-05-15 17:18:49 -04:00
Leosvel Pérez Espinosa 5067887cdd cleanup(linter): parallelize projects processing and better dedupe/ignore projects and files in eslint plugin (#23011)
(cherry picked from commit e78776a53e)
2024-04-25 19:10:11 -04:00
Craigory Coppola 76a62bd0d3 fix(core): ensure create nodes functions are properly parallelized (#23005)
(cherry picked from commit 0fd6d23e3f)
2024-04-25 19:10:07 -04:00
Craigory Coppola 3771061076 fix(misc): perf logging shouldn't be enabled twice (#23012)
(cherry picked from commit c6fe9696fd)
2024-04-25 15:35:32 -04:00
Nicholas Cunningham b856175d34 fix(node): e2e target fails out of the box (#22987)
(cherry picked from commit 9cf8c29d27)
2024-04-25 15:35:27 -04:00
Emily Xiong 3369237b25 fix(gradle): get gradlew path with projectRoot joins workspaceRoot (#22988)
(cherry picked from commit cec57c4951)
2024-04-25 15:35:15 -04:00
Jack Hsu dcc14e3f56 fix(misc): fix publish script (#22981)
(cherry picked from commit 8cd326e71a)
2024-04-25 15:35:11 -04:00
Jason Jean 8398e6d980 fix(core): handle created directories when watching on linux (#22980)
Co-authored-by: Jonathan Cammisuli <jon@cammisuli.ca>
(cherry picked from commit 12dd8722ef)
2024-04-25 15:35:08 -04:00
Emily Xiong 4afbda5759 fix(gradle): should skip println in project report (#22862)
(cherry picked from commit df87a5ec47)
2024-04-25 15:35:04 -04:00
Richard Roozenboom afea39efe2 fix(core): regression register ts transpiler for local plugin (#22964)
(cherry picked from commit abaad324e6)
2024-04-25 15:34:27 -04:00
Emily Xiong 49bc160167 fix(gradle): change gradle command to be relative path (#22963)
(cherry picked from commit 99e5c869b3)
2024-04-25 15:34:23 -04:00
Leosvel Pérez Espinosa 9a48b69cac fix(storybook): handle inherited config correctly when identifying the framework used for inferred tasks (#22953)
(cherry picked from commit 6cab4c9a1b)
2024-04-25 15:34:21 -04:00
Craigory Coppola ccddf0ec92 fix(core): move a few api points to return root maps directly (#22949)
(cherry picked from commit 355d5c593d)
2024-04-25 15:34:18 -04:00
Jason Jean 0b03198cb4 fix(repo): downgrade to macos-13 in publish workflow (#22961)
(cherry picked from commit f76b8c63c1)
2024-04-25 15:34:14 -04:00
Leosvel Pérez Espinosa 2d4646d30c fix(linter): do not infer lint tasks for projects without files to lint (#22944)
(cherry picked from commit 1e4324fbce)
2024-04-25 15:33:18 -04:00
Jason Jean 9b5a1cbd4f fix(core): fix exclude for empty array (#22951)
(cherry picked from commit 8ded3807ac)
2024-04-25 15:33:10 -04:00
Jonathan Cammisuli f07ac05d62 fix(core): handle events that do not have paths (#22947)
(cherry picked from commit 3d9bd16ce0)
2024-04-25 15:33:07 -04:00
MaxKless c5b0aea60c fix(graph): don't listen to system theme changes in console (#22938)
(cherry picked from commit b37bfdb1af)
2024-04-22 22:31:43 -04:00
Emily Xiong 397da97e40 feat(core): add root level forwardAllArgs (#22753)
(cherry picked from commit c3a3d1b0a6)
2024-04-22 22:31:32 -04:00
Colum Ferry 3db58e12fa fix(core): workspace context glob respects exclude (#22939)
(cherry picked from commit 6ef17d2b56)
2024-04-22 22:31:22 -04:00
Miroslav Jonaš ea583069df fix(core): fix pnpm install order on ci workflows (#22580)
(cherry picked from commit 705baa7dfd)
2024-04-22 22:31:18 -04:00
Craigory Coppola 72fe71a1a2 fix(core): different commands should not be considered compatible targets (#22863)
(cherry picked from commit c49c07d926)
2024-04-22 22:31:14 -04:00
Jason Jean 581ede1486 fix(core): fix cursor being hidden and process shutdown for ctrl c (#22895)
(cherry picked from commit 9ca53b7153)
2024-04-19 20:44:26 -04:00
Craigory Coppola f4ea297b7b fix(misc): don't clear node_modules require cache (#22907)
(cherry picked from commit 80b55144db)
2024-04-19 20:44:21 -04:00
Craigory Coppola 39f8713dc9 fix(misc): mark migration for escaping env vars as skipped in nx repair (#22916)
(cherry picked from commit 82be2aebe5)
2024-04-19 20:44:19 -04:00
Craigory Coppola 645e2d8231 fix(core): disable pty on windows until stable (#22910)
(cherry picked from commit 9a0b70de69)
2024-04-19 20:44:16 -04:00
Colum Ferry 7a9a7799a0 fix(vue): do not add verbatimImportSyntax to tsconfig (#22905)
(cherry picked from commit 75234ae1b5)
2024-04-19 20:44:10 -04:00
Leosvel Pérez Espinosa 8bdedc82e0 fix(angular): fix loading postcss configuration in ng-packagr executors (#22900)
(cherry picked from commit 397276da04)
2024-04-19 20:44:05 -04:00
Jason Jean fdfa18b1ba fix(core): handle plugin errors from isolation correctly (#22890)
(cherry picked from commit 08f64211ec)
2024-04-19 20:44:02 -04:00
Craigory Coppola 9e32083ce8 fix(misc): add --verbose support to nx graph (#22889)
(cherry picked from commit a927e935a9)
2024-04-19 20:44:00 -04:00
Jason Jean 792092255f fix(testing): bust require cache in jest plugin so configs reload (#22893)
(cherry picked from commit 595ea21424)
2024-04-19 20:43:57 -04:00
Craigory Coppola 6ab9ae3626 fix(core): group command exit listeners to avoid warning (#22892)
(cherry picked from commit e2cee00767)
2024-04-19 20:43:53 -04:00
Jason Jean b633c0409c fix(testing): fix jest ci target names (#22858)
(cherry picked from commit 25f598ffbe)
2024-04-18 10:20:16 -04:00
Jason Jean becd93d923 fix(core): fix hashing of external dependencies (#22865)
(cherry picked from commit 2d20ad819b)
2024-04-18 10:20:12 -04:00
MaxKless d21fd019dd feat(core): load native files from tmp location instead of node_modules (#22648)
(cherry picked from commit da1808d36c)
2024-04-18 10:20:03 -04:00
Leosvel Pérez Espinosa 7ac5ca5f16 fix(core): forward args provided to the nx add command to the invoked init generator (#22855)
(cherry picked from commit 125c1d2786)
2024-04-18 10:19:47 -04:00
Jason Jean ed39d8cf36 fix(core): handle schema validation errors running commands directly (#22864)
(cherry picked from commit 7f0dc268a0)
2024-04-18 10:19:44 -04:00
Kyle Cannon aa1fdf66ad fix(bundling): handle circular dependencies in @nx/esbuild getExtraDependencies (#22644)
(cherry picked from commit e549ea2b58)
2024-04-18 10:19:40 -04:00
Craigory Coppola a0b4ac14ff fix(nx-cloud): ensure root .env files are loaded during dte (#22859)
(cherry picked from commit edc7931330)
2024-04-18 10:19:36 -04:00
Craigory Coppola e981d11e8e fix(core): keep plugin workers until main process shutdown (#22860)
(cherry picked from commit e7c2ebdd98)
2024-04-18 10:19:31 -04:00
Colum Ferry 6afcdf158b fix(core): load config util supports absolute paths on windows (#22837)
(cherry picked from commit 82145e7d02)
2024-04-18 10:19:11 -04:00
118 changed files with 2637 additions and 1337 deletions
+4
View File
@@ -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."
}
]
}
+15 -7
View File
@@ -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
View File
@@ -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
+6
View File
@@ -159,6 +159,12 @@ Type: `boolean`
Untracked changes
### verbose
Type: `boolean`
Prints additional information about the commands (e.g., stack traces)
### version
Type: `boolean`
+2 -2
View File
@@ -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`
![Okta 4](/nx-cloud/enterprise/on-premise/images/saml/okta_4_public.png)
{% 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 %}
![Okta 4](/nx-cloud/enterprise/on-premise/images/saml/okta_4_public.png)
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.
+3 -1
View File
@@ -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.
+6
View File
@@ -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' : ''}`,
+17
View File
@@ -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;
+3
View File
@@ -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',
+9 -2
View File
@@ -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'],
+4
View File
@@ -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
+4 -1
View File
@@ -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');
+33 -3
View File
@@ -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 = {
+19 -6
View File
@@ -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];
+230 -133
View File
@@ -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
+92 -29
View File
@@ -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);
}
+5 -20
View File
@@ -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';
+152
View File
@@ -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,
},
},
}
`);
});
});
+7 -2
View File
@@ -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
+3 -6
View File
@@ -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']);
});
});
+39 -17
View File
@@ -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,
+13 -4
View File
@@ -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] = {
+3 -41
View File
@@ -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(
+4
View File
@@ -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
View File
@@ -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.`,
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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": {
+4
View File
@@ -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', {
@@ -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',
+1 -1
View File
@@ -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',
+19
View File
@@ -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();
+4 -2
View File
@@ -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,
+5 -4
View File
@@ -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);
+3 -18
View File
@@ -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} }`;
}
+1
View File
@@ -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
+3 -1
View File
@@ -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)]
+1 -4
View File
@@ -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
View File
@@ -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;
+268
View File
@@ -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>,
}
+30 -29
View File
@@ -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()),
},
+7 -8
View File
@@ -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,
};
+120 -58
View File
@@ -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),
}
+5 -6
View File
@@ -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)
}))
}
@@ -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',
},
},
};
@@ -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,
+92 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
},
};
}
},
+45 -40
View File
@@ -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:],
]
`);
});
});
+33 -34
View File
@@ -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(() => {
+18 -90
View File
@@ -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) {
@@ -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