Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 068b066b50 | |||
| 42cab264dc | |||
| c585aeaf1f | |||
| add2061f3e | |||
| afd07bbcf6 | |||
| e07d47a00b | |||
| 279e799b05 | |||
| b078d7587e | |||
| 9352f83dfa | |||
| a3fef980fe | |||
| 108dbcdab7 | |||
| 98d81c3168 | |||
| 2ece11d7fb | |||
| 8b2393e8bf | |||
| 5063a4cce7 | |||
| c398cbd760 |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 13 KiB |
@@ -59,18 +59,30 @@ if you are interested.
|
||||
|
||||

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

|
||||
|
||||
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. We’ll use this in a bit to initialize an environment variable
|
||||
13. Copy the Login URL:
|
||||
For **Signing Option**, select **Sign SAML response and assertion**
|
||||
|
||||

|
||||
|
||||
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**:
|
||||
|
||||

|
||||
|
||||
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. We’ll use this in a bit to initialize an environment variable
|
||||
14. Copy the Login URL:
|
||||
|
||||

|
||||
|
||||
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>`
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
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`.
|
||||
|
||||

|
||||
|
||||
From there, click on the `Personal access tokens` tab.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -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" %}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.',
|
||||
})
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] [_..]',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}`
|
||||
: '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])) {
|
||||
|
||||