Compare commits

...

16 Commits

Author SHA1 Message Date
Craigory Coppola 068b066b50 fix(core): handleErrors should display error cause if it exists (#27886)
Some error messages are not displaying properly, as they pass their
original message as a cause. While `node` supports this, our
`handleErrors` function was not displaying error causes.

```
"Failed to process project graph. Run "nx reset" to fix this. Please report the issue if you keep seeing it.
            CreateMetadataError: The "test-plugin" plugin threw an error while creating metadata: cause message
            at /Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.spec.ts:17:29
            at handleErrors (/Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.ts:11:26)
            at Object.<anonymous> (/Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.spec.ts:15:23)
            at Promise.then.completed (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/utils.js:298:28)
            at new Promise (<anonymous>)
            at callAsyncCircusFn (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/utils.js:231:10)
            at _callCircusTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:316:40)
            at async _runTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:252:3)
            at async _runTestsForDescribeBlock (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:126:9)
            at async _runTestsForDescribeBlock (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:121:9)
            at async run (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:71:3)
            at async runAndTransformResultsToJestFormat (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
            at async jestAdapter (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
            at async runTestInternal (/Users/agentender/repos/nx/node_modules/.pnpm/jest-runner@29.7.0/node_modules/jest-runner/build/runTest.js:367:16)
            at async runTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-runner@29.7.0/node_modules/jest-runner/build/runTest.js:444:34)
        Caused by:
            Error: cause message
              at /Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.spec.ts:16:21
              at handleErrors (/Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.ts:11:26)
              at Object.<anonymous> (/Users/agentender/repos/nx/packages/nx/src/utils/handle-errors.spec.ts:15:23)
              at Promise.then.completed (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/utils.js:298:28)
              at new Promise (<anonymous>)
              at callAsyncCircusFn (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/utils.js:231:10)
              at _callCircusTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:316:40)
              at async _runTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:252:3)
              at async _runTestsForDescribeBlock (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:126:9)
              at async _runTestsForDescribeBlock (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:121:9)
              at async run (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/run.js:71:3)
              at async runAndTransformResultsToJestFormat (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
              at async jestAdapter (/Users/agentender/repos/nx/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
              at async runTestInternal (/Users/agentender/repos/nx/node_modules/.pnpm/jest-runner@29.7.0/node_modules/jest-runner/build/runTest.js:367:16)
              at async runTest (/Users/agentender/repos/nx/node_modules/.pnpm/jest-runner@29.7.0/node_modules/jest-runner/build/runTest.js:444:34)"
    `
```

(cherry picked from commit 1924bc30b6)
2024-09-12 18:18:07 -04:00
Craigory Coppola 42cab264dc fix(misc): createNodesV2 plugins should show inference capabilities (#27896)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
Plugins that export `createNodesV2` are not considered as having
inference capabilities

## Expected Behavior
`createNodesV2` is the future API replacing `createNodes`, so it should
show the same capability

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

Fixes #

(cherry picked from commit 2e0f374964)
2024-09-12 18:18:06 -04:00
Louie Weng c585aeaf1f fix(nx-cloud): include nxCloudId when generating connect urls (#27882)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

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

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

NxCloudId should be part of the URL that is created when connecting a
new workspace.

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

Fixes #

(cherry picked from commit 7f7e4d0c4f)
2024-09-12 18:18:05 -04:00
Chau Tran add2061f3e docs(nx-cloud): update azure saml setup (#27898)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

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

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

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

Fixes #

(cherry picked from commit 0e603af8a9)
2024-09-12 18:18:04 -04:00
Nate Jacobs afd07bbcf6 fix(core): respect filenames of inputs when computing task hash (#27873)
(cherry picked from commit b6140d4590)
2024-09-12 18:18:03 -04:00
Jack Hsu e07d47a00b fix(webpack): handle relative paths for additionalEntryPath (#27885)
The `NxAppWebpackPlugin` does not support relative paths in
`additionalEntryPoints`.

So this will fail:

```js
new NxAppWebpackPlugin({
  ...
  additionalEntryPoints: ['.src/foo.ts']
```

The resolved path is relative to workspace root when it should be
project root.

<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
Build will fail.
## Expected Behavior
Build should work.

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

Fixes #

(cherry picked from commit d8cb932422)
2024-09-12 18:18:02 -04:00
Isaac Mann 279e799b05 chore(nx-dev): increase timeout for nx-dev-e2e (#27872)
Increase timeout for nx-dev-e2e tasks

(cherry picked from commit 43eaa5a348)
2024-09-12 18:18:01 -04:00
Louie Weng b078d7587e docs(nx-cloud): add more information about setting up CI access tokens (#27883)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

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

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

More graphics for changing and viewing access tokens (both CI and
personal). Also update the CI access token page with more information on
how to set them and the purposes of read-only tokens.

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

Fixes #

---------

Co-authored-by: Isaac Mann <isaacplmann@users.noreply.github.com>
(cherry picked from commit 7da48d022c)
2024-09-12 18:18:00 -04:00
Jack Hsu 9352f83dfa feat(core): import warns when source and destination directories are different (#27875)
This PR adds a warning when the user choose different source and
destination roots. This is a problem for Nx workspaces using path
options in `project.json`, and possibly other config files such as
`tsconfig.json`, `jest.config.ts`, etc.

Note: Also included a guard that the destination directory isn't an
absolute path like `/tmp/foo`, because the behavior will not work as
expected.

The message when the source is an Nx workspace:
<img width="1392" alt="Screenshot 2024-09-11 at 9 32 54 AM"
src="https://github.com/user-attachments/assets/c8ebedba-fd66-4dbf-ada9-eacf86bd67fc">

The message when the source is not an Nx workspace:
<img width="1392" alt="Screenshot 2024-09-11 at 9 30 44 AM"
src="https://github.com/user-attachments/assets/ade9fbd1-4d5d-4d0c-93f6-eaad176af333">

(cherry picked from commit 8b177bd60e)
2024-09-12 18:17:59 -04:00
Emily Xiong a3fef980fe fix(core): handle --no-interative for create-nx-workspace (#27702)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

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

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

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

Fixes #

(cherry picked from commit 24edc5ad99)
2024-09-12 18:17:58 -04:00
Emily Xiong 108dbcdab7 fix(gradle): fix gradle app deps (#27865)
<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

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

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

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

Fixes https://github.com/nrwl/nx/issues/27819

(cherry picked from commit 2eb5592ac3)
2024-09-12 18:17:57 -04:00
Philip Fulcher 98d81c3168 docs(nx-dev): fixes for personal access token blog (#27864)
* Updates screenshot of workspace settings
* Clarifies language around the personal access token

(cherry picked from commit d6b87e5306)
2024-09-12 18:17:56 -04:00
Isaac Mann 2ece11d7fb docs(core): changes to login docs (#27863)
- Fixes typo for login/logout on commands landing page
- Mention that `nx-cloud login` is the same as `nx login`
- Update references to `npx nx-cloud login` to use `nx login` by default
instead
- Updates Workspace ID access level wording in the "Nx CLI and CI Access
Tokens" page

(cherry picked from commit 5576ba1159)
2024-09-12 18:17:54 -04:00
Leosvel Pérez Espinosa 8b2393e8bf fix(js): keep refs to ignored files and allow opting out of pruning stale refs in typescript sync generator (#27636)
(cherry picked from commit 2a3307cfad)
2024-09-12 18:17:53 -04:00
Leosvel Pérez Espinosa 5063a4cce7 fix(core): handle sync generator failures (#27650)
(cherry picked from commit 4986b88bb2)
2024-09-12 18:17:53 -04:00
Isaac Mann c398cbd760 docs(core): reword import help text (#27732)
Updates the help text for the `nx import` command

(cherry picked from commit 5d039c2dcd)
2024-09-12 18:17:52 -04:00
67 changed files with 1215 additions and 405 deletions
@@ -38,14 +38,10 @@ platform so that they're no longer committed to your repo.
## What are personal access tokens?
[Personal access tokens](/ci/recipes/security/personal-access-tokens) are a new type of access token that is scoped to
an individual user. This means that this token lives and dies with that member's access to your Nx Cloud workspace.
Users must log in to Nx Cloud and they are a member of a workspace before a personal access token can be created.
Once created, we validate that token for access each time you use the distributed cache. As soon as a user loses access,
the personal access token no
longer works, and access to the cache is removed.
an individual user, rather than the workspace. This token authenticates the user with Nx Cloud when running tasks, so that we can validate their access to the distributed cache for a workspace. As soon as a user loses access to an Nx Cloud organization, they will no longer be able to access the cache for any of the organization's workspaces. The user's token belongs to them and will still allow access to their remaining organizations.
This gets even more powerful when combined with the GitHub VCS integration. When a user's GitHub access is removed from
a GitHub-connected organization, their access to Nx Cloud is removed, and their personal access token is invalidated.
a GitHub-connected organization, their access to your Nx Cloud organization is removed, and their access to the cache for any of the organization's workspaces is removed.
This means that Nx Cloud can fit into existing user de-provisioning processes you already have.
Open source teams also benefit from personal access tokens. You can configure your access to allow anonymous users to
@@ -71,7 +67,7 @@ can [find more details in our docs](/ci/recipes/security/personal-access-tokens)
CI
access token defined in the `nxCloudAccessToken` property. This command will replace that with `nxCloudId`, a generic
id that references your workspace but no longer provides access to the cache.
2. **Generate a personal access token by running `npx nx-cloud login`** - Follow the directions in your terminal to log
2. **Generate a personal access token by running `npx nx login`** - Follow the directions in your terminal to log
in
to Nx Cloud. Each contributor with access to the workspace will need to complete this step.
3. **Move CI access tokens to environment variables** - Now that the access token is no longer committed to your
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -59,18 +59,30 @@ if you are interested.
![Step 11](/nx-cloud/enterprise/on-premise/images/saml/azure_11.png)
11. Download the certificate in **Base64**:
Make sure your application user profile exposes the email address under `user.mail`. This can be configured in `Users and Groups` in the Azure portal. Alternatively, you can always configure the `email` claim to use a different property under the `user` object.
11. Under `SAML Certificates`, click the pencil icon to edit
![Step 12](/nx-cloud/enterprise/on-premise/images/saml/azure_12.png)
12. Extract the downloaded certificate value as a one-line string:
1. `awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' azure_cert_file.cer`
2. Well use this in a bit to initialize an environment variable
13. Copy the Login URL:
For **Signing Option**, select **Sign SAML response and assertion**
![Step 13](/nx-cloud/enterprise/on-premise/images/saml/azure_13.png)
14. Then add these two env vars to your Nx Cloud cluster secrets (see [Helm config](#helm-config) below):
Then click **Save** and close the popover.
12. Download the certificate in **Base64**:
![Step 14](/nx-cloud/enterprise/on-premise/images/saml/azure_14.png)
13. Extract the downloaded certificate value as a one-line string:
1. `awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' azure_cert_file.cer`
2. Well use this in a bit to initialize an environment variable
14. Copy the Login URL:
![Step 15](/nx-cloud/enterprise/on-premise/images/saml/azure_15.png)
15. Then add these two env vars to your Nx Cloud cluster secrets (see [Helm config](#helm-config) below):
1. `SAML_CERT=<your-cert-string-from-above>`
2. `SAML_ENTRY_POINT=<your-login-url-from-above>`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+26 -6
View File
@@ -1,6 +1,8 @@
# Nx CLI and CI Access Tokens
The permissions and membership define what developers can access on nx.app but they don't affect what happens when you run Nx commands in CI. To manage that, you need to provision CI access tokens in Workspace settings / Manage CI access tokens.
The permissions and membership define what developers can access on nx.app but they don't affect what happens when you run Nx commands in CI. To manage that, you need to provision CI access tokens in your workspace settings, under the `CI access tokens` tab.
![CI Access Tokens Settings Page](/nx-cloud/recipes/ci-access-tokens-settings.avif)
## Access Types
@@ -15,7 +17,7 @@ There are currently two (2) types of CI Access Token for Nx Cloud's runner that
### Read Only Access
The `read-only` access tokens will only read from the remote cache. Task results will not be stored in the remote cache for other machines or CI pipelines to use.
The `read-only` access tokens will only read from the remote cache. New task results will not be stored in the remote cache, but cached results can be downloaded and replayed for other machines or CI pipelines to use. This option provides the benefit of remote cache hits while restricting machines without proper permissions from adding entries into the remote cache.
### Read & Write Access
@@ -23,9 +25,27 @@ The `read-write` access tokens allows task results to be stored in the remote ca
## Setting CI Access Tokens
You can configure an access token in CI by setting the `NX_CLOUD_ACCESS_TOKEN` environment variable. `NX_CLOUD_ACCESS_TOKEN` takes precedence over any value in your `nx.json`.
You can configure an access token in CI by setting the `NX_CLOUD_ACCESS_TOKEN` environment variable. `NX_CLOUD_ACCESS_TOKEN` takes precedence over any authentication method in your `nx.json`.
We do not recommend that you commit an access token to your repository but older versions of Nx do support this and if you open your `nx.json`, you may see something like this:
The following example shows how to set the `NX_CLOUD_ACCESS_TOKEN` environment variable in a GitHub Actions workflow. You will need to add the `secrets.NX_CLOUD_ACCESS_TOKEN` secret to your repository based on instructions provided by your CI provider.
```yml {% fileName=".github/workflows/ci.yml" highlightLines=["29-32"] %}
name: CI
# ...
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
main:
runs-on: ubuntu-latest
steps: ...
```
### Legacy methods of setting CI Access Tokens
#### Using CI Access Tokens in nx.json
We **do not recommend** that you commit an access token to your repository but older versions of Nx do support this and if you open your `nx.json`, you may see something like this:
{% tabs %}
{% tab label="Nx >= 17" %}
@@ -56,9 +76,9 @@ We do not recommend that you commit an access token to your repository but older
{% /tabs %}
{% callout type="warning" title="Nx Cloud authentication is changing" %}
From Nx 19.7 new workspaces are connected to Nx Cloud with a property called `nxCloudId` instead, and we recommend developers use [`nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) to provision their own local [personal access tokens](/ci/recipes/security/personal-access-tokens).
From Nx 19.7 new workspaces are connected to Nx Cloud with a property called `nxCloudId` instead, and we recommend developers use [`nx login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) to provision their own local [personal access tokens](/ci/recipes/security/personal-access-tokens) for user based authentication.
{% /callout %}
## Using `nx-cloud.env`
#### Using `nx-cloud.env`
You can set an environment variable locally via the `nx-cloud.env` file. Nx Cloud CLI will look in this file to load custom configuration like `NX_CLOUD_ACCESS_TOKEN`. These environment variables will take precedence over the configuration in `nx.json`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,6 +1,6 @@
# Nx Cloud and Personal Access Tokens
From Nx 19.7 repositories are connected to Nx Cloud via a property in `nx.json` called `nxCloudId`. By default this value allows anyone who clones the repository `read-write` access to Nx Cloud features for that workspace. These permissions can be updated in the workspace settings. To disallow access to anonymous users or allow `read-write` access to known users it is required that all users provision their own personal access token. To do that they need to use [`npx nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login).
From Nx 19.7 repositories are connected to Nx Cloud via a property in `nx.json` called `nxCloudId`. By default this value allows anyone who clones the repository `read-write` access to Nx Cloud features for that workspace. These permissions can be updated in the workspace settings. To disallow access to anonymous users or allow `read-write` access to known users it is required that all users provision their own personal access token. To do that they need to use [`npx nx login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login).
{% callout type="warning" title="Personal Access Tokens require the `nxCloudId` field in `nx.json`" %}
Ensure that you have the `nxCloudId` property in your `nx.json` file to connect to Nx Cloud with a Personal Access Token. If you have been using `nxCloudAccessToken`, you can convert it to `nxCloudId` by running [`npx nx-cloud convert-to-nx-cloud-id`](/ci/reference/nx-cloud-cli#npx-nxcloud-converttonxcloudid).
@@ -44,7 +44,23 @@ To utilize personal access tokens and Nx Cloud ID with Nx <= 19.6, the nx-cloud
## Personal Access Tokens (PATs)
When you run [`npx nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) you will be directed to the Nx Cloud app where you will be required to create an account and login. A new personal access token will be provisioned and saved in a local configuration file in your home folder (the location of this will be displayed when login is complete and varies depending on OS).
When you run [`npx nx login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) you will be directed to the Nx Cloud app where you will be required to create an account and login. A new personal access token will be provisioned and saved in a local configuration file in your home folder (the location of this will be displayed when login is complete and varies depending on OS).
### View your Personal Access Tokens
You can view your personal access tokens in the Nx Cloud app by navigating to your profile settings. Click your user icon in the top right corner of the app and select `Profile`.
![Profile Settings](/nx-cloud/recipes/profile-page.avif)
From there, click on the `Personal access tokens` tab.
![Personal Access Tokens](/nx-cloud/recipes/personal-access-tokens-profile.avif)
### Manually create a Personal Access Token
Personal access tokens can also be manually created in the Nx Cloud app. Navigate to your profile settings and click on the `Personal access tokens` tab. Select `New access token`, enter a name for the token and click `Generate Token`. The token will be displayed on the screen and can be copied to your clipboard.
You can then use [nx-cloud configure](/ci/reference/nx-cloud-cli#npx-nxcloud-configure) in your terminal to set the token in your local configuration file.
## Permissions
@@ -52,12 +68,13 @@ There are two types of permissions that can be granted to users.
### Workspace ID access level
These are the permissions granted to users who clone your workspace, but have not authenticated with a personal access token via [`npx nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login).
By default, all users have `read-write` access to the workspace. This can be updated in the workspace settings to `read-only` or `none`.
These are the permissions granted to users who are not [logged in](/ci/reference/nx-cloud-cli#npx-nxcloud-login) or are not members of the Nx Cloud organization for this workspace. By default, all users have `read-write` access to the workspace. This can be updated in the workspace settings to `read-only` or `none`.
While the initial setting for workspace ID access level is `read-write`, we recommend that you change this setting to `read-only` or `none` for any repository that is visible to people that do not have permission to edit the repository (i.e. open source repositories or repositories that are visible across an organization, but only editable by a specific team).
### Personal Access Token access level
When a workspace member logs in with a personal access token after running [`npx nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) they are granted access to Nx Cloud features.
When a workspace member logs in with a personal access token after running [`npx nx login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login) they are granted access to Nx Cloud features.
By default all personal access tokens have `read-write` access to the remote cache. This can be updated to `read-only` in the workspace settings if required.
## Better Security
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+2
View File
@@ -4,6 +4,8 @@
To provision a local personal access token to access Nx Cloud features run `npx nx-cloud login`. This will open your browser to the Nx Cloud application and after signing in will generate a personal access token and save it in a configuration file locally called `nxcloud.ini`.
This command is the same as running `npx nx login`
{% tabs %}
{% tab label="macOS & Linux" %}
+2 -2
View File
@@ -297,7 +297,7 @@ nx connect
### login
Connect an Nx workspace to Nx Cloud
Login to Nx Cloud. This command is an alias for [`nx-cloud login`](/ci/reference/nx-cloud-cli#npx-nxcloud-login).
```shell
nx login
@@ -310,7 +310,7 @@ nx login
### logout
Connect an Nx workspace to Nx Cloud
Logout from Nx Cloud. This command is an alias for [`nx-cloud logout`](/ci/reference/nx-cloud-cli#npx-nxcloud-logout).
```shell
nx logout
+16 -7
View File
@@ -32,9 +32,11 @@ describe('Gradle', () => {
expect(projects).toContain('utilities');
expect(projects).toContain(gradleProjectName);
const buildOutput = runCLI('build app', { verbose: true });
let buildOutput = runCLI('build app', { verbose: true });
// app depends on list and utilities
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:build');
expect(buildOutput).toContain(':utilities:classes');
checkFilesExist(
@@ -43,9 +45,14 @@ describe('Gradle', () => {
`utilities/build/libs/utilities.jar`
);
expect(() => {
runCLI(`build ${gradleProjectName}`, { verbose: true });
}).not.toThrow();
buildOutput = runCLI(`build ${gradleProjectName}`, { verbose: true });
// root project depends on app, list and utilities
expect(buildOutput).toContain('nx run app:build');
expect(buildOutput).toContain(':app:classes');
expect(buildOutput).toContain('nx run list:build');
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain('nx run utilities:build');
expect(buildOutput).toContain(':utilities:classes');
});
it('should track dependencies for new app', () => {
@@ -85,9 +92,11 @@ dependencies {
return content;
}
);
expect(() => {
runCLI('build app2', { verbose: true });
}).not.toThrow();
let buildOutput = runCLI('build app2', { verbose: true });
// app2 depends on app
expect(buildOutput).toContain('nx run app:build');
expect(buildOutput).toContain(':app:classes');
});
}
);
+18 -6
View File
@@ -132,8 +132,12 @@ describe('Webpack Plugin', () => {
it('should be able to build with NxWebpackPlugin and a standard webpack config file', () => {
const appName = uniq('app');
runCLI(`generate @nx/web:app ${appName} --bundler webpack`);
runCLI(
`generate @nx/web:app ${appName} --bundler webpack --directory=apps/${appName} --projectNameAndRootFormat=as-provided`
);
updateFile(`apps/${appName}/src/main.ts`, `console.log('Hello');\n`);
updateFile(`apps/${appName}/src/foo.ts`, `console.log('Foo');\n`);
updateFile(`apps/${appName}/src/bar.ts`, `console.log('Bar');\n`);
updateFile(
`apps/${appName}/webpack.config.js`,
@@ -144,13 +148,20 @@ describe('Webpack Plugin', () => {
module.exports = {
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
path: path.join(__dirname, '../../dist/apps/${appName}')
},
plugins: [
new NxAppWebpackPlugin({
compiler: 'tsc',
main: 'apps/${appName}/src/main.ts',
tsConfig: 'apps/${appName}/tsconfig.app.json',
main: './src/main.ts',
additionalEntryPoints: [
'./src/foo.ts',
{
entryName: 'bar',
entryPath: './src/bar.ts',
}
],
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
@@ -160,8 +171,9 @@ describe('Webpack Plugin', () => {
runCLI(`build ${appName}`);
let output = runCommand(`node dist/${appName}/main.js`);
expect(output).toMatch(/Hello/);
expect(runCommand(`node dist/apps/${appName}/main.js`)).toMatch(/Hello/);
expect(runCommand(`node dist/apps/${appName}/foo.js`)).toMatch(/Foo/);
expect(runCommand(`node dist/apps/${appName}/bar.js`)).toMatch(/Bar/);
}, 500_000);
it('should bundle in NX_PUBLIC_ environment variables', () => {
@@ -215,6 +215,20 @@ describe('create-nx-workspace', () => {
expectCodeIsFormatted();
});
it('should be able to create a react workspace without options and --no-interactive', () => {
const wsName = uniq('react');
runCreateWorkspace(wsName, {
preset: 'react-monorepo',
});
expectNoAngularDevkit();
expectNoTsJestInJestConfig(wsName);
const packageJson = readJson('package.json');
expect(packageJson.devDependencies['@nx/vite']).toBeDefined(); // vite should be default bundler
expectCodeIsFormatted();
});
it('should be able to create an next workspace', () => {
const wsName = uniq('next');
const appName = uniq('app');
+1 -1
View File
@@ -56,7 +56,7 @@ export default defineConfig({
command: 'pnpm exec nx run nx-dev:start',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
timeout: 180_000,
},
projects: [
{
@@ -29,6 +29,7 @@ import { showNxWarning } from '../src/utils/nx/show-nx-warning';
import { messages, recordStat } from '../src/utils/nx/ab-testing';
import { mapErrorToBodyLines } from '../src/utils/error-utils';
import { existsSync } from 'fs';
import { isCI } from '../src/utils/ci/is-ci';
interface BaseArguments extends CreateWorkspaceOptions {
preset: Preset;
@@ -320,13 +321,13 @@ async function determineFolder(
? parsedArgs._[0].toString()
: parsedArgs.name;
if (folderName) return folderName;
const reply = await enquirer.prompt<{ folderName: string }>([
{
name: 'folderName',
message: `Where would you like to create your workspace?`,
initial: 'org',
type: 'input',
skip: !parsedArgs.interactive || isCI(),
},
]);
@@ -369,6 +370,7 @@ async function determineStack(
return 'vue';
case Preset.Nest:
case Preset.NodeStandalone:
case Preset.NodeMonorepo:
case Preset.Express:
return 'node';
case Preset.Apps:
@@ -477,6 +479,7 @@ async function determineNoneOptions(
},
],
initial: 0,
skip: !parsedArgs.interactive || isCI(),
},
]);
js = reply.ts === 'No';
@@ -578,6 +581,7 @@ async function determineReactOptions(
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'css',
@@ -678,6 +682,7 @@ async function determineVueOptions(
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'css',
@@ -764,6 +769,7 @@ async function determineAngularOptions(
name: 'bundler',
message: `Which bundler would you like to use?`,
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'esbuild',
@@ -789,6 +795,7 @@ async function determineAngularOptions(
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'css',
@@ -819,6 +826,7 @@ async function determineAngularOptions(
type: 'autocomplete',
choices: [{ name: 'Yes' }, { name: 'No' }],
initial: 1,
skip: !parsedArgs.interactive || isCI(),
},
]);
ssr = reply.ssr === 'Yes';
@@ -887,6 +895,7 @@ async function determineNodeOptions(
message:
'Would you like to generate a Dockerfile? [https://docs.docker.com/]',
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'Yes',
@@ -1000,6 +1009,7 @@ async function determineAppName(
message: `Application name`,
type: 'input',
initial: parsedArgs.name,
skip: !parsedArgs.interactive || isCI(),
},
]);
invariant(appName, {
@@ -1059,6 +1069,7 @@ async function determineReactBundler(
name: 'bundler',
message: `Which bundler would you like to use?`,
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'vite',
@@ -1073,6 +1084,7 @@ async function determineReactBundler(
message: 'Rspack [ https://www.rspack.dev/ ]',
},
],
initial: 0,
},
]);
return reply.bundler;
@@ -1087,6 +1099,7 @@ async function determineNextAppDir(
name: 'nextAppDir',
message: 'Would you like to use the App Router (recommended)?',
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'Yes',
@@ -1110,6 +1123,7 @@ async function determineNextSrcDir(
name: 'nextSrcDir',
message: 'Would you like to use the src/ directory?',
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'Yes',
@@ -1135,6 +1149,7 @@ async function determineVueFramework(
name: 'framework',
message: 'What framework would you like to use?',
type: 'autocomplete',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'none',
@@ -1155,7 +1170,7 @@ async function determineVueFramework(
async function determineNodeFramework(
parsedArgs: yargs.Arguments<NodeArguments>
): Promise<'express' | 'fastify' | 'koa' | 'nest' | 'none'> {
if (parsedArgs.framework) return parsedArgs.framework;
if (!!parsedArgs.framework) return parsedArgs.framework;
const reply = await enquirer.prompt<{
framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none';
}>([
@@ -1163,6 +1178,7 @@ async function determineNodeFramework(
message: 'What framework should be used?',
type: 'autocomplete',
name: 'framework',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'none',
@@ -1185,6 +1201,7 @@ async function determineNodeFramework(
message: 'NestJs [ https://nestjs.com/ ]',
},
],
initial: 0,
},
]);
return reply.framework;
@@ -1203,6 +1220,7 @@ async function determineE2eTestRunner(
message: 'Test runner to use for end to end (E2E) tests',
type: 'autocomplete',
name: 'e2eTestRunner',
skip: !parsedArgs.interactive || isCI(),
choices: [
{
name: 'playwright',
@@ -1217,6 +1235,7 @@ async function determineE2eTestRunner(
message: 'None',
},
],
initial: 0,
},
]);
return reply.e2eTestRunner;
@@ -15,9 +15,9 @@ export function readNxCloudToken(directory: string) {
// nx-ignore-next-line
)) as typeof import('nx/src/nx-cloud/utilities/get-cloud-options');
const { accessToken } = getCloudOptions(directory);
const { accessToken, nxCloudId } = getCloudOptions(directory);
nxCloudSpinner.succeed('Nx Cloud has been set up successfully');
return accessToken;
return accessToken || nxCloudId;
}
export async function getOnboardingInfo(
@@ -11,8 +11,8 @@ antBuilderFactory: org.gradle.api.internal.project.DefaultAntBuilderFactory@5a5e
artifacts: org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler_Decorated@3793af5a
asDynamicObject: DynamicObject for root project 'My Application'
baseClassLoaderScope: org.gradle.api.internal.initialization.DefaultClassLoaderScope@4dc8adab
buildDir: /Users/emily/code/tmp/nx-android/build
buildFile: /Users/emily/code/tmp/nx-android/build.gradle
buildDir: /tmp/nx-android/build
buildFile: /tmp/nx-android/build.gradle
buildPath: :
buildScriptSource: org.gradle.groovy.scripts.TextResourceScriptSource@622acc87
buildTreePath: :
@@ -73,13 +73,13 @@ plugins: [org.gradle.api.plugins.HelpTasksPlugin$Inject@611c939a, org.gradle.bui
processOperations: org.gradle.process.internal.DefaultExecActionFactory$DecoratingExecActionFactory@2bcbdbd2
project: root project 'My Application'
projectConfigurator: org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator@e136ccb
projectDir: /Users/emily/code/tmp/nx-android
projectDir: /tmp/nx-android
projectEvaluationBroadcaster: ProjectEvaluationListener broadcast
projectEvaluator: org.gradle.configuration.project.LifecycleProjectEvaluator@4d3e0414
projectPath: :
projectReport: task ':projectReport'
projectReportAll: task ':projectReportAll'
projectReportDir: /Users/emily/code/tmp/nx-android/build/reports/project
projectReportDir: /tmp/nx-android/build/reports/project
projectReportDirName: project
projects: [root project 'My Application']
properties: {...}
@@ -89,7 +89,7 @@ publicType: org.gradle.api.plugins.ProjectReportsPluginConvention
reporting: extension 'reporting'
repositories: repository container
resources: org.gradle.api.internal.resources.DefaultResourceHandler@3bfc23c4
rootDir: /Users/emily/code/tmp/nx-android
rootDir: /tmp/nx-android
rootProject: root project 'My Application'
rootScript: false
script: false
+12 -3
View File
@@ -191,10 +191,16 @@ export function processProjectReports(
absBuildDirPath = line.substring('buildDir: '.length);
}
if (line.startsWith('childProjects: ')) {
const childProjects = line.substring('childProjects: {'.length); // remove curly braces {} around childProjects
const childProjects = line.substring(
'childProjects: {'.length,
line.length - 1
); // remove curly braces {} around childProjects
gradleProjectToChildProjects.set(
gradleProject,
childProjects.split(',').map((c) => c.trim().split('=')[0]) // e.g. get project name from text like "app=project ':app', mylibrary=project ':mylibrary'"
childProjects
.split(',')
.map((c) => c.trim().split('=')[0])
.filter(Boolean) // e.g. get project name from text like "app=project ':app', mylibrary=project ':mylibrary'"
);
}
if (line.includes('Dir: ')) {
@@ -228,7 +234,10 @@ export function processProjectReports(
gradleFileToOutputDirsMap.set(buildFile, outputDirMap);
gradleFileToGradleProjectMap.set(buildFile, gradleProject);
gradleProjectToProjectName.set(gradleProject, projectName);
gradleProjectNameToProjectRootMap.set(projectName, dirname(buildFile));
gradleProjectNameToProjectRootMap.set(
gradleProject,
dirname(buildFile)
);
}
if (line.endsWith('taskReport')) {
const gradleProject = line.substring(
@@ -368,7 +368,7 @@ describe('syncGenerator()', () => {
references: [
{ path: './some/thing' },
{ path: './another/one' },
{ path: '../packages/c' }, // this is not a dependency, should be pruned
{ path: '../c' }, // this is not a dependency, should be pruned
],
});
@@ -391,6 +391,86 @@ describe('syncGenerator()', () => {
`);
});
it('should not prune existing external project references that are not dependencies but are git ignored', async () => {
writeJson(tree, 'packages/b/tsconfig.json', {
compilerOptions: {
composite: true,
},
references: [
{ path: './some/thing' },
{ path: './another/one' },
{ path: '../../some-path/dir' }, // this is not a dependency but it's git ignored, should not be pruned
{ path: '../c' }, // this is not a dependency and it's not git ignored, should be pruned
],
});
tree.write('some-path/dir/tsconfig.json', '{}');
tree.write('.gitignore', 'some-path/dir');
await syncGenerator(tree);
const rootTsconfig = readJson(tree, 'packages/b/tsconfig.json');
// The dependency reference on "a" is added to the start of the array
expect(rootTsconfig.references).toMatchInlineSnapshot(`
[
{
"path": "../a",
},
{
"path": "./some/thing",
},
{
"path": "./another/one",
},
{
"path": "../../some-path/dir",
},
]
`);
});
it('should not prune stale project references from projects included in `nx.sync.ignoredReferences`', async () => {
writeJson(tree, 'packages/b/tsconfig.json', {
compilerOptions: {
composite: true,
},
references: [
{ path: './some/thing' },
{ path: './another/one' },
// this is not a dependency and it's not git ignored, it would normally be pruned,
// but it's included in `nx.sync.ignoredReferences`, so we don't prune it
{ path: '../c' },
],
nx: {
sync: {
ignoredReferences: ['../c'],
},
},
});
tree.write('some-path/dir/tsconfig.json', '{}');
tree.write('.gitignore', 'some-path/dir');
await syncGenerator(tree);
const rootTsconfig = readJson(tree, 'packages/b/tsconfig.json');
// The dependency reference on "a" is added to the start of the array
expect(rootTsconfig.references).toMatchInlineSnapshot(`
[
{
"path": "../a",
},
{
"path": "./some/thing",
},
{
"path": "./another/one",
},
{
"path": "../c",
},
]
`);
});
it('should collect transitive dependencies and sync project references to tsconfig.json files', async () => {
// c => b => a
// d => b => a
@@ -10,6 +10,7 @@ import {
type ProjectGraphProjectNode,
type Tree,
} from '@nx/devkit';
import ignore from 'ignore';
import { applyEdits, modify } from 'jsonc-parser';
import { dirname, normalize, relative } from 'node:path/posix';
import type { SyncGeneratorResult } from 'nx/src/utils/sync-generators';
@@ -26,6 +27,11 @@ interface Tsconfig {
rootDir?: string;
outDir?: string;
};
nx?: {
sync?: {
ignoredReferences?: string[];
};
};
}
const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [
@@ -37,6 +43,12 @@ const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [
'tsconfig.runtime.json',
];
type GeneratorOptions = {
runtimeTsConfigFileNames?: string[];
};
type NormalizedGeneratorOptions = Required<GeneratorOptions>;
export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
// Ensure that the plugin has been wired up in nx.json
const nxJson = readNxJson(tree);
@@ -151,23 +163,27 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
}
}
const runtimeTsConfigFileNames =
(nxJson.sync?.generatorOptions?.['@nx/js:typescript-sync']
?.runtimeTsConfigFileNames as string[]) ??
COMMON_RUNTIME_TS_CONFIG_FILE_NAMES;
const userOptions = nxJson.sync?.generatorOptions?.[
'@nx/js:typescript-sync'
] as GeneratorOptions | undefined;
const { runtimeTsConfigFileNames }: NormalizedGeneratorOptions = {
runtimeTsConfigFileNames:
userOptions?.runtimeTsConfigFileNames ??
COMMON_RUNTIME_TS_CONFIG_FILE_NAMES,
};
const collectedDependencies = new Map<string, ProjectGraphProjectNode[]>();
for (const [name, data] of Object.entries(projectGraph.dependencies)) {
for (const [projectName, data] of Object.entries(projectGraph.dependencies)) {
if (
!projectGraph.nodes[name] ||
projectGraph.nodes[name].data.root === '.' ||
!projectGraph.nodes[projectName] ||
projectGraph.nodes[projectName].data.root === '.' ||
!data.length
) {
continue;
}
// Get the source project nodes for the source and target
const sourceProjectNode = projectGraph.nodes[name];
const sourceProjectNode = projectGraph.nodes[projectName];
// Find the relevant tsconfig file for the source project
const sourceProjectTsconfigPath = joinPathFragments(
@@ -179,7 +195,7 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
logger.warn(
`Skipping project "${name}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}".`
`Skipping project "${projectName}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}".`
);
}
continue;
@@ -188,7 +204,7 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
// Collect the dependencies of the source project
const dependencies = collectProjectDependencies(
tree,
name,
projectName,
projectGraph,
collectedDependencies
);
@@ -299,14 +315,23 @@ function updateTsConfigReferences(
tsConfigPath
);
const tsConfig = parseJson<Tsconfig>(stringifiedJsonContents);
const ignoredReferences = new Set(tsConfig.nx?.sync?.ignoredReferences ?? []);
// We have at least one dependency so we can safely set it to an empty array if not already set
const references = [];
const originalReferencesSet = new Set();
const newReferencesSet = new Set();
for (const ref of tsConfig.references ?? []) {
const normalizedPath = normalizeReferencePath(ref.path);
originalReferencesSet.add(normalizedPath);
if (ignoredReferences.has(ref.path)) {
// we keep the user-defined ignored references
references.push(ref);
newReferencesSet.add(normalizedPath);
continue;
}
// reference path is relative to the tsconfig file
const resolvedRefPath = getTsConfigPathFromReferencePath(
tree,
@@ -320,9 +345,10 @@ function updateTsConfigReferences(
resolvedRefPath,
projectRoot,
projectRoots
)
) ||
isProjectReferenceIgnored(tree, resolvedRefPath)
) {
// we keep all references within the current Nx project
// we keep all references within the current Nx project or that are ignored
references.push(ref);
newReferencesSet.add(normalizedPath);
}
@@ -511,6 +537,22 @@ function isProjectReferenceWithinNxProject(
return true;
}
function isProjectReferenceIgnored(
tree: Tree,
refTsConfigPath: string
): boolean {
const ig = ignore();
if (tree.exists('.gitignore')) {
ig.add('.git');
ig.add(tree.read('.gitignore', 'utf-8'));
}
if (tree.exists('.nxignore')) {
ig.add(tree.read('.nxignore', 'utf-8'));
}
return ig.ignores(refTsConfigPath);
}
function getTsConfigDirName(
tree: Tree,
rawTsconfigContentsCache: Map<string, string>,
+1 -1
View File
@@ -9,7 +9,7 @@ import { writeJsonFile } from '../../utils/fileutils';
import { logger } from '../../utils/logger';
import { output } from '../../utils/output';
import { getPackageManagerCommand } from '../../utils/package-manager';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { getPluginCapabilities } from '../../utils/plugins';
import { nxVersion } from '../../utils/versions';
import { workspaceRoot } from '../../utils/workspace-root';
@@ -9,7 +9,7 @@ import {
withRunOptions,
withTargetAndConfigurationOption,
} from '../yargs-utils/shared-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export const yargsAffectedCommand: CommandModule = {
command: 'affected',
@@ -1,5 +1,5 @@
import { CommandModule } from 'yargs';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import {
withAffectedOptions,
withTargetAndConfigurationOption,
@@ -12,14 +12,13 @@ import {
import { logger, NX_PREFIX } from '../../utils/logger';
import {
combineOptionsForGenerator,
handleErrors,
Options,
Schema,
} from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { getLocalWorkspacePlugins } from '../../utils/plugins/local-plugins';
import { printHelp } from '../../utils/print-help';
import { workspaceRoot } from '../../utils/workspace-root';
import { NxJsonConfiguration } from '../../config/nx-json';
import { calculateDefaultProjectName } from '../../config/calculate-default-project-name';
import { findInstalledPlugins } from '../../utils/plugins/installed-plugins';
import { getGeneratorInformation } from './generator-utils';
@@ -1,26 +1,28 @@
import { CommandModule } from 'yargs';
import { linkToNxDevAndExamples } from '../yargs-utils/documentation';
import { withVerbose } from '../yargs-utils/shared-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export const yargsImportCommand: CommandModule = {
command: 'import [sourceRemoteUrl] [destination]',
command: 'import [sourceRepository] [destinationDirectory]',
describe: false,
builder: (yargs) =>
linkToNxDevAndExamples(
withVerbose(
yargs
.positional('sourceRemoteUrl', {
.positional('sourceRepository', {
type: 'string',
description: 'The remote URL of the source to import.',
})
.positional('destination', {
.positional('destinationDirectory', {
type: 'string',
alias: 'destination',
description:
'The directory in the current workspace to import into.',
})
.option('source', {
.option('sourceDirectory', {
type: 'string',
alias: 'source',
description:
'The directory in the source repository to import from.',
})
+60 -33
View File
@@ -1,4 +1,4 @@
import { dirname, join, relative, resolve } from 'path';
import { dirname, isAbsolute, join, relative, resolve } from 'path';
import { minimatch } from 'minimatch';
import { existsSync, promises as fsp } from 'node:fs';
import * as chalk from 'chalk';
@@ -36,7 +36,7 @@ export interface ImportOptions {
/**
* The remote URL of the repository to import
*/
sourceRemoteUrl: string;
sourceRepository: string;
/**
* The branch or reference to import
*/
@@ -59,50 +59,49 @@ export interface ImportOptions {
}
export async function importHandler(options: ImportOptions) {
let { sourceRemoteUrl, ref, source, destination } = options;
let { sourceRepository, ref, source, destination } = options;
output.log({
title:
'Nx will walk you through the process of importing code from another repository into this workspace:',
'Nx will walk you through the process of importing code from the source repository into this repository:',
bodyLines: [
`1. Nx will clone the other repository into a temporary directory`,
`2. Code to be imported will be moved to the same directory it will be imported into on a temporary branch`,
`3. The code will be merged into the current branch in this workspace`,
`4. Nx will recommend plugins to integrate tools used in the imported code with Nx`,
`5. The code will be successfully imported into this workspace`,
`1. Nx will clone the source repository into a temporary directory`,
`2. The project code from the sourceDirectory will be moved to the destinationDirectory on a temporary branch in this repository`,
`3. The temporary branch will be merged into the current branch in this repository`,
`4. Nx will recommend plugins to integrate any new tools used in the imported code`,
'',
`Git history will be preserved during this process`,
`Git history will be preserved during this process as long as you MERGE these changes. Do NOT squash and do NOT rebase the changes when merging branches. If you would like to UNDO these changes, run "git reset HEAD~1 --hard"`,
],
});
const tempImportDirectory = join(tmpdir, 'nx-import');
if (!sourceRemoteUrl) {
sourceRemoteUrl = (
await prompt<{ sourceRemoteUrl: string }>([
if (!sourceRepository) {
sourceRepository = (
await prompt<{ sourceRepository: string }>([
{
type: 'input',
name: 'sourceRemoteUrl',
name: 'sourceRepository',
message:
'What is the URL of the repository you want to import? (This can be a local git repository or a git remote URL)',
required: true,
},
])
).sourceRemoteUrl;
).sourceRepository;
}
try {
const maybeLocalDirectory = await stat(sourceRemoteUrl);
const maybeLocalDirectory = await stat(sourceRepository);
if (maybeLocalDirectory.isDirectory()) {
sourceRemoteUrl = resolve(sourceRemoteUrl);
sourceRepository = resolve(sourceRepository);
}
} catch (e) {
// It's a remote url
}
const sourceRepoPath = join(tempImportDirectory, 'repo');
const sourceTempRepoPath = join(tempImportDirectory, 'repo');
const spinner = createSpinner(
`Cloning ${sourceRemoteUrl} into a temporary directory: ${sourceRepoPath} (Use --depth to limit commit history and speed up clone times)`
`Cloning ${sourceRepository} into a temporary directory: ${sourceTempRepoPath} (Use --depth to limit commit history and speed up clone times)`
).start();
try {
await rm(tempImportDirectory, { recursive: true });
@@ -111,17 +110,23 @@ export async function importHandler(options: ImportOptions) {
let sourceGitClient: GitRepository;
try {
sourceGitClient = await cloneFromUpstream(sourceRemoteUrl, sourceRepoPath, {
originName: importRemoteName,
depth: options.depth,
});
sourceGitClient = await cloneFromUpstream(
sourceRepository,
sourceTempRepoPath,
{
originName: importRemoteName,
depth: options.depth,
}
);
} catch (e) {
spinner.fail(`Failed to clone ${sourceRemoteUrl} into ${sourceRepoPath}`);
let errorMessage = `Failed to clone ${sourceRemoteUrl} into ${sourceRepoPath}. Please double check the remote and try again.\n${e.message}`;
spinner.fail(
`Failed to clone ${sourceRepository} into ${sourceTempRepoPath}`
);
let errorMessage = `Failed to clone ${sourceRepository} into ${sourceTempRepoPath}. Please double check the remote and try again.\n${e.message}`;
throw new Error(errorMessage);
}
spinner.succeed(`Cloned into ${sourceRepoPath}`);
spinner.succeed(`Cloned into ${sourceTempRepoPath}`);
// Detecting the package manager before preparing the source repo for import.
const sourcePackageManager = detectPackageManager(sourceGitClient.root);
@@ -171,7 +176,14 @@ export async function importHandler(options: ImportOptions) {
).destination;
}
const absSource = join(sourceRepoPath, source);
const absSource = join(sourceTempRepoPath, source);
if (isAbsolute(destination)) {
throw new Error(
`The destination directory must be a relative path in this repository.`
);
}
const absDestination = join(process.cwd(), destination);
const destinationGitClient = new GitRepository(process.cwd());
@@ -180,7 +192,7 @@ export async function importHandler(options: ImportOptions) {
const tempImportBranch = getTempImportBranch(ref);
await sourceGitClient.addFetchRemote(importRemoteName, ref);
await sourceGitClient.fetch(importRemoteName, ref);
spinner.succeed(`Fetched ${ref} from ${sourceRemoteUrl}`);
spinner.succeed(`Fetched ${ref} from ${sourceRepository}`);
spinner.start(
`Checking out a temporary branch, ${tempImportBranch} based on ${ref}`
);
@@ -195,7 +207,7 @@ export async function importHandler(options: ImportOptions) {
await stat(absSource);
} catch (e) {
throw new Error(
`The source directory ${source} does not exist in ${sourceRemoteUrl}. Please double check to make sure it exists.`
`The source directory ${source} does not exist in ${sourceRepository}. Please double check to make sure it exists.`
);
}
@@ -205,6 +217,8 @@ export async function importHandler(options: ImportOptions) {
packageManager
);
const sourceIsNxWorkspace = existsSync(join(sourceGitClient.root, 'nx.json'));
const relativeDestination = relative(
destinationGitClient.root,
absDestination
@@ -215,18 +229,18 @@ export async function importHandler(options: ImportOptions) {
source,
relativeDestination,
tempImportBranch,
sourceRemoteUrl
sourceRepository
);
await createTemporaryRemote(
destinationGitClient,
join(sourceRepoPath, '.git'),
join(sourceTempRepoPath, '.git'),
importRemoteName
);
await mergeRemoteSource(
destinationGitClient,
sourceRemoteUrl,
sourceRepository,
tempImportBranch,
destination,
importRemoteName,
@@ -305,13 +319,26 @@ export async function importHandler(options: ImportOptions) {
await warnOnMissingWorkspacesEntry(packageManager, pmc, relativeDestination);
if (source != destination) {
output.warn({
title: `Check configuration files`,
bodyLines: [
`The source directory (${source}) and destination directory (${destination}) are different.`,
`You may need to update configuration files to match the directory in this repository.`,
sourceIsNxWorkspace
? `For example, path options in project.json such as "main", "tsConfig", and "outputPath" need to be updated.`
: `For example, relative paths in tsconfig.json and other tooling configuration files may need to be updated.`,
],
});
}
// When only a subdirectory is imported, there might be devDependencies in the root package.json file
// that needs to be ported over as well.
if (ref) {
output.log({
title: `Check root dependencies`,
bodyLines: [
`"dependencies" and "devDependencies" are not imported from the source repository (${sourceRemoteUrl}).`,
`"dependencies" and "devDependencies" are not imported from the source repository (${sourceRepository}).`,
`You may need to add some of those dependencies to this workspace in order to run tasks successfully.`,
],
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { verifyOrUpdateNxCloudClient } from '../../nx-cloud/update-manager';
import { getCloudOptions } from '../../nx-cloud/utilities/get-cloud-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export interface LoginArgs {
nxCloudUrl?: string;
@@ -1,6 +1,6 @@
import { verifyOrUpdateNxCloudClient } from '../../nx-cloud/update-manager';
import { getCloudOptions } from '../../nx-cloud/utilities/get-cloud-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export interface LogoutArgs {
verbose?: boolean;
@@ -49,7 +49,7 @@ import {
packageRegistryView,
resolvePackageVersionUsingRegistry,
} from '../../utils/package-manager';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import {
connectToNxCloudWithPrompt,
onlyDefaultRunnerIsUsed,
+2 -1
View File
@@ -1,5 +1,6 @@
import { flushChanges, FsTree } from '../../generators/tree';
import { combineOptionsForGenerator, handleErrors } from '../../utils/params';
import { combineOptionsForGenerator } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { getGeneratorInformation } from '../generate/generator-utils';
function removeSpecialFlags(generatorOptions: { [p: string]: any }): void {
@@ -25,7 +25,7 @@ import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { interpolate } from '../../tasks-runner/utils';
import { isCI } from '../../utils/is-ci';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { joinPathFragments } from '../../utils/path';
import { workspaceRoot } from '../../utils/workspace-root';
import { ChangelogOptions } from './command-object';
@@ -7,7 +7,7 @@ import {
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { PlanCheckOptions, PlanOptions } from './command-object';
import {
createNxReleaseConfig,
+1 -1
View File
@@ -12,7 +12,7 @@ import {
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { PlanOptions } from './command-object';
import {
createNxReleaseConfig,
@@ -15,7 +15,7 @@ import {
readGraphFileFromGraphArg,
} from '../../utils/command-line-utils';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { projectHasTarget } from '../../utils/project-graph-utils';
import { generateGraph } from '../graph/graph';
import { PublishOptions } from './command-object';
@@ -4,7 +4,7 @@ import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import {
createAPI as createReleaseChangelogAPI,
shouldCreateGitHubRelease,
@@ -19,7 +19,7 @@ import {
readProjectsConfigurationFromProjectGraph,
} from '../../project-graph/project-graph';
import { output } from '../../utils/output';
import { combineOptionsForGenerator, handleErrors } from '../../utils/params';
import { combineOptionsForGenerator } from '../../utils/params';
import { joinPathFragments } from '../../utils/path';
import { workspaceRoot } from '../../utils/workspace-root';
import { parseGeneratorString } from '../generate/generate';
@@ -52,6 +52,7 @@ import {
createGitTagValues,
handleDuplicateGitTags,
} from './utils/shared';
import { handleErrors } from '../../utils/handle-errors';
const LARGE_BUFFER = 1024 * 1000000;
@@ -1,4 +1,4 @@
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import * as migrationsJson from '../../../migrations.json';
import { executeMigrations } from '../migrate/migrate';
import { output } from '../../utils/output';
@@ -7,7 +7,7 @@ import {
withOverrides,
withBatch,
} from '../yargs-utils/shared-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export const yargsRunManyCommand: CommandModule = {
command: 'run-many',
@@ -4,7 +4,7 @@ import {
withOverrides,
withRunOneOptions,
} from '../yargs-utils/shared-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export const yargsRunCommand: CommandModule = {
command: 'run [project][:target][:configuration] [_..]',
+2 -5
View File
@@ -1,9 +1,6 @@
import { env as appendLocalEnv } from 'npm-run-path';
import {
combineOptionsForExecutor,
handleErrors,
Schema,
} from '../../utils/params';
import { combineOptionsForExecutor, Schema } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import { printHelp } from '../../utils/print-help';
import { NxJsonConfiguration } from '../../config/nx-json';
import { relative } from 'path';
@@ -5,7 +5,7 @@ import {
withAffectedOptions,
withVerbose,
} from '../yargs-utils/shared-options';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
export interface NxShowArgs {
json?: boolean;
+91 -11
View File
@@ -2,12 +2,15 @@ import * as ora from 'ora';
import { readNxJson } from '../../config/nx-json';
import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import { handleErrors } from '../../utils/handle-errors';
import {
collectAllRegisteredSyncGenerators,
flushSyncGeneratorChanges,
getFailedSyncGeneratorsFixMessageLines,
getFlushFailureMessageLines,
getSyncGeneratorChanges,
syncGeneratorResultsToMessageLines,
getSyncGeneratorSuccessResultsMessageLines,
processSyncGeneratorResultErrors,
} from '../../utils/sync-generators';
import type { SyncArgs } from './command-object';
import chalk = require('chalk');
@@ -52,29 +55,106 @@ export function syncHandler(options: SyncOptions): Promise<number> {
return 0;
}
if (options.check) {
const {
failedGeneratorsCount,
areAllResultsFailures,
anySyncGeneratorsFailed,
} = processSyncGeneratorResultErrors(results);
const failedSyncGeneratorsFixMessageLines =
getFailedSyncGeneratorsFixMessageLines(results, options.verbose);
if (areAllResultsFailures) {
output.error({
title: `The workspace is out of sync`,
bodyLines: syncGeneratorResultsToMessageLines(results),
title: `The workspace is probably out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});
return 1;
}
const resultBodyLines = getSyncGeneratorSuccessResultsMessageLines(results);
if (options.check) {
output.error({
title: 'The workspace is out of sync',
bodyLines: resultBodyLines,
});
if (anySyncGeneratorsFailed) {
output.error({
title:
failedGeneratorsCount === 1
? 'A sync generator failed to run'
: 'Some sync generators failed to run',
bodyLines: failedSyncGeneratorsFixMessageLines,
});
}
return 1;
}
output.warn({
title: `The workspace is out of sync`,
bodyLines: syncGeneratorResultsToMessageLines(results),
title: 'The workspace is out of sync',
bodyLines: resultBodyLines,
});
const spinner = ora('Syncing the workspace...');
spinner.start();
await flushSyncGeneratorChanges(results);
try {
const flushResult = await flushSyncGeneratorChanges(results);
spinner.succeed(`The workspace was synced successfully!
if ('generatorFailures' in flushResult) {
spinner.fail();
output.error({
title: 'Failed to sync the workspace',
bodyLines: getFlushFailureMessageLines(flushResult, options.verbose),
});
Please make sure to commit the changes to your repository.
`);
return 1;
}
} catch (e) {
spinner.fail();
output.error({
title: 'Failed to sync the workspace',
bodyLines: [
'Syncing the workspace failed with the following error:',
'',
e.message,
...(options.verbose && !!e.stack ? [`\n${e.stack}`] : []),
'',
'Please rerun with `--verbose` and report the error at: https://github.com/nrwl/nx/issues/new/choose',
],
});
return 1;
}
const successTitle = anySyncGeneratorsFailed
? // the identified changes were synced successfully, but the workspace
// is still not up to date, which we'll mention next
'The identified changes were synced successfully!'
: // the workspace is fully up to date
'The workspace was synced successfully!';
const successSubtitle =
'Please make sure to commit the changes to your repository.';
spinner.succeed(`${successTitle}\n\n${successSubtitle}`);
if (anySyncGeneratorsFailed) {
output.error({
title: `The workspace is probably still out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});
return 1;
}
return 0;
});
+8 -3
View File
@@ -54,7 +54,10 @@ import {
GET_SYNC_GENERATOR_CHANGES,
type HandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import type { SyncGeneratorChangesResult } from '../../utils/sync-generators';
import type {
FlushSyncGeneratorChangesResult,
SyncGeneratorRunResult,
} from '../../utils/sync-generators';
import {
GET_REGISTERED_SYNC_GENERATORS,
type HandleGetRegisteredSyncGeneratorsMessage,
@@ -364,7 +367,7 @@ export class DaemonClient {
getSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
const message: HandleGetSyncGeneratorChangesMessage = {
type: GET_SYNC_GENERATOR_CHANGES,
generators,
@@ -372,7 +375,9 @@ export class DaemonClient {
return this.sendToDaemonViaQueue(message);
}
flushSyncGeneratorChangesToDisk(generators: string[]): Promise<void> {
flushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<FlushSyncGeneratorChangesResult> {
const message: HandleFlushSyncGeneratorChangesToDiskMessage = {
type: FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
generators,
@@ -4,10 +4,10 @@ import { flushSyncGeneratorChangesToDisk } from './sync-generators';
export async function handleFlushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<HandlerResult> {
await flushSyncGeneratorChangesToDisk(generators);
const result = await flushSyncGeneratorChangesToDisk(generators);
return {
response: '{}',
response: JSON.stringify(result),
description: 'handleFlushSyncGeneratorChangesToDisk',
};
}
@@ -6,12 +6,16 @@ export async function handleGetSyncGeneratorChanges(
): Promise<HandlerResult> {
const changes = await getCachedSyncGeneratorChanges(generators);
// strip out the content of the changes and any potential callback
const result = changes.map((change) => ({
generatorName: change.generatorName,
changes: change.changes.map((c) => ({ ...c, content: null })),
outOfSyncMessage: change.outOfSyncMessage,
}));
const result = changes.map((change) =>
'error' in change
? change
: // strip out the content of the changes and any potential callback
{
generatorName: change.generatorName,
changes: change.changes.map((c) => ({ ...c, content: null })),
outOfSyncMessage: change.outOfSyncMessage,
}
);
return {
response: JSON.stringify(result),
@@ -1,9 +1,9 @@
import type { SyncGeneratorChangesResult } from '../../utils/sync-generators';
import type { SyncGeneratorRunResult } from '../../utils/sync-generators';
import { _getConflictingGeneratorGroups } from './sync-generators';
describe('_getConflictingGeneratorGroups', () => {
it('should return grouped conflicting generators', () => {
const results: SyncGeneratorChangesResult[] = [
const results: SyncGeneratorRunResult[] = [
{
generatorName: 'a',
changes: [
@@ -9,7 +9,9 @@ import {
collectRegisteredGlobalSyncGenerators,
flushSyncGeneratorChanges,
runSyncGenerator,
type SyncGeneratorChangesResult,
type FlushSyncGeneratorChangesResult,
type SyncGeneratorRunResult,
type SyncGeneratorRunSuccessResult,
} from '../../utils/sync-generators';
import { workspaceRoot } from '../../utils/workspace-root';
import { serverLogger } from './logger';
@@ -17,7 +19,7 @@ import { getCachedSerializedProjectGraphPromise } from './project-graph-incremen
const syncGeneratorsCacheResultPromises = new Map<
string,
Promise<SyncGeneratorChangesResult>
Promise<SyncGeneratorRunResult>
>();
let registeredTaskSyncGenerators = new Set<string>();
let registeredGlobalSyncGenerators = new Set<string>();
@@ -36,7 +38,7 @@ const log = (...messageParts: unknown[]) => {
export async function getCachedSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
try {
log('get sync generators changes on demand', generators);
// this is invoked imperatively, so we clear any scheduled run
@@ -70,7 +72,7 @@ export async function getCachedSyncGeneratorChanges(
export async function flushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<void> {
): Promise<FlushSyncGeneratorChangesResult> {
log('flush sync generators changes', generators);
const results = await getCachedSyncGeneratorChanges(generators);
@@ -79,7 +81,7 @@ export async function flushSyncGeneratorChangesToDisk(
syncGeneratorsCacheResultPromises.delete(generator);
}
await flushSyncGeneratorChanges(results);
return await flushSyncGeneratorChanges(results);
}
export function collectAndScheduleSyncGenerators(
@@ -154,7 +156,7 @@ export async function getCachedRegisteredSyncGenerators(): Promise<string[]> {
async function getFromCacheOrRunGenerators(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
let projects: Record<string, ProjectConfiguration> | null;
let errored = false;
const getProjectsConfigurations = async () => {
@@ -223,7 +225,7 @@ async function getFromCacheOrRunGenerators(
async function runConflictingGenerators(
tree: Tree,
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
const { projectGraph } = await getCachedSerializedProjectGraphPromise();
const projects = projectGraph
? readProjectsConfigurationFromProjectGraph(projectGraph).projects
@@ -246,7 +248,7 @@ async function runConflictingGenerators(
}
// we need to run conflicting generators sequentially because they use the same tree
const results: SyncGeneratorChangesResult[] = [];
const results: SyncGeneratorRunResult[] = [];
for (const generator of generators) {
log(generator, 'running it now');
results.push(await runGenerator(generator, projects, tree));
@@ -257,16 +259,17 @@ async function runConflictingGenerators(
async function processConflictingGenerators(
conflicts: string[][],
initialResults: SyncGeneratorChangesResult[]
): Promise<SyncGeneratorChangesResult[]> {
initialResults: SyncGeneratorRunResult[]
): Promise<SyncGeneratorRunResult[]> {
const conflictRunResults = (
await Promise.all(
conflicts.map((generators) => {
const [firstGenerator, ...generatorsToRun] = generators;
// it must exists because the conflicts were identified from the initial results
// and it's guaranteed to be a success result
const firstGeneratorResult = initialResults.find(
(r) => r.generatorName === firstGenerator
)!;
)! as SyncGeneratorRunSuccessResult;
const tree = new FsTree(
workspaceRoot,
@@ -319,10 +322,14 @@ async function processConflictingGenerators(
* @internal
*/
export function _getConflictingGeneratorGroups(
results: SyncGeneratorChangesResult[]
results: SyncGeneratorRunResult[]
): string[][] {
const changedFileToGeneratorMap = new Map<string, Set<string>>();
for (const result of results) {
if ('error' in result) {
continue;
}
for (const change of result.changes) {
if (!changedFileToGeneratorMap.has(change.path)) {
changedFileToGeneratorMap.set(change.path, new Set());
@@ -419,7 +426,7 @@ function runGenerator(
generator: string,
projects: Record<string, ProjectConfiguration>,
tree?: Tree
): Promise<SyncGeneratorChangesResult> {
): Promise<SyncGeneratorRunResult> {
log('running scheduled generator', generator);
// remove it from the scheduled set
scheduledGenerators.delete(generator);
@@ -430,7 +437,11 @@ function runGenerator(
);
return runSyncGenerator(tree, generator, projects).then((result) => {
log(generator, 'changes:', result.changes.map((c) => c.path).join(', '));
if ('error' in result) {
log(generator, 'error:', result.error.message);
} else {
log(generator, 'changes:', result.changes.map((c) => c.path).join(', '));
}
return result;
});
}
@@ -157,17 +157,17 @@ describe('native task hasher', () => {
"env:TESTENV": "11441948532827618368",
"parent:ProjectConfiguration": "3608670998275221195",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"parent:{projectRoot}/**/*": "17059468255294227635",
"runtime:echo runtime123": "29846575039086708",
"tagged:ProjectConfiguration": "8596726088057301092",
"tagged:TsConfig": "2264969541778889434",
"tagged:{projectRoot}/**/*": "112200405683630828",
"tagged:{projectRoot}/**/*": "14666997081331501901",
"unrelated:ProjectConfiguration": "11133337791644294114",
"unrelated:TsConfig": "2264969541778889434",
"unrelated:{projectRoot}/**/*": "10505120368757496776",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192",
"unrelated:{projectRoot}/**/*": "4127219831408253695",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "18099427347122160586",
},
"value": "13049022000906481001",
"value": "14471888706892195399",
},
]
`);
@@ -226,13 +226,13 @@ describe('native task hasher', () => {
"AllExternalDependencies": "3244421341483603138",
"child:ProjectConfiguration": "710102491746666394",
"child:TsConfig": "2264969541778889434",
"child:{projectRoot}/**/*": "7694964870822928111",
"child:{projectRoot}/**/*": "3347149359534435991",
"parent:ProjectConfiguration": "8031122597231773116",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "18099427347122160586",
},
"value": "17442516481637512275",
"value": "2877551238604232699",
}
`);
});
@@ -305,13 +305,13 @@ describe('native task hasher', () => {
"AllExternalDependencies": "3244421341483603138",
"child:ProjectConfiguration": "13051054958929525761",
"child:TsConfig": "2264969541778889434",
"child:{projectRoot}/**/*": "7694964870822928111",
"parent:!{projectRoot}/**/*.spec.ts": "7663204892242899157",
"child:{projectRoot}/**/*": "3347149359534435991",
"parent:!{projectRoot}/**/*.spec.ts": "8911122541468969799",
"parent:ProjectConfiguration": "3608670998275221195",
"parent:TsConfig": "2264969541778889434",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "11114659294156087056",
},
"value": "3497993078654537309",
"value": "5146047476750743843",
}
`);
});
@@ -372,22 +372,22 @@ describe('native task hasher', () => {
{
"details": {
"AllExternalDependencies": "3244421341483603138",
"parent:!{projectRoot}/**/*.spec.ts": "7663204892242899157",
"parent:!{projectRoot}/**/*.spec.ts": "8911122541468969799",
"parent:ProjectConfiguration": "16402137858974842465",
"parent:TsConfig": "2264969541778889434",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "11114659294156087056",
},
"value": "10775755355957559912",
"value": "845448225466199915",
},
{
"details": {
"AllExternalDependencies": "3244421341483603138",
"parent:ProjectConfiguration": "16402137858974842465",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "11114659294156087056",
},
"value": "13219368697419749776",
"value": "16342992587503026008",
},
]
`);
@@ -467,18 +467,18 @@ describe('native task hasher', () => {
{
"details": {
"AllExternalDependencies": "3244421341483603138",
"child:!{projectRoot}/**/*.spec.ts": "13790135045935437026",
"child:!{projectRoot}/**/*.spec.ts": "6212660753359890679",
"child:ProjectConfiguration": "10085593111011845427",
"child:TsConfig": "2264969541778889434",
"env:MY_TEST_HASH_ENV": "17357374746554314488",
"parent:ProjectConfiguration": "14398811678394411425",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/global1]": "13078141817211771580",
"workspace:[{workspaceRoot}/global2]": "13625885481717016690",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "10897751101872977225",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/global1]": "14542405497386871555",
"workspace:[{workspaceRoot}/global2]": "12932836274958677781",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "12076281115618125366",
},
"value": "14298810822113951946",
"value": "8928030752507058",
},
]
`);
@@ -529,10 +529,10 @@ describe('native task hasher', () => {
"AllExternalDependencies": "3244421341483603138",
"parent:ProjectConfiguration": "3608670998275221195",
"parent:TsConfig": "8661678577354855152",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "18099427347122160586",
},
"value": "10821775409399212451",
"value": "10182005362255577288",
}
`);
});
@@ -604,13 +604,13 @@ describe('native task hasher', () => {
"AllExternalDependencies": "3244421341483603138",
"child:ProjectConfiguration": "13748859057138736105",
"child:TsConfig": "2264969541778889434",
"child:{projectRoot}/**/*": "7694964870822928111",
"child:{projectRoot}/**/*": "3347149359534435991",
"parent:ProjectConfiguration": "3608670998275221195",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "18099427347122160586",
},
"value": "12197760444984597111",
"value": "12868980333884890989",
}
`);
@@ -626,13 +626,13 @@ describe('native task hasher', () => {
"AllExternalDependencies": "3244421341483603138",
"child:ProjectConfiguration": "13748859057138736105",
"child:TsConfig": "2264969541778889434",
"child:{projectRoot}/**/*": "7694964870822928111",
"child:{projectRoot}/**/*": "3347149359534435991",
"parent:ProjectConfiguration": "3608670998275221195",
"parent:TsConfig": "2264969541778889434",
"parent:{projectRoot}/**/*": "15295586939211629225",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192",
"parent:{projectRoot}/**/*": "17059468255294227635",
"workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "18099427347122160586",
},
"value": "12197760444984597111",
"value": "12868980333884890989",
}
`);
});
@@ -18,6 +18,7 @@ pub fn hash_project_files(
let mut hasher = xxhash_rust::xxh3::Xxh3::new();
for file in collected_files {
hasher.update(file.hash.as_bytes());
hasher.update(file.file.as_bytes());
}
Ok(hasher.digest().to_string())
}
@@ -157,7 +158,12 @@ mod tests {
let hash_result = hash_project_files(proj_name, proj_root, file_sets, &file_map).unwrap();
assert_eq!(
hash_result,
hash(&[file_data1.hash.as_bytes(), file_data3.hash.as_bytes()].concat())
hash(&[
file_data1.hash.as_bytes(),
file_data1.file.as_bytes(),
file_data3.hash.as_bytes(),
file_data3.file.as_bytes()
].concat())
);
}
@@ -199,7 +205,12 @@ mod tests {
let hash_result = hash_project_files(proj_name, proj_root, file_sets, &file_map).unwrap();
assert_eq!(
hash_result,
hash(&[file_data1.hash.as_bytes(), file_data3.hash.as_bytes()].concat())
hash(&[
file_data1.hash.as_bytes(),
file_data1.file.as_bytes(),
file_data3.hash.as_bytes(),
file_data3.file.as_bytes(),
].concat())
);
}
}
@@ -53,7 +53,8 @@ pub fn hash_workspace_files(
.filter(|file| glob.is_match(&file.file))
{
trace!("{:?} was found with glob {:?}", file.file, globs);
hashes.push(file.hash.clone())
hashes.push(file.hash.clone());
hashes.push(file.file.clone());
}
hasher.update(hashes.join(",").as_bytes());
let hashed_value = hasher.digest().to_string();
@@ -110,6 +111,9 @@ mod test {
Arc::new(DashMap::new()),
)
.unwrap();
assert_eq!(result, hash(gitignore_file.hash.as_bytes()));
assert_eq!(result, hash([
gitignore_file.hash,
gitignore_file.file
].join(",").as_bytes()));
}
}
@@ -140,12 +140,6 @@ export async function connectToNxCloud(
const isGitHubDetected =
schema.github ?? (await repoUsesGithub(schema.github));
let responseFromCreateNxCloudWorkspaceV1:
| {
token: string;
}
| undefined;
let responseFromCreateNxCloudWorkspaceV2:
| {
nxCloudId: string;
@@ -21,3 +21,7 @@ export function getCloudUrl() {
export function removeTrailingSlash(apiUrl: string) {
return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl;
}
export function isNxCloudId(token: string): boolean {
return token.length === 24;
}
@@ -1,15 +1,16 @@
import { getCloudUrl } from './get-cloud-options';
import { getCloudUrl, isNxCloudId } from './get-cloud-options';
export async function isWorkspaceClaimed(nxCloudAccessToken) {
if (!nxCloudAccessToken) return false;
export async function isWorkspaceClaimed(accessToken: string) {
if (!accessToken) return false;
const apiUrl = getCloudUrl();
try {
const requestData = isNxCloudId(accessToken)
? { nxCloudId: accessToken }
: { nxCloudAccessToken: accessToken };
const response = await require('axios').post(
`${apiUrl}/nx-cloud/is-workspace-claimed`,
{
nxCloudAccessToken,
}
requestData
);
if (response.data.message) {
@@ -29,6 +29,11 @@ export async function getNxCloudAppOnBoardingUrl(token: string) {
export function readNxCloudToken(tree: Tree) {
const nxJson = readNxJson(tree);
const { accessToken } = getRunnerOptions('default', nxJson, {}, true);
return accessToken;
const { accessToken, nxCloudId } = getRunnerOptions(
'default',
nxJson,
{},
true
);
return accessToken || nxCloudId;
}
+34 -7
View File
@@ -42,7 +42,7 @@ export class ProjectGraphError extends Error {
this.#partialProjectGraph = partialProjectGraph;
this.#partialSourceMaps = partialSourceMaps;
this.stack = `${this.message}\n ${errors
.map((error) => error.stack.split('\n').join('\n '))
.map((error) => indentString(formatErrorStackAndCause(error), 2))
.join('\n')}`;
}
@@ -263,15 +263,21 @@ export class MergeNodesError extends Error {
this.name = this.constructor.name;
this.file = file;
this.pluginName = pluginName;
this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`;
this.stack = `${this.message}\n${indentString(
formatErrorStackAndCause(error),
2
)}`;
}
}
export class CreateMetadataError extends Error {
constructor(public readonly error: Error, public readonly plugin: string) {
super(`The "${plugin}" plugin threw an error while creating metadata:`, {
cause: error,
});
super(
`The "${plugin}" plugin threw an error while creating metadata: ${error.message}`,
{
cause: error,
}
);
this.name = this.constructor.name;
}
}
@@ -279,7 +285,7 @@ export class CreateMetadataError extends Error {
export class ProcessDependenciesError extends Error {
constructor(public readonly pluginName: string, { cause }) {
super(
`The "${pluginName}" plugin threw an error while creating dependencies:`,
`The "${pluginName}" plugin threw an error while creating dependencies: ${cause.message}`,
{
cause,
}
@@ -316,7 +322,7 @@ export class ProcessProjectGraphError extends Error {
}
);
this.name = this.constructor.name;
this.stack = `${this.message}\n ${cause.stack.split('\n').join('\n ')}`;
this.stack = `${this.message}\n${indentString(cause, 2)}`;
}
}
@@ -394,3 +400,24 @@ export class LoadPluginError extends Error {
this.name = this.constructor.name;
}
}
function indentString(str: string, indent: number): string {
return (
' '.repeat(indent) +
str
.split('\n')
.map((line) => ' '.repeat(indent) + line)
.join('\n')
);
}
function formatErrorStackAndCause(error: Error): string {
const cause =
error.cause && error.cause instanceof Error ? error.cause : null;
return (
error.stack +
(cause
? `\nCaused by: \n${indentString(cause.stack ?? cause.message, 2)}`
: '')
);
}
+173 -28
View File
@@ -13,18 +13,22 @@ import { TargetDependencyConfig } from '../config/workspace-json-project-json';
import { daemonClient } from '../daemon/client/client';
import { createTaskHasher } from '../hasher/create-task-hasher';
import { hashTasksThatDoNotDependOnOutputsOfOtherTasks } from '../hasher/hash-task';
import { IS_WASM } from '../native';
import { createProjectGraphAsync } from '../project-graph/project-graph';
import { NxArgs } from '../utils/command-line-utils';
import { isRelativePath } from '../utils/fileutils';
import { isCI } from '../utils/is-ci';
import { isNxCloudUsed } from '../utils/nx-cloud-utils';
import { output } from '../utils/output';
import { handleErrors } from '../utils/params';
import { handleErrors } from '../utils/handle-errors';
import {
collectEnabledTaskSyncGeneratorsFromTaskGraph,
flushSyncGeneratorChanges,
getFailedSyncGeneratorsFixMessageLines,
getFlushFailureMessageLines,
getSyncGeneratorChanges,
syncGeneratorResultsToMessageLines,
getSyncGeneratorSuccessResultsMessageLines,
processSyncGeneratorResultErrors,
} from '../utils/sync-generators';
import { workspaceRoot } from '../utils/workspace-root';
import { createTaskGraph } from './create-task-graph';
@@ -46,7 +50,6 @@ import {
import { TasksRunner, TaskStatus } from './tasks-runner';
import { shouldStreamOutput } from './utils';
import chalk = require('chalk');
import { IS_WASM } from '../native';
async function getTerminalOutputLifeCycle(
initiatingProject: string,
@@ -256,18 +259,66 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(
return { projectGraph, taskGraph };
}
const {
failedGeneratorsCount,
areAllResultsFailures,
anySyncGeneratorsFailed,
} = processSyncGeneratorResultErrors(results);
const failedSyncGeneratorsFixMessageLines =
getFailedSyncGeneratorsFixMessageLines(results, nxArgs.verbose);
const outOfSyncTitle = 'The workspace is out of sync';
const resultBodyLines = [...syncGeneratorResultsToMessageLines(results), ''];
const resultBodyLines = getSyncGeneratorSuccessResultsMessageLines(results);
const fixMessage =
'You can manually run `nx sync` to update your workspace or you can set `sync.applyChanges` to `true` in your `nx.json` to apply the changes automatically when running tasks in interactive environments.';
const willErrorOnCiMessage = 'Please note that this will be an error on CI.';
'You can manually run `nx sync` to update your workspace with the identified changes or you can set `sync.applyChanges` to `true` in your `nx.json` to apply the changes automatically when running tasks in interactive environments.';
const willErrorOnCiMessage =
'Please note that having the workspace out of sync will result in an error in CI.';
if (isCI() || !process.stdout.isTTY) {
// If the user is running in CI or is running in a non-TTY environment we
// throw an error to stop the execution of the tasks.
throw new Error(
`${outOfSyncTitle}\n${resultBodyLines.join('\n')}\n${fixMessage}`
);
if (areAllResultsFailures) {
output.error({
title: `The workspace is probably out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});
} else {
output.error({
title: outOfSyncTitle,
bodyLines: [...resultBodyLines, '', fixMessage],
});
if (anySyncGeneratorsFailed) {
output.error({
title:
failedGeneratorsCount === 1
? 'A sync generator failed to run'
: 'Some sync generators failed to run',
bodyLines: failedSyncGeneratorsFixMessageLines,
});
}
}
process.exit(1);
}
if (areAllResultsFailures) {
output.warn({
title: `The workspace is probably out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});
await confirmRunningTasksWithSyncFailures();
// if all sync generators failed to run there's nothing to sync, we just let the tasks run
return { projectGraph, taskGraph };
}
if (nxJson.sync?.applyChanges === false) {
@@ -279,11 +330,25 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(
title: outOfSyncTitle,
bodyLines: [
...resultBodyLines,
'Your workspace is set to not apply changes automatically (`sync.applyChanges` is set to `false` in your `nx.json`).',
'',
'Your workspace is set to not apply the identified changes automatically (`sync.applyChanges` is set to `false` in your `nx.json`).',
willErrorOnCiMessage,
fixMessage,
],
});
if (anySyncGeneratorsFailed) {
output.warn({
title:
failedGeneratorsCount === 1
? 'A sync generator failed to run'
: 'Some sync generators failed to run',
bodyLines: failedSyncGeneratorsFixMessageLines,
});
await confirmRunningTasksWithSyncFailures();
}
return { projectGraph, taskGraph };
}
@@ -291,8 +356,9 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(
title: outOfSyncTitle,
bodyLines: [
...resultBodyLines,
'',
nxJson.sync?.applyChanges === true
? 'Proceeding to sync the changes automatically (`sync.applyChanges` is set to `true` in your `nx.json`).'
? 'Proceeding to sync the identified changes automatically (`sync.applyChanges` is set to `true` in your `nx.json`).'
: willErrorOnCiMessage,
],
});
@@ -306,7 +372,24 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(
spinner.start();
// Flush sync generator changes to disk
await flushSyncGeneratorChanges(results);
const flushResult = await flushSyncGeneratorChanges(results);
if ('generatorFailures' in flushResult) {
spinner.fail();
output.error({
title: 'Failed to sync the workspace',
bodyLines: [
...getFlushFailureMessageLines(flushResult, nxArgs.verbose),
...(flushResult.generalFailure
? [
'If needed, you can run the tasks with the `--skip-sync` flag to disable syncing.',
]
: []),
],
});
await confirmRunningTasksWithSyncFailures();
}
// Re-create project graph and task graph
projectGraph = await createProjectGraphAsync();
@@ -319,25 +402,52 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(
extraOptions
);
if (nxJson.sync?.applyChanges === true) {
spinner.succeed(`The workspace was synced successfully!
const successTitle = anySyncGeneratorsFailed
? // the identified changes were synced successfully, but the workspace
// is still not up to date, which we'll mention next
'The identified changes were synced successfully!'
: // the workspace is fully up to date
'The workspace was synced successfully!';
const successSubtitle =
nxJson.sync?.applyChanges === true
? 'Please make sure to commit the changes to your repository or this will error in CI.'
: // The user was prompted and we already logged a message about erroring in CI
// so here we just tell them to commit the changes.
'Please make sure to commit the changes to your repository.';
spinner.succeed(`${successTitle}\n\n${successSubtitle}`);
Please make sure to commit the changes to your repository or this will error on CI.`);
} else {
// The user was prompted and we already logged a message about erroring on CI
// so here we just tell them to commit the changes.
spinner.succeed(`The workspace was synced successfully!
if (anySyncGeneratorsFailed) {
output.warn({
title: `The workspace is probably still out of sync because ${
failedGeneratorsCount === 1
? 'a sync generator'
: 'some sync generators'
} failed to run`,
bodyLines: failedSyncGeneratorsFixMessageLines,
});
Please make sure to commit the changes to your repository.`);
await confirmRunningTasksWithSyncFailures();
}
} else {
output.warn({
title: 'Syncing the workspace was skipped',
bodyLines: [
'This could lead to unexpected results or errors when running tasks.',
fixMessage,
],
});
if (anySyncGeneratorsFailed) {
output.warn({
title:
failedGeneratorsCount === 1
? 'A sync generator failed to report the sync status'
: 'Some sync generators failed to report the sync status',
bodyLines: failedSyncGeneratorsFixMessageLines,
});
await confirmRunningTasksWithSyncFailures();
} else {
output.warn({
title: 'Syncing the workspace was skipped',
bodyLines: [
'This could lead to unexpected results or errors when running tasks.',
fixMessage,
],
});
}
}
return { projectGraph, taskGraph };
@@ -349,7 +459,7 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
name: 'applyChanges',
type: 'select',
message:
'Would you like to sync the changes to get your worskpace up to date?',
'Would you like to sync the identified changes to get your worskpace up to date?',
choices: [
{
name: 'yes',
@@ -374,6 +484,41 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
}
}
async function confirmRunningTasksWithSyncFailures(): Promise<void> {
try {
const promptConfig = {
name: 'runTasks',
type: 'select',
message:
'Would you like to ignore the sync failures and continue running the tasks?',
choices: [
{
name: 'yes',
message: 'Yes, ignore the failures and run the tasks',
},
{
name: 'no',
message: `No, don't run the tasks`,
},
],
footer: () =>
chalk.dim(
`\nWhen running in CI and there are sync failures, the tasks won't run. Addressing the errors above is highly recommended to prevent failures in CI.`
),
};
const runTasks = await prompt<{ runTasks: 'yes' | 'no' }>([
promptConfig,
]).then(({ runTasks }) => runTasks === 'yes');
if (!runTasks) {
process.exit(1);
}
} catch {
process.exit(1);
}
}
function setEnvVarsBasedOnArgs(nxArgs: NxArgs, loadDotEnvFiles: boolean) {
if (
nxArgs.outputStyle == 'stream' ||
@@ -0,0 +1,70 @@
import {
CreateMetadataError,
ProjectGraphError,
} from '../project-graph/error-types';
import { handleErrors } from './handle-errors';
import { output } from './output';
describe('handleErrors', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should display project graph error cause message', async () => {
const spy = jest.spyOn(output, 'error').mockImplementation(() => {});
await handleErrors(true, async () => {
const cause = new Error('cause message');
const metadataError = new CreateMetadataError(cause, 'test-plugin');
throw new ProjectGraphError(
[metadataError],
{ nodes: {}, dependencies: {} },
{}
);
});
const { bodyLines, title } = spy.mock.calls[0][0];
const body = bodyLines.join('\n');
expect(body).toContain('cause message');
expect(body).toContain('test-plugin');
});
it('should only display wrapper error if not verbose', async () => {
const spy = jest.spyOn(output, 'error').mockImplementation(() => {});
await handleErrors(false, async () => {
const cause = new Error('cause message');
const metadataError = new CreateMetadataError(cause, 'test-plugin');
throw new ProjectGraphError(
[metadataError],
{ nodes: {}, dependencies: {} },
{}
);
});
const { bodyLines, title } = spy.mock.calls[0][0];
const body = bodyLines.join('\n');
expect(body).not.toContain('cause message');
});
it('should display misc errors that do not have a cause', async () => {
const spy = jest.spyOn(output, 'error').mockImplementation(() => {});
await handleErrors(true, async () => {
throw new Error('misc error');
});
const { bodyLines, title } = spy.mock.calls[0][0];
const body = bodyLines.join('\n');
expect(body).toContain('misc error');
expect(body).not.toMatch(/[Cc]ause/);
});
it('should display misc errors that have a cause', async () => {
const spy = jest.spyOn(output, 'error').mockImplementation(() => {});
await handleErrors(true, async () => {
const cause = new Error('cause message');
const err = new Error('misc error', { cause });
throw err;
});
const { bodyLines, title } = spy.mock.calls[0][0];
const body = bodyLines.join('\n');
expect(body).toContain('misc error');
expect(body).toContain('cause message');
});
});
+74
View File
@@ -0,0 +1,74 @@
import { daemonClient } from '../daemon/client/client';
import { ProjectGraphError } from '../project-graph/error-types';
import { logger } from './logger';
import { output } from './output';
export async function handleErrors(
isVerbose: boolean,
fn: Function
): Promise<number> {
try {
const result = await fn();
if (typeof result === 'number') {
return result;
}
return 0;
} catch (err) {
err ||= new Error('Unknown error caught');
if (err.constructor.name === 'UnsuccessfulWorkflowExecution') {
logger.error('The generator workflow failed. See above.');
} else if (err.name === 'ProjectGraphError') {
const projectGraphError = err as ProjectGraphError;
let title = projectGraphError.message;
if (
projectGraphError.cause &&
typeof projectGraphError.cause === 'object' &&
'message' in projectGraphError.cause
) {
title += ' ' + projectGraphError.cause.message + '.';
}
if (isVerbose) {
title += ' See errors below.';
}
const bodyLines = isVerbose
? formatErrorStackAndCause(projectGraphError)
: ['Pass --verbose to see the stacktraces.'];
output.error({
title,
bodyLines: bodyLines,
});
} else {
const lines = (err.message ? err.message : err.toString()).split('\n');
const bodyLines: string[] = lines.slice(1);
if (isVerbose) {
bodyLines.push(...formatErrorStackAndCause(err));
} else if (err.stack) {
bodyLines.push('Pass --verbose to see the stacktrace.');
}
output.error({
title: lines[0],
bodyLines,
});
}
if (daemonClient.enabled()) {
daemonClient.reset();
}
return 1;
}
}
function formatErrorStackAndCause<T extends Error>(error: T): string[] {
return [
error.stack || error.message,
...(error.cause && typeof error.cause === 'object'
? [
'Caused by:',
'stack' in error.cause
? error.cause.stack.toString()
: error.cause.toString(),
]
: []),
];
}
-18
View File
@@ -23,21 +23,3 @@ export function getNxCloudUrl(nxJson: NxJsonConfiguration): string {
throw new Error('nx-cloud runner not found in nx.json');
return cloudRunner?.options?.url ?? nxJson.nxCloudUrl ?? 'https://nx.app';
}
export function getNxCloudToken(nxJson: NxJsonConfiguration): string {
const cloudRunner = Object.values(nxJson.tasksRunnerOptions ?? {}).find(
(r) => r.runner == '@nrwl/nx-cloud' || r.runner == 'nx-cloud'
);
if (
!cloudRunner &&
!(nxJson.nxCloudAccessToken || process.env.NX_CLOUD_ACCESS_TOKEN)
)
throw new Error('nx-cloud runner not found in nx.json');
return (
process.env.NX_CLOUD_ACCESS_TOKEN ??
cloudRunner?.options.accessToken ??
nxJson.nxCloudAccessToken
);
}
-50
View File
@@ -89,56 +89,6 @@ export type Options = {
[k: string]: string | number | boolean | string[] | Unmatched[] | undefined;
};
export async function handleErrors(
isVerbose: boolean,
fn: Function
): Promise<number> {
try {
const result = await fn();
if (typeof result === 'number') {
return result;
}
return 0;
} catch (err) {
err ||= new Error('Unknown error caught');
if (err.constructor.name === 'UnsuccessfulWorkflowExecution') {
logger.error('The generator workflow failed. See above.');
} else if (err.name === 'ProjectGraphError') {
const projectGraphError = err as ProjectGraphError;
let title = projectGraphError.message;
if (isVerbose) {
title += ' See errors below.';
}
const bodyLines = isVerbose
? [projectGraphError.stack]
: ['Pass --verbose to see the stacktraces.'];
output.error({
title,
bodyLines: bodyLines,
});
} else {
const lines = (err.message ? err.message : err.toString()).split('\n');
const bodyLines = lines.slice(1);
if (err.stack && !isVerbose) {
bodyLines.push('Pass --verbose to see the stacktrace.');
}
output.error({
title: lines[0],
bodyLines,
});
if (err.stack && isVerbose) {
logger.info(err.stack);
}
}
if (daemonClient.enabled()) {
daemonClient.reset();
}
return 1;
}
}
function camelCase(input: string): string {
if (input.indexOf('-') > 1) {
return input
@@ -89,11 +89,14 @@ export async function getPluginCapabilities(
pluginModule &&
('processProjectGraph' in pluginModule ||
'createNodes' in pluginModule ||
'createNodesV2' in pluginModule ||
'createMetadata' in pluginModule ||
'createDependencies' in pluginModule),
projectInference:
pluginModule &&
('projectFilePatterns' in pluginModule ||
'createNodes' in pluginModule),
'createNodes' in pluginModule ||
'createNodesV2' in pluginModule),
};
} catch {
return null;
+250 -81
View File
@@ -31,18 +31,46 @@ export type SyncGenerator = (
tree: Tree
) => SyncGeneratorResult | Promise<SyncGeneratorResult>;
export type SyncGeneratorChangesResult = {
changes: FileChange[];
export type SyncGeneratorRunSuccessResult = {
generatorName: string;
changes: FileChange[];
callback?: GeneratorCallback;
outOfSyncMessage?: string;
};
// Error is not serializable, so we use a simple object instead
type SerializableSimpleError = {
message: string;
stack: string | undefined;
};
export type SyncGeneratorRunErrorResult = {
generatorName: string;
error: SerializableSimpleError;
};
export type SyncGeneratorRunResult =
| SyncGeneratorRunSuccessResult
| SyncGeneratorRunErrorResult;
type FlushSyncGeneratorChangesSuccess = { success: true };
type FlushSyncGeneratorFailure = {
generator: string;
error: SerializableSimpleError;
};
type FlushSyncGeneratorChangesFailure = {
generatorFailures: FlushSyncGeneratorFailure[];
generalFailure?: SerializableSimpleError;
};
export type FlushSyncGeneratorChangesResult =
| FlushSyncGeneratorChangesSuccess
| FlushSyncGeneratorChangesFailure;
export async function getSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
performance.mark('get-sync-generators-changes:start');
let results: SyncGeneratorChangesResult[];
let results: SyncGeneratorRunResult[];
if (!daemonClient.enabled()) {
results = await runSyncGenerators(generators);
@@ -57,19 +85,19 @@ export async function getSyncGeneratorChanges(
'get-sync-generators-changes:end'
);
return results.filter((r) => r.changes.length > 0);
return results.filter((r) => ('error' in r ? true : r.changes.length > 0));
}
export async function flushSyncGeneratorChanges(
results: SyncGeneratorChangesResult[]
): Promise<void> {
results: SyncGeneratorRunResult[]
): Promise<FlushSyncGeneratorChangesResult> {
if (isOnDaemon() || !daemonClient.enabled()) {
await flushSyncGeneratorChangesToDisk(results);
} else {
await daemonClient.flushSyncGeneratorChangesToDisk(
results.map((r) => r.generatorName)
);
return await flushSyncGeneratorChangesToDisk(results);
}
return await daemonClient.flushSyncGeneratorChangesToDisk(
results.map((r) => r.generatorName)
);
}
export async function collectAllRegisteredSyncGenerators(
@@ -90,38 +118,45 @@ export async function runSyncGenerator(
tree: Tree,
generatorSpecifier: string,
projects: Record<string, ProjectConfiguration>
): Promise<SyncGeneratorChangesResult> {
performance.mark(`run-sync-generator:${generatorSpecifier}:start`);
const { collection, generator } = parseGeneratorString(generatorSpecifier);
const { implementationFactory } = getGeneratorInformation(
collection,
generator,
workspaceRoot,
projects
);
const implementation = implementationFactory() as SyncGenerator;
const result = await implementation(tree);
): Promise<SyncGeneratorRunResult> {
try {
performance.mark(`run-sync-generator:${generatorSpecifier}:start`);
const { collection, generator } = parseGeneratorString(generatorSpecifier);
const { implementationFactory } = getGeneratorInformation(
collection,
generator,
workspaceRoot,
projects
);
const implementation = implementationFactory() as SyncGenerator;
const result = await implementation(tree);
let callback: GeneratorCallback | undefined;
let outOfSyncMessage: string | undefined;
if (result && typeof result === 'object') {
callback = result.callback;
outOfSyncMessage = result.outOfSyncMessage;
let callback: GeneratorCallback | undefined;
let outOfSyncMessage: string | undefined;
if (result && typeof result === 'object') {
callback = result.callback;
outOfSyncMessage = result.outOfSyncMessage;
}
performance.mark(`run-sync-generator:${generatorSpecifier}:end`);
performance.measure(
`run-sync-generator:${generatorSpecifier}`,
`run-sync-generator:${generatorSpecifier}:start`,
`run-sync-generator:${generatorSpecifier}:end`
);
return {
changes: tree.listChanges(),
generatorName: generatorSpecifier,
callback,
outOfSyncMessage,
};
} catch (e) {
return {
generatorName: generatorSpecifier,
error: { message: e.message, stack: e.stack },
};
}
performance.mark(`run-sync-generator:${generatorSpecifier}:end`);
performance.measure(
`run-sync-generator:${generatorSpecifier}`,
`run-sync-generator:${generatorSpecifier}:start`,
`run-sync-generator:${generatorSpecifier}:end`
);
return {
changes: tree.listChanges(),
generatorName: generatorSpecifier,
callback,
outOfSyncMessage,
};
}
export function collectEnabledTaskSyncGeneratorsFromProjectGraph(
@@ -205,12 +240,16 @@ export function collectRegisteredGlobalSyncGenerators(
return globalSyncGenerators;
}
export function syncGeneratorResultsToMessageLines(
results: SyncGeneratorChangesResult[]
export function getSyncGeneratorSuccessResultsMessageLines(
results: SyncGeneratorRunResult[]
): string[] {
const messageLines: string[] = [];
for (const result of results) {
if ('error' in result) {
continue;
}
messageLines.push(
`The ${chalk.bold(
result.generatorName
@@ -228,14 +267,108 @@ export function syncGeneratorResultsToMessageLines(
return messageLines;
}
export function getFailedSyncGeneratorsFixMessageLines(
results: SyncGeneratorRunResult[],
verbose: boolean
): string[] {
const messageLines: string[] = [];
const generators: string[] = [];
for (const result of results) {
if ('error' in result) {
messageLines.push(
`The ${chalk.bold(
result.generatorName
)} sync generator reported the following error:
${chalk.bold(result.error.message)}${
verbose && result.error.stack ? '\n' + result.error.stack : ''
}`
);
generators.push(result.generatorName);
}
}
messageLines.push(
...getFailedSyncGeneratorsMessageLines(generators, verbose)
);
return messageLines;
}
export function getFlushFailureMessageLines(
result: FlushSyncGeneratorChangesFailure,
verbose: boolean
): string[] {
const messageLines: string[] = [];
const generators: string[] = [];
for (const failure of result.generatorFailures) {
messageLines.push(
`The ${chalk.bold(
failure.generator
)} sync generator failed to apply its changes with the following error:
${chalk.bold(failure.error.message)}${
verbose && failure.error.stack ? '\n' + failure.error.stack : ''
}`
);
generators.push(failure.generator);
}
messageLines.push(
...getFailedSyncGeneratorsMessageLines(generators, verbose)
);
if (result.generalFailure) {
if (messageLines.length > 0) {
messageLines.push('');
messageLines.push('Additionally, an unexpected error occurred:');
} else {
messageLines.push('An unexpected error occurred:');
}
messageLines.push(
...[
'',
result.generalFailure.message,
...(verbose && !!result.generalFailure.stack
? [`\n${result.generalFailure.stack}`]
: []),
'',
verbose
? 'Please report the error at: https://github.com/nrwl/nx/issues/new/choose'
: 'Please run with `--verbose` and report the error at: https://github.com/nrwl/nx/issues/new/choose',
]
);
}
return messageLines;
}
export function processSyncGeneratorResultErrors(
results: SyncGeneratorRunResult[]
) {
let failedGeneratorsCount = 0;
for (const result of results) {
if ('error' in result) {
failedGeneratorsCount++;
}
}
const areAllResultsFailures = failedGeneratorsCount === results.length;
const anySyncGeneratorsFailed = failedGeneratorsCount > 0;
return {
failedGeneratorsCount,
areAllResultsFailures,
anySyncGeneratorsFailed,
};
}
async function runSyncGenerators(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
): Promise<SyncGeneratorRunResult[]> {
const tree = new FsTree(workspaceRoot, false, 'running sync generators');
const projectGraph = await createProjectGraphAsync();
const { projects } = readProjectsConfigurationFromProjectGraph(projectGraph);
const results: SyncGeneratorChangesResult[] = [];
const results: SyncGeneratorRunResult[] = [];
for (const generator of generators) {
const result = await runSyncGenerator(tree, generator, projects);
results.push(result);
@@ -245,50 +378,21 @@ async function runSyncGenerators(
}
async function flushSyncGeneratorChangesToDisk(
results: SyncGeneratorChangesResult[]
): Promise<void> {
results: SyncGeneratorRunResult[]
): Promise<FlushSyncGeneratorChangesResult> {
performance.mark('flush-sync-generator-changes-to-disk:start');
const { changes, createdFiles, updatedFiles, deletedFiles, callbacks } =
processSyncGeneratorResults(results);
// Write changes to disk
flushChanges(workspaceRoot, changes);
// Run the callbacks
if (callbacks.length) {
for (const callback of callbacks) {
await callback();
}
}
// Update the context files
await updateContextWithChangedFiles(
workspaceRoot,
createdFiles,
updatedFiles,
deletedFiles
);
performance.mark('flush-sync-generator-changes-to-disk:end');
performance.measure(
'flush sync generator changes to disk',
'flush-sync-generator-changes-to-disk:start',
'flush-sync-generator-changes-to-disk:end'
);
}
function processSyncGeneratorResults(results: SyncGeneratorChangesResult[]) {
const changes: FileChange[] = [];
const createdFiles: string[] = [];
const updatedFiles: string[] = [];
const deletedFiles: string[] = [];
const callbacks: GeneratorCallback[] = [];
const generatorFailures: FlushSyncGeneratorFailure[] = [];
for (const result of results) {
if (result.callback) {
callbacks.push(result.callback);
if ('error' in result) {
continue;
}
for (const change of result.changes) {
changes.push(change);
if (change.type === 'CREATE') {
createdFiles.push(change.path);
} else if (change.type === 'UPDATE') {
@@ -297,7 +401,72 @@ function processSyncGeneratorResults(results: SyncGeneratorChangesResult[]) {
deletedFiles.push(change.path);
}
}
try {
// Write changes to disk
flushChanges(workspaceRoot, result.changes);
// Run the callback
if (result.callback) {
await result.callback();
}
} catch (e) {
generatorFailures.push({
generator: result.generatorName,
error: { message: e.message, stack: e.stack },
});
}
}
return { changes, createdFiles, updatedFiles, deletedFiles, callbacks };
try {
// Update the context files
await updateContextWithChangedFiles(
workspaceRoot,
createdFiles,
updatedFiles,
deletedFiles
);
performance.mark('flush-sync-generator-changes-to-disk:end');
performance.measure(
'flush sync generator changes to disk',
'flush-sync-generator-changes-to-disk:start',
'flush-sync-generator-changes-to-disk:end'
);
} catch (e) {
return {
generatorFailures,
generalFailure: { message: e.message, stack: e.stack },
};
}
return generatorFailures.length > 0
? { generatorFailures }
: { success: true };
}
function getFailedSyncGeneratorsMessageLines(
generators: string[],
verbose: boolean
): string[] {
const messageLines: string[] = [];
if (generators.length === 1) {
messageLines.push(
'',
verbose
? 'Please check the error above and address the issue.'
: 'Please check the error above and address the issue. You can provide the `--verbose` flag to get more details.',
`If needed, you can disable the failing sync generator by setting \`sync.disabledTaskSyncGenerators: ["${generators[0]}"]\` in your \`nx.json\`.`
);
} else if (generators.length > 1) {
const generatorsString = generators.map((g) => `"${g}"`).join(', ');
messageLines.push(
'',
verbose
? 'Please check the errors above and address the issues.'
: 'Please check the errors above and address the issues. You can provide the `--verbose` flag to get more details.',
`If needed, you can disable the failing sync generators by setting \`sync.disabledTaskSyncGenerators: [${generatorsString}]\` in your \`nx.json\`.`
);
}
return messageLines;
}
@@ -1,4 +1,4 @@
import { basename, dirname, join, relative, resolve } from 'path';
import { basename, dirname, join, parse, relative, resolve } from 'path';
import { statSync } from 'fs';
import {
normalizePath,
@@ -204,6 +204,18 @@ function normalizeRelativePaths(
for (const [fieldName, fieldValue] of Object.entries(options)) {
if (isRelativePath(fieldValue)) {
options[fieldName] = join(projectRoot, fieldValue);
} else if (fieldName === 'additionalEntryPoints') {
for (let i = 0; i < fieldValue.length; i++) {
const v = fieldValue[i];
if (isRelativePath(v)) {
fieldValue[i] = {
entryName: parse(v).name,
entryPath: join(projectRoot, v),
};
} else if (isRelativePath(v.entryPath)) {
v.entryPath = join(projectRoot, v.entryPath);
}
}
} else if (Array.isArray(fieldValue)) {
for (let i = 0; i < fieldValue.length; i++) {
if (isRelativePath(fieldValue[i])) {