feat(misc): non conflicting init/add flow (#22791)

This commit is contained in:
Jason Jean 2024-04-15 16:45:08 -04:00 committed by GitHub
parent e97c5887ee
commit bf206e578e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
180 changed files with 1815 additions and 1252 deletions

View File

@ -1,6 +1,7 @@
# Adding Nx to your Existing Project
Nx can be added to any type of project, not just monorepos. The main benefit is to get caching abilities for the package scripts. Each project usually has a set of scripts in the `package.json`:
Nx can be added to any type of project, not just monorepos. The main benefit is to get caching abilities for the package
scripts. Each project usually has a set of scripts in the `package.json`:
```json {% fileName="package.json" %}
{
@ -16,7 +17,8 @@ Nx can be added to any type of project, not just monorepos. The main benefit is
You can make these scripts faster by leveraging Nx's caching capabilities. For example:
- You change some spec files: in that case the `build` task can be cached and doesn't have to re-run.
- You update your docs, changing a couple of markdown files: then there's no need to re-run builds, tests, linting on your CI. All you might want to do is trigger the Docusaurus build.
- You update your docs, changing a couple of markdown files: then there's no need to re-run builds, tests, linting on
your CI. All you might want to do is trigger the Docusaurus build.
## Install Nx on a Non-Monorepo Project
@ -26,7 +28,55 @@ Run the following command:
npx nx@latest init
```
This will set up Nx for you - updating the `package.json` file and creating a new `nx.json` file with Nx configuration based on your answers during the set up process. The set up process will suggest installing Nx plugins that might be useful based on your existing repository. The example below is using the `@nx/eslint` and `@nx/next` plugins to run ESLint and Next.js tasks with Nx:
Running this command will ask you a few questions about your workspace and then set up Nx for you accordingly. The setup
process detects tools which are used in your workspace and suggests installing Nx plugins to integrate the tools you use
with Nx. Running those tools through Nx will have caching enabled when possible, providing you with a faster alternative
for running those tools. You can start with a few to see how it works and then add more with
the [`nx add`](/nx-api/nx/documents/add) command later. You can also decide to add them all and get the full experience
right
away because adding plugins will not break your existing workflow.
The first thing you may notice is that Nx updates your `package.json` scripts during the setup process. Nx Plugins setup
Nx commands which run the underlying tool with caching enabled. When a `package.json` script runs a command which can be
run through Nx, Nx will replace that script in the `package.json` scripts with an Nx command that has
caching automatically enabled. Anywhere those `package.json` scripts are used, including your CI, will become faster
when possible. Let's go through an example where the `@nx/next/plugin` and `@nx/eslint/plugin` plugins are added to a
workspace with the
following `package.json`.
```diff {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
- "build": "next build && echo 'Build complete'",
+ "build": "nx next:build && echo 'Build complete'",
- "lint": "eslint ./src",
+ "lint": "nx eslint:lint",
"test": "node ./run-tests.js"
},
+ "nx": {}
}
```
The `@nx/next/plugin` plugin adds a `next:build` target which runs `next build` and sets up caching correctly. In other
words, running `nx next:build` is the same as running `next build` with the added benefit of it being cacheable. Hence,
Nx replaces `next build` in the `package.json` `build` script to add caching to anywhere running `npm run build`.
Similarly, `@nx/eslint/plugin` sets up the `nx eslint:lint` command to run `eslint ./src` with caching enabled.
The `test` script was not recognized by any Nx plugin, so it was left as is. After Nx has been setup,
running `npm run build` or `npm run lint` multiple times, will be instant when possible.
You can also run any npm scripts directly through Nx with `nx build` or `nx lint` which will run the `npm run build`
and `npm run lint` scripts respectively. In the later portion of the setup flow, Nx will ask if you would like some of
those npm scripts to be cacheable. By making those cacheable, running `nx build` rather than `npm run build` will add
another layer of cacheability. However, `nx build` must be run instead of `npm run build` to take advantage of the
cache.
## Inferred Tasks
You may have noticed that `@nx/next` provides `dev` and `start` tasks in addition to the `next:build` task. Those tasks
were created by the `@nx/next/plugin` plugin from your existing Next.js configuration. You can see the configuration for
the Nx Plugins in `nx.json`:
```json {% fileName="nx.json" %}
{
@ -34,13 +84,13 @@ This will set up Nx for you - updating the `package.json` file and creating a ne
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
"targetName": "eslint:lint"
}
},
{
"plugin": "@nx/next/plugin",
"options": {
"buildTargetName": "build",
"buildTargetName": "next:build",
"devTargetName": "dev",
"startTargetName": "start"
}
@ -49,33 +99,11 @@ This will set up Nx for you - updating the `package.json` file and creating a ne
}
```
When Nx updates your `package.json` scripts, it looks for scripts that can be replaced with an Nx command that has caching automatically enabled. The `package.json` defined above would be updated to look like this:
Each plugin can accept options to customize the projects which they create. You can see more information about
configuring the plugins on the [`@nx/next/plugin`](/nx-api/next) and [`@nx/eslint/plugin`](/nx-api/eslint) plugin pages.
```json {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
"build": "nx build",
"lint": "nx lint",
"test": "node ./run-tests.js"
},
...
"nx": {
"includedScripts": []
}
}
```
The `@nx/next` plugin can run `next build` for you and set up caching correctly, so it replaces `next build` with `nx build`. Similarly, `@nx/eslint` can set up caching for `eslint ./src`. When you run `npm run build` or `npm run lint` multiple times, you'll see that caching is enabled. You can also call Nx directly from the terminal with `nx build` or `nx lint`.
The `test` script was not recognized by any Nx plugin, so it was left as is.
The `includedScripts` array allows you to specify `package.json` scripts that can be run with the `nx build` syntax.
## Inferred Tasks
You may have noticed that `@nx/next` provides `dev` and `start` tasks in addition to the `build` task. Those tasks were created by the `@nx/next` plugin from your existing Next.js configuration. To view all available tasks, open the Project Details view with Nx Console or use the terminal to launch the project details in a browser window.
To view all available tasks, open the Project Details view with Nx Console or use the terminal to launch the project
details in a browser window.
```shell
nx show project my-workspace --web
@ -90,7 +118,7 @@ nx show project my-workspace --web
"data": {
"root": ".",
"targets": {
"lint": {
"eslint:lint": {
"cache": true,
"options": {
"cwd": ".",
@ -107,7 +135,7 @@ nx show project my-workspace --web
"executor": "nx:run-commands",
"configurations": {}
},
"build": {
"next:build": {
"options": {
"cwd": ".",
"command": "next build"
@ -146,7 +174,6 @@ nx show project my-workspace --web
"sourceRoot": ".",
"name": "my-workspace",
"projectType": "library",
"includedScripts": [],
"implicitDependencies": [],
"tags": []
}
@ -154,20 +181,20 @@ nx show project my-workspace --web
"sourceMap": {
"root": ["package.json", "nx/core/package-json-workspaces"],
"targets": ["package.json", "nx/core/package-json-workspaces"],
"targets.lint": ["package.json", "@nx/eslint/plugin"],
"targets.lint.command": ["package.json", "@nx/eslint/plugin"],
"targets.lint.cache": ["package.json", "@nx/eslint/plugin"],
"targets.lint.options": ["package.json", "@nx/eslint/plugin"],
"targets.lint.inputs": ["package.json", "@nx/eslint/plugin"],
"targets.lint.options.cwd": ["package.json", "@nx/eslint/plugin"],
"targets.build": ["next.config.js", "@nx/next/plugin"],
"targets.build.command": ["next.config.js", "@nx/next/plugin"],
"targets.build.options": ["next.config.js", "@nx/next/plugin"],
"targets.build.dependsOn": ["next.config.js", "@nx/next/plugin"],
"targets.build.cache": ["next.config.js", "@nx/next/plugin"],
"targets.build.inputs": ["next.config.js", "@nx/next/plugin"],
"targets.build.outputs": ["next.config.js", "@nx/next/plugin"],
"targets.build.options.cwd": ["next.config.js", "@nx/next/plugin"],
"targets.eslint:lint": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.command": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.cache": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.options": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.inputs": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.options.cwd": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.next:build": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.command": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.options": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.dependsOn": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.cache": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.inputs": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.outputs": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.options.cwd": ["next.config.js", "@nx/next/plugin"],
"targets.dev": ["next.config.js", "@nx/next/plugin"],
"targets.dev.command": ["next.config.js", "@nx/next/plugin"],
"targets.dev.options": ["next.config.js", "@nx/next/plugin"],
@ -180,7 +207,6 @@ nx show project my-workspace --web
"sourceRoot": ["package.json", "nx/core/package-json-workspaces"],
"name": ["package.json", "nx/core/package-json-workspaces"],
"projectType": ["package.json", "nx/core/package-json-workspaces"],
"includedScripts": ["package.json", "nx/core/package-json-workspaces"],
"targets.nx-release-publish": [
"package.json",
"nx/core/package-json-workspaces"
@ -203,34 +229,38 @@ nx show project my-workspace --web
{% /project-details %}
The project detail view lists all available tasks, the configuration values for those tasks and where those configuration values are being set.
The project detail view lists all available tasks, the configuration values for those tasks and where those
configuration values are being set.
## Configure an Existing Script to Run with Nx
If you want to run one of your existing scripts with Nx, you need to tell Nx about it.
1. Preface the script with `nx exec -- ` to have `npm run test` invoke the command with Nx.
2. Add the script to `includedScripts`.
3. Define caching settings.
2. Define caching settings.
The `nx exec` command allows you to keep using `npm test` or `npm run test` (or other package manager's alternatives) as you're accustomed to. But still get the benefits of making those operations cacheable. Configuring the `test` script from the example above to run with Nx would look something like this:
The `nx exec` command allows you to keep using `npm test` or `npm run test` (or other package manager's alternatives) as
you're accustomed to. But still get the benefits of making those operations cacheable. Configuring the `test` script
from the example above to run with Nx would look something like this:
```json {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
"build": "nx build",
"lint": "nx lint",
"build": "nx next:build",
"lint": "nx eslint:lint",
"test": "nx exec -- node ./run-tests.js"
},
...
"nx": {
"includedScripts": ["test"],
"targets": {
"test": {
"cache": "true",
"inputs": ["default", "^default"],
"inputs": [
"default",
"^default"
],
"outputs": []
}
}
@ -238,17 +268,22 @@ The `nx exec` command allows you to keep using `npm test` or `npm run test` (or
}
```
Now if you run `npm run test` or `nx test` twice, the results will be retrieved from the cache. The `inputs` used in this example are as cautious as possible, so you can significantly improve the value of the cache by [customizing Nx Inputs](/recipes/running-tasks/configure-inputs) for each task.
Now if you run `npm run test` or `nx test` twice, the results will be retrieved from the cache. The `inputs` used in
this example are as cautious as possible, so you can significantly improve the value of the cache
by [customizing Nx Inputs](/recipes/running-tasks/configure-inputs) for each task.
## Learn More
{% cards %}
{% card title="Customizing Inputs and Named Inputs" description="Learn more about how to fine-tune caching with custom inputs" type="documentation" url="/recipes/running-tasks/configure-inputs" /%}
{% card title="Customizing Inputs and Named Inputs" description="Learn more about how to fine-tune caching with custom
inputs" type="documentation" url="/recipes/running-tasks/configure-inputs" /%}
{% card title="Cache Task Results" description="Learn more about how caching works" type="documentation" url="/features/cache-task-results" /%}
{% card title="Cache Task Results" description="Learn more about how caching works" type="documentation" url="
/features/cache-task-results" /%}
{% card title="Adding Nx to NPM/Yarn/PNPM Workspace" description="Learn more about how to add Nx to an existing monorepo" type="documentation" url="/recipes/adopting-nx/adding-to-monorepo" /%}
{% card title="Adding Nx to NPM/Yarn/PNPM Workspace" description="Learn more about how to add Nx to an existing
monorepo" type="documentation" url="/recipes/adopting-nx/adding-to-monorepo" /%}
{% /cards %}

View File

@ -1,10 +1,13 @@
# Adding Nx to NPM/Yarn/PNPM Workspace
{% callout type="note" title="Migrating from Lerna?" %}
Interested in migrating from [Lerna](https://github.com/lerna/lerna) in particular? In case you missed it, Lerna v6 is powering Nx underneath. As a result, Lerna gets all the modern features such as caching and task pipelines. Read more on [https://lerna.js.org/upgrade](https://lerna.js.org/upgrade).
Interested in migrating from [Lerna](https://github.com/lerna/lerna) in particular? In case you missed it, Lerna v6 is
powering Nx underneath. As a result, Lerna gets all the modern features such as caching and task pipelines. Read more
on [https://lerna.js.org/upgrade](https://lerna.js.org/upgrade).
{% /callout %}
Nx has first-class support for [monorepos](/getting-started/tutorials/npm-workspaces-tutorial). As a result, if you have an existing NPM/Yarn or PNPM-based monorepo setup, you can easily add Nx to get
Nx has first-class support for [monorepos](/getting-started/tutorials/npm-workspaces-tutorial). As a result, if you have
an existing NPM/Yarn or PNPM-based monorepo setup, you can easily add Nx to get
- fast [task scheduling](/features/run-tasks)
- support for [task pipelines](/concepts/task-pipeline-configuration)
@ -12,7 +15,8 @@ Nx has first-class support for [monorepos](/getting-started/tutorials/npm-worksp
- [remote caching with Nx Cloud](/ci/features/remote-cache)
- [distributed task execution with Nx Cloud](/ci/features/distribute-task-execution)
This is a low-impact operation because all that needs to be done is to install the `nx` package at the root level and add an `nx.json` for configuring caching and task pipelines.
This is a low-impact operation because all that needs to be done is to install the `nx` package at the root level and
add an `nx.json` for configuring caching and task pipelines.
{% youtube
src="https://www.youtube.com/embed/ngdoUQBvAjo"
@ -27,7 +31,55 @@ Run the following command to automatically set up Nx:
npx nx@latest init
```
This will set up Nx for you - updating the `package.json` file and creating a new `nx.json` file with Nx configuration based on your answers during the set up process. The set up process will suggest installing Nx plugins that might be useful based on your existing repository. The example below is using the `@nx/eslint` and `@nx/next` plugins to run ESLint and Next.js tasks with Nx:
Running this command will ask you a few questions about your workspace and then set up Nx for you accordingly. The setup
process detects tools which are used in your workspace and suggests installing Nx plugins to integrate the tools you use
with Nx. Running those tools through Nx will have caching enabled when possible, providing you with a faster alternative
for running those tools. You can start with a few to see how it works and then add more with
the [`nx add`](/nx-api/nx/documents/add) command later. You can also decide to add them all and get the full experience
right
away because adding plugins will not break your existing workflow.
The first thing you may notice is that Nx updates your `package.json` scripts during the setup process. Nx Plugins setup
Nx commands which run the underlying tool with caching enabled. When a `package.json` script runs a command which can be
run through Nx, Nx will replace that script in the `package.json` scripts with an Nx command that has
caching automatically enabled. Anywhere those `package.json` scripts are used, including your CI, will become faster
when possible. Let's go through an example where the `@nx/next/plugin` and `@nx/eslint/plugin` plugins are added to a
workspace with the
following `package.json`.
```diff {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
- "build": "next build && echo 'Build complete'",
+ "build": "nx next:build && echo 'Build complete'",
- "lint": "eslint ./src",
+ "lint": "nx eslint:lint",
"test": "node ./run-tests.js"
},
...
}
```
The `@nx/next/plugin` plugin adds a `next:build` target which runs `next build` and sets up caching correctly. In other
words, running `nx next:build` is the same as running `next build` with the added benefit of it being cacheable. Hence,
Nx replaces `next build` in the `package.json` `build` script to add caching to anywhere running `npm run build`.
Similarly, `@nx/eslint/plugin` sets up the `nx eslint:lint` command to run `eslint ./src` with caching enabled.
The `test` script was not recognized by any Nx plugin, so it was left as is. After Nx has been setup,
running `npm run build` or `npm run lint` multiple times, will be instant when possible.
You can also run any npm scripts directly through Nx with `nx build` or `nx lint` which will run the `npm run build`
and `npm run lint` scripts respectively. In the later portion of the setup flow, Nx will ask if you would like some of
those npm scripts to be cacheable. By making those cacheable, running `nx build` rather than `npm run build` will add
another layer of cacheability. However, `nx build` must be run instead of `npm run build` to take advantage of the
cache.
## Inferred Tasks
You may have noticed that `@nx/next` provides `dev` and `start` tasks in addition to the `next:build` task. Those tasks
were created by the `@nx/next/plugin` plugin from your existing Next.js configuration. You can see the configuration for
the Nx Plugins in `nx.json`:
```json {% fileName="nx.json" %}
{
@ -35,13 +87,13 @@ This will set up Nx for you - updating the `package.json` file and creating a ne
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
"targetName": "eslint:lint"
}
},
{
"plugin": "@nx/next/plugin",
"options": {
"buildTargetName": "build",
"buildTargetName": "next:build",
"devTargetName": "dev",
"startTargetName": "start"
}
@ -50,48 +102,11 @@ This will set up Nx for you - updating the `package.json` file and creating a ne
}
```
When Nx updates your `package.json` scripts, it looks for scripts that can be replaced with an Nx command that has caching automatically enabled. Assuming you initially had a `package.json` file looking like the following:
Each plugin can accept options to customize the projects which they create. You can see more information about
configuring the plugins on the [`@nx/next/plugin`](/nx-api/next) and [`@nx/eslint/plugin`](/nx-api/eslint) plugin pages.
```json {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
"build": "next build",
"lint": "eslint ./src",
"test": "node ./run-tests.js"
},
...
}
```
After setting up Nx, the `package.json` file would be updated to look like this:
```json {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
"build": "nx build",
"lint": "nx lint",
"test": "node ./run-tests.js"
},
...
"nx": {
"includedScripts": []
}
}
```
The `@nx/next` plugin can run `next build` for you and set up caching correctly, so it replaces `next build` with `nx build`. Similarly, `@nx/eslint` can set up caching for `eslint ./src`. When you run `npm run build` or `npm run lint` multiple times, you'll see that caching is enabled. You can also call Nx directly from the terminal with `nx build` or `nx lint`.
The `test` script was not recognized by any Nx plugin, so it was left as is.
The `includedScripts` array allows you to specify `package.json` scripts that can be run with the `nx build` syntax.
## Inferred Tasks
You may have noticed that `@nx/next` provides `dev` and `start` tasks in addition to the `build` task. Those tasks were created by the `@nx/next` plugin from your existing Next.js configuration. To view all available tasks, open the Project Details view with Nx Console or use the terminal to launch the project details in a browser window.
To view all available tasks, open the Project Details view with Nx Console or use the terminal to launch the project
details in a browser window.
```shell
nx show project my-workspace --web
@ -106,7 +121,7 @@ nx show project my-workspace --web
"data": {
"root": ".",
"targets": {
"lint": {
"eslint:lint": {
"cache": true,
"options": {
"cwd": ".",
@ -123,7 +138,7 @@ nx show project my-workspace --web
"executor": "nx:run-commands",
"configurations": {}
},
"build": {
"next:build": {
"options": {
"cwd": ".",
"command": "next build"
@ -162,7 +177,6 @@ nx show project my-workspace --web
"sourceRoot": ".",
"name": "my-workspace",
"projectType": "library",
"includedScripts": [],
"implicitDependencies": [],
"tags": []
}
@ -170,20 +184,20 @@ nx show project my-workspace --web
"sourceMap": {
"root": ["package.json", "nx/core/package-json-workspaces"],
"targets": ["package.json", "nx/core/package-json-workspaces"],
"targets.lint": ["package.json", "@nx/eslint/plugin"],
"targets.lint.command": ["package.json", "@nx/eslint/plugin"],
"targets.lint.cache": ["package.json", "@nx/eslint/plugin"],
"targets.lint.options": ["package.json", "@nx/eslint/plugin"],
"targets.lint.inputs": ["package.json", "@nx/eslint/plugin"],
"targets.lint.options.cwd": ["package.json", "@nx/eslint/plugin"],
"targets.build": ["next.config.js", "@nx/next/plugin"],
"targets.build.command": ["next.config.js", "@nx/next/plugin"],
"targets.build.options": ["next.config.js", "@nx/next/plugin"],
"targets.build.dependsOn": ["next.config.js", "@nx/next/plugin"],
"targets.build.cache": ["next.config.js", "@nx/next/plugin"],
"targets.build.inputs": ["next.config.js", "@nx/next/plugin"],
"targets.build.outputs": ["next.config.js", "@nx/next/plugin"],
"targets.build.options.cwd": ["next.config.js", "@nx/next/plugin"],
"targets.eslint:lint": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.command": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.cache": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.options": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.inputs": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.eslint:lint.options.cwd": [".eslintrc.json", "@nx/eslint/plugin"],
"targets.next:build": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.command": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.options": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.dependsOn": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.cache": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.inputs": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.outputs": ["next.config.js", "@nx/next/plugin"],
"targets.next:build.options.cwd": ["next.config.js", "@nx/next/plugin"],
"targets.dev": ["next.config.js", "@nx/next/plugin"],
"targets.dev.command": ["next.config.js", "@nx/next/plugin"],
"targets.dev.options": ["next.config.js", "@nx/next/plugin"],
@ -196,7 +210,6 @@ nx show project my-workspace --web
"sourceRoot": ["package.json", "nx/core/package-json-workspaces"],
"name": ["package.json", "nx/core/package-json-workspaces"],
"projectType": ["package.json", "nx/core/package-json-workspaces"],
"includedScripts": ["package.json", "nx/core/package-json-workspaces"],
"targets.nx-release-publish": [
"package.json",
"nx/core/package-json-workspaces"
@ -219,34 +232,38 @@ nx show project my-workspace --web
{% /project-details %}
The project detail view lists all available tasks, the configuration values for those tasks and where those configuration values are being set.
The project detail view lists all available tasks, the configuration values for those tasks and where those
configuration values are being set.
## Configure an Existing Script to Run with Nx
If you want to run one of your existing scripts with Nx, you need to tell Nx about it.
1. Preface the script with `nx exec -- ` to have `npm run test` invoke the command with Nx.
2. Add the script to `includedScripts`.
3. Define caching settings.
2. Define caching settings.
The `nx exec` command allows you to keep using `npm test` or `npm run test` (or other package manager's alternatives) as you're accustomed to. But still get the benefits of making those operations cacheable. Configuring the `test` script from the example above to run with Nx would look something like this:
The `nx exec` command allows you to keep using `npm test` or `npm run test` (or other package manager's alternatives) as
you're accustomed to. But still get the benefits of making those operations cacheable. Configuring the `test` script
from the example above to run with Nx would look something like this:
```json {% fileName="package.json" %}
{
"name": "my-workspace",
...
"scripts": {
"build": "nx build",
"lint": "nx lint",
"build": "nx next:build",
"lint": "nx eslint:lint",
"test": "nx exec -- node ./run-tests.js"
},
...
"nx": {
"includedScripts": ["test"],
"targets": {
"test": {
"cache": "true",
"inputs": ["default", "^default"],
"inputs": [
"default",
"^default"
],
"outputs": []
}
}
@ -254,11 +271,14 @@ The `nx exec` command allows you to keep using `npm test` or `npm run test` (or
}
```
Now if you run `npm run test` or `nx test` twice, the results will be retrieved from the cache. The `inputs` used in this example are as cautious as possible, so you can significantly improve the value of the cache by [customizing Nx Inputs](/recipes/running-tasks/configure-inputs) for each task.
Now if you run `npm run test` or `nx test` twice, the results will be retrieved from the cache. The `inputs` used in
this example are as cautious as possible, so you can significantly improve the value of the cache
by [customizing Nx Inputs](/recipes/running-tasks/configure-inputs) for each task.
## Incrementally Adopting Nx
All the features of Nx can be enabled independently of each other. Hence, Nx can easily be adopted incrementally by initially using Nx just for a subset of your scripts and then gradually adding more.
All the features of Nx can be enabled independently of each other. Hence, Nx can easily be adopted incrementally by
initially using Nx just for a subset of your scripts and then gradually adding more.
For example, use Nx to run your builds:
@ -266,7 +286,8 @@ For example, use Nx to run your builds:
npx nx run-many -t build
```
But instead keep using NPM/Yarn/PNPM workspace commands for your tests and other scripts. Here's an example of using PNPM commands to run tests across packages
But instead keep using NPM/Yarn/PNPM workspace commands for your tests and other scripts. Here's an example of using
PNPM commands to run tests across packages
```shell
pnpm run -r test
@ -278,14 +299,19 @@ This allows for incrementally adopting Nx in your existing workspace.
{% cards %}
{% card title="Cache Task Results" description="Learn more about how caching works" type="documentation" url="/features/cache-task-results" /%}
{% card title="Cache Task Results" description="Learn more about how caching works" type="documentation" url="
/features/cache-task-results" /%}
{% card title="Task Pipeline Configuration" description="Learn more about how to setup task dependencies" type="documentation" url="/concepts/task-pipeline-configuration" /%}
{% card title="Task Pipeline Configuration" description="Learn more about how to setup task dependencies" type="
documentation" url="/concepts/task-pipeline-configuration" /%}
{% card title="Nx Ignore" description="Learn about how to ignore certain projects using .nxignore" type="documentation" url="/reference/nxignore" /%}
{% card title="Nx Ignore" description="Learn about how to ignore certain projects using .nxignore" type="documentation"
url="/reference/nxignore" /%}
{% card title="Nx and Turbo" description="Read about how Nx compares to Turborepo" url="/concepts/more-concepts/turbo-and-nx" /%}
{% card title="Nx and Turbo" description="Read about how Nx compares to Turborepo" url="
/concepts/more-concepts/turbo-and-nx" /%}
{% card title="Integrated Repos vs Package-Based Repos" description="Learn about two styles of monorepos." url="/concepts/integrated-vs-package-based" /%}
{% card title="Integrated Repos vs Package-Based Repos" description="Learn about two styles of monorepos." url="
/concepts/integrated-vs-package-based" /%}
{% /cards %}

View File

@ -11,17 +11,22 @@ import {
describe('@nx/detox (legacy)', () => {
const appName = uniq('myapp');
let originalEnv: string;
beforeAll(() => {
originalEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
newProject();
});
afterAll(() => cleanupProject());
afterAll(() => {
process.env.NX_ADD_PLUGINS = originalEnv;
cleanupProject();
});
it('should create files and run lint command for react-native apps', async () => {
runCLI(
`generate @nx/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false`
);
checkFilesExist(`apps/${appName}-e2e/.detoxrc.json`);
checkFilesExist(`apps/${appName}-e2e/tsconfig.json`);
@ -38,8 +43,7 @@ describe('@nx/detox (legacy)', () => {
it('should create files and run lint command for expo apps', async () => {
const expoAppName = uniq('myapp');
runCLI(
`generate @nx/expo:app ${expoAppName} --e2eTestRunner=detox --linter=eslint`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/expo:app ${expoAppName} --e2eTestRunner=detox --linter=eslint`
);
checkFilesExist(`apps/${expoAppName}-e2e/.detoxrc.json`);
checkFilesExist(`apps/${expoAppName}-e2e/tsconfig.json`);
@ -57,8 +61,7 @@ describe('@nx/detox (legacy)', () => {
const appName = uniq('app1');
runCLI(
`generate @nx/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false --project-name-and-root-format=as-provided --interactive=false`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false --project-name-and-root-format=as-provided --interactive=false`
);
// check files are generated without the layout directory ("apps/") and

View File

@ -23,10 +23,15 @@ describe('@nx/expo (legacy)', () => {
let proj: string;
let appName = uniq('my-app');
let libName = uniq('lib');
let originalEnv: string;
beforeAll(() => {
proj = newProject({ packages: ['@nx/expo'] });
// we create empty preset above which skips creation of `production` named input
originalEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
updateJson('nx.json', (nxJson) => {
nxJson.namedInputs = {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
@ -36,14 +41,16 @@ describe('@nx/expo (legacy)', () => {
return nxJson;
});
runCLI(
`generate @nx/expo:application ${appName} --e2eTestRunner=cypress --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/expo:application ${appName} --e2eTestRunner=cypress --no-interactive`
);
runCLI(
`generate @nx/expo:library ${libName} --buildable --publishable --importPath=${proj}/${libName}`
);
});
afterAll(() => cleanupProject());
afterAll(() => {
process.env.NX_ADD_PLUGINS = originalEnv;
cleanupProject();
});
it('should test and lint', async () => {
const componentName = uniq('Component');
@ -193,8 +200,7 @@ describe('@nx/expo (legacy)', () => {
const libName = uniq('@my-org/lib1');
runCLI(
`generate @nx/expo:application ${appName} --project-name-and-root-format=as-provided --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/expo:application ${appName} --project-name-and-root-format=as-provided --no-interactive`
);
// check files are generated without the layout directory ("apps/") and
@ -268,8 +274,7 @@ describe('@nx/expo (legacy)', () => {
it('should run e2e for playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/expo:application ${appName2} --e2eTestRunner=playwright --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/expo:application ${appName2} --e2eTestRunner=playwright --no-interactive`
);
if (runE2ETests()) {
const results = runCLI(`e2e ${appName2}-e2e`, { verbose: true });

View File

@ -23,8 +23,12 @@ describe('@nx/react-native (legacy)', () => {
let proj: string;
let appName = uniq('my-app');
let libName = uniq('lib');
let originalEnv: string;
beforeAll(() => {
originalEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
proj = newProject();
// we create empty preset above which skips creation of `production` named input
updateJson('nx.json', (nxJson) => {
@ -36,14 +40,16 @@ describe('@nx/react-native (legacy)', () => {
return nxJson;
});
runCLI(
`generate @nx/react-native:application ${appName} --bunlder=webpack --e2eTestRunner=cypress --install=false --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/react-native:application ${appName} --bunlder=webpack --e2eTestRunner=cypress --install=false --no-interactive`
);
runCLI(
`generate @nx/react-native:library ${libName} --buildable --publishable --importPath=${proj}/${libName} --no-interactive`
);
});
afterAll(() => cleanupProject());
afterAll(() => {
process.env.NX_ADD_PLUGINS = originalEnv;
cleanupProject();
});
it('should build for web', async () => {
const results = runCLI(`build ${appName}`);
@ -269,8 +275,7 @@ describe('@nx/react-native (legacy)', () => {
const libName = uniq('@my-org/lib1');
runCLI(
`generate @nx/react-native:application ${appName} --project-name-and-root-format=as-provided --install=false --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/react-native:application ${appName} --project-name-and-root-format=as-provided --install=false --no-interactive`
);
// check files are generated without the layout directory ("apps/") and
@ -306,8 +311,7 @@ describe('@nx/react-native (legacy)', () => {
it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`,
{ env: { NX_ADD_PLUGINS: 'false' } }
`generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
);
const buildResults = runCLI(`build ${appName2}`);
expect(buildResults).toContain('Successfully ran target build');

View File

@ -47,7 +47,7 @@ export function nxComponentTestingPreset(
pathToConfig: string,
options?: NxComponentTestingOptions
) {
if (global.NX_GRAPH_CREATION || global.NX_CYPRESS_INIT_GENERATOR_RUNNING) {
if (global.NX_GRAPH_CREATION) {
// this is only used by plugins, so we don't need the component testing
// options, cast to any to avoid type errors
return nxBaseCypressPreset(pathToConfig, {

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
ProjectConfiguration,
Tree,

View File

@ -32,6 +32,10 @@ jest.mock('@nx/devkit', () => {
return {
...original,
ensurePackage: (pkg: string) => jest.requireActual(pkg),
createProjectGraphAsync: jest.fn().mockResolvedValue({
nodes: {},
dependencies: {},
}),
};
});

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { assertMinimumCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -9,19 +9,21 @@ import {
updateProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { componentGenerator } from '../component/component';
import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point';
import { generateTestApplication, generateTestLibrary } from '../utils/testing';
import { cypressComponentConfiguration } from './cypress-component-configuration';
let projectGraph: ProjectGraph = { nodes: {}, dependencies: {} };
jest.mock('@nx/cypress/src/utils/cypress-version');
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => projectGraph),
}));
import { componentGenerator } from '../component/component';
import { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point';
import { generateTestApplication, generateTestLibrary } from '../utils/testing';
import { cypressComponentConfiguration } from './cypress-component-configuration';
jest.mock('@nx/cypress/src/utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { getProjects } from '@nx/devkit';
import { Schema } from './schema';
import { Schema as remoteSchma } from '../remote/schema';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readJson, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import * as devkit from '@nx/devkit';
import { addProjectConfiguration, readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
getProjects,
NxJsonConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import * as devkit from '@nx/devkit';
import { ProjectGraph, readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,4 +1,6 @@
import type { Tree } from '@nx/devkit';
import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree } from '@nx/devkit';
import {
readJson,
readProjectConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type {
ProjectConfiguration,
TargetConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
// mock so we can test multiple versions
jest.mock('@nx/cypress/src/utils/cypress-version');
// mock bc the nxE2EPreset uses fs for path normalization

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import { readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import { readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { E2eTestRunner } from '../../utils/test-runners';
import {
getProjects,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import scamGenerator from '../scam/scam';
import { generateTestApplication } from '../utils/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
NxJsonConfiguration,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import type { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { Tree } from '@nx/devkit';
import { writeJson } from '@nx/devkit';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readJson, updateJson } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import type { Tree } from '@nx/devkit';
import { logger, visitNotIgnoredFiles } from '@nx/devkit';
import { basename } from 'path';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
readJson,
readProjectConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Builders } from '@schematics/angular/utility/workspace-models';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
ProjectConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
ProjectConfiguration,

View File

@ -70,6 +70,7 @@ export async function configurationGeneratorInternal(
opts.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
const tasks: GeneratorCallback[] = [];
const projectGraph = await createProjectGraphAsync();
if (!installedCypressVersion()) {
tasks.push(await jsInitGenerator(tree, { ...options, skipFormat: true }));
tasks.push(
@ -79,10 +80,9 @@ export async function configurationGeneratorInternal(
})
);
} else if (opts.addPlugin) {
addPlugin(tree);
await addPlugin(tree, projectGraph, false);
}
const projectGraph = await createProjectGraphAsync();
const nxJson = readNxJson(tree);
const hasPlugin = nxJson.plugins?.some((p) =>
typeof p === 'string'

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { NxJsonConfiguration, readJson, Tree, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -1,14 +1,19 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
formatFiles,
GeneratorCallback,
ProjectGraph,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import {
addPlugin as _addPlugin,
generateCombinations,
} from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin';
import { cypressVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
@ -55,30 +60,28 @@ function updateDependencies(tree: Tree, options: Schema) {
return runTasksInSerial(...tasks);
}
export function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/cypress/plugin'
: plugin.plugin === '@nx/cypress/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
componentTestingTargetName: 'component-test',
ciTargetName: 'e2e-ci',
openTargetName: 'open-cypress',
} as CypressPluginOptions,
});
updateNxJson(tree, nxJson);
export function addPlugin(
tree: Tree,
graph: ProjectGraph,
updatePackageScripts: boolean
) {
return _addPlugin(
tree,
graph,
'@nx/cypress/plugin',
createNodes,
{
targetName: ['e2e', 'cypress:e2e', 'cypress-e2e'],
openTargetName: ['open-cypress', 'cypress-open'],
componentTestingTargetName: [
'component-test',
'cypress:component-test',
'cypress-component-test',
],
ciTargetName: ['e2e-ci', 'cypress:e2e-ci', 'cypress-e2e-ci'],
},
updatePackageScripts
);
}
function updateProductionFileset(tree: Tree) {
@ -115,7 +118,11 @@ export async function cypressInitGeneratorInternal(
nxJson.useInferencePlugins !== false;
if (options.addPlugin) {
addPlugin(tree);
await addPlugin(
tree,
await createProjectGraphAsync(),
options.updatePackageScripts
);
} else {
setupE2ETargetDefaults(tree);
}
@ -125,12 +132,6 @@ export async function cypressInitGeneratorInternal(
installTask = updateDependencies(tree, options);
}
if (options.updatePackageScripts) {
global.NX_CYPRESS_INIT_GENERATOR_RUNNING = true;
await updatePackageScripts(tree, createNodes);
global.NX_CYPRESS_INIT_GENERATOR_RUNNING = false;
}
if (!options.skipFormat) {
await formatFiles(tree);
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
joinPathFragments,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readProjectConfiguration,
@ -8,6 +10,7 @@ import { libraryGenerator } from '@nx/js';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import updateToCypress11 from './cypress-11';
import { installedCypressVersion } from '../../utils/cypress-version';
jest.mock('../../utils/cypress-version');
import cypressComponentConfiguration from '../../generators/component-configuration/component-configuration';
@ -21,7 +24,6 @@ describe('Cypress 11 Migration', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
jest.resetAllMocks();
});
it('should not update if cypress <v10 is used', async () => {

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
Tree,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { detoxInitGenerator } from './init';

View File

@ -1,14 +1,17 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
formatFiles,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import {
addPlugin,
generateCombinations,
} from '@nx/devkit/src/utils/add-plugin';
import { createNodes, DetoxPluginOptions } from '../../plugins/plugin';
import { detoxVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
@ -33,11 +36,18 @@ export async function detoxInitGeneratorInternal(host: Tree, schema: Schema) {
}
if (schema.addPlugin) {
addPlugin(host);
}
if (schema.updatePackageScripts) {
await updatePackageScripts(host, createNodes);
await addPlugin(
host,
await createProjectGraphAsync(),
'@nx/detox/plugin',
createNodes,
{
buildTargetName: ['build', 'detox:build', 'detox-build'],
startTargetName: ['start', 'detox:start', 'detox-start'],
testTargetName: ['test', 'detox:test', 'detox-test'],
},
schema.updatePackageScripts
);
}
if (!schema.skipFormat) {
@ -64,29 +74,4 @@ function moveDependency(host: Tree) {
return removeDependenciesFromPackageJson(host, ['@nx/detox'], []);
}
function addPlugin(host: Tree) {
const nxJson = readNxJson(host);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/detox/plugin'
: plugin.plugin === '@nx/detox/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/detox/plugin',
options: {
buildTargetName: 'build',
startTargetName: 'start',
testTargetName: 'test',
} as DetoxPluginOptions,
});
updateNxJson(host, nxJson);
}
export default detoxInitGenerator;

View File

@ -0,0 +1,470 @@
import { createTreeWithEmptyWorkspace } from 'nx/src/generators/testing-utils/create-tree-with-empty-workspace';
import type { Tree } from 'nx/src/generators/tree';
import { readJson, writeJson } from 'nx/src/generators/utils/json';
import type { PackageJson } from 'nx/src/utils/package-json';
import { CreateNodes } from 'nx/src/project-graph/plugins';
import { ProjectGraph } from 'nx/src/devkit-exports';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { addPlugin, generateCombinations } from './add-plugin';
describe('addPlugin', () => {
let tree: Tree;
let createNodes: CreateNodes;
let graph: ProjectGraph;
let fs: TempFs;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
fs = new TempFs('add-plugin');
tree.root = fs.tempDir;
graph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1',
targets: {},
},
},
},
dependencies: {
app1: [],
},
};
createNodes = [
'**/next.config.{js,cjs,mjs}',
(_, { targetName }) => ({
projects: {
app1: {
name: 'app1',
targets: {
[targetName]: { command: 'next build' },
},
},
},
}),
];
await fs.createFiles({
'app1/next.config.js': '',
});
});
afterEach(() => {
fs.cleanup();
});
describe('adding the plugin', () => {
it('should not conflicting with the existing graph', async () => {
graph.nodes.app1.data.targets.build = {};
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build', '_build'],
},
true
);
expect(readJson(tree, 'nx.json').plugins).toContainEqual({
plugin: '@nx/next/plugin',
options: {
targetName: '_build',
},
});
});
});
it('should throw an error if no non-conflicting options are provided', async () => {
graph.nodes.app1.data.targets.build = {};
try {
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
fail('Should have thrown an error');
} catch (e) {}
});
describe('updating package scripts', () => {
test.each`
script
${'next-remote-watch'}
${'anext build'}
${'next builda'}
`('should not replace "$script"', async ({ script }) => {
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
build: script,
},
});
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.build).toBe(script);
});
test.each`
script | expected
${'next build'} | ${'nx build'}
${'npx next build'} | ${'npx nx build'}
${'next build --debug'} | ${'nx build --debug'}
${'NODE_OPTIONS="--inspect" next build'} | ${'NODE_OPTIONS="--inspect" nx build'}
${'NODE_OPTIONS="--inspect" npx next build --debug'} | ${'NODE_OPTIONS="--inspect" npx nx build --debug'}
${'next build && echo "Done"'} | ${'nx build && echo "Done"'}
${'echo "Building..." && next build'} | ${'echo "Building..." && nx build'}
${'echo "Building..." && next build && echo "Done"'} | ${'echo "Building..." && nx build && echo "Done"'}
${'echo "Building..." &&next build&& echo "Done"'} | ${'echo "Building..." &&nx build&& echo "Done"'}
${'echo "Building..." && NODE_OPTIONS="--inspect" npx next build --debug && echo "Done"'} | ${'echo "Building..." && NODE_OPTIONS="--inspect" npx nx build --debug && echo "Done"'}
`(
'should replace "$script" with "$expected"',
async ({ script, expected }) => {
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
build: script,
},
});
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.build).toBe(expected);
}
);
test.each`
script | expected
${'cypress run --e2e --config-file cypress.config.ts'} | ${'nx e2e'}
${'echo "Starting..." && cypress run --e2e --config-file cypress.config.ts && echo "Done"'} | ${'echo "Starting..." && nx e2e && echo "Done"'}
`(
'should replace "$script" with "$expected"',
async ({ script, expected }) => {
await fs.createFile('app1/cypress.config.ts', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
e2e: script,
},
});
createNodes = [
'**/cypress.config.{js,ts,mjs,mts,cjs,cts}',
() => ({
projects: {
app1: {
name: 'app1',
targets: {
e2e: {
command:
'cypress run --config-file cypress.config.ts --e2e',
},
},
},
},
}),
];
await addPlugin(
tree,
graph,
'@nx/cypress/plugin',
createNodes,
{
targetName: ['e2e'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.e2e).toBe(expected);
}
);
it('should handle scripts with name different than the target name', async () => {
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
'build:dev': 'next build',
},
});
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts['build:dev']).toBe('nx build');
});
it('should support replacing multiple scripts', async () => {
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
dev: 'PORT=4000 next dev --experimental-https',
start: 'next build && PORT=4000 next start --experimental-https',
},
});
createNodes = [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
name: 'app1',
targets: {
build: { command: 'next build' },
dev: { command: 'next dev' },
start: { command: 'next start' },
},
},
},
}),
];
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.dev).toBe('PORT=4000 nx dev --experimental-https');
expect(scripts.start).toBe(
'nx build && PORT=4000 nx start --experimental-https'
);
});
it('should support multiple occurrences of the same command within a script', async () => {
await fs.createFile('app1/tsconfig.json', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
typecheck: 'tsc -p tsconfig.lib.json && tsc -p tsconfig.spec.json',
},
});
createNodes = [
'**/tsconfig.json',
() => ({
projects: {
app1: {
name: 'app1',
targets: {
build: { command: 'tsc' },
},
},
},
}),
];
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.typecheck).toBe(
'nx build -p tsconfig.lib.json && nx build -p tsconfig.spec.json'
);
});
it('should support multiple occurrences of the same command within a script with extra commands', async () => {
await fs.createFile('app1/tsconfig.json', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
typecheck:
'echo "Typechecking..." && tsc -p tsconfig.lib.json && tsc -p tsconfig.spec.json && echo "Done"',
},
});
createNodes = [
'**/tsconfig.json',
() => ({
projects: {
app1: {
name: 'app1',
targets: {
build: { command: 'tsc' },
},
},
},
}),
];
await addPlugin(
tree,
graph,
'@nx/next/plugin',
createNodes,
{
targetName: ['build'],
},
true
);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.typecheck).toBe(
'echo "Typechecking..." && nx build -p tsconfig.lib.json && nx build -p tsconfig.spec.json && echo "Done"'
);
});
});
});
describe('generateCombinations', () => {
it('should return all combinations for a 2x2 array of strings', () => {
const input = {
prop1: ['1', '2'],
prop2: ['a', 'b'],
};
expect(generateCombinations(input).map((v) => JSON.stringify(v)))
.toMatchInlineSnapshot(`
[
"{"prop1":"1","prop2":"a"}",
"{"prop1":"2","prop2":"a"}",
"{"prop1":"1","prop2":"b"}",
"{"prop1":"2","prop2":"b"}",
]
`);
});
it('should handle arrays of 2x3 array of strings', () => {
const input = {
prop1: ['1', '2', '3'],
prop2: ['a', 'b'],
};
expect(generateCombinations(input).map((v) => JSON.stringify(v)))
.toMatchInlineSnapshot(`
[
"{"prop1":"1","prop2":"a"}",
"{"prop1":"2","prop2":"a"}",
"{"prop1":"3","prop2":"a"}",
"{"prop1":"1","prop2":"b"}",
"{"prop1":"2","prop2":"b"}",
"{"prop1":"3","prop2":"b"}",
]
`);
});
it('should handle arrays of 3x2 array of strings', () => {
const input = {
prop1: ['1', '2'],
prop2: ['a', 'b'],
prop3: ['i', 'ii'],
};
expect(generateCombinations(input).map((v) => JSON.stringify(v)))
.toMatchInlineSnapshot(`
[
"{"prop1":"1","prop2":"a","prop3":"i"}",
"{"prop1":"2","prop2":"a","prop3":"i"}",
"{"prop1":"1","prop2":"b","prop3":"i"}",
"{"prop1":"2","prop2":"b","prop3":"i"}",
"{"prop1":"1","prop2":"a","prop3":"ii"}",
"{"prop1":"2","prop2":"a","prop3":"ii"}",
"{"prop1":"1","prop2":"b","prop3":"ii"}",
"{"prop1":"2","prop2":"b","prop3":"ii"}",
]
`);
});
it('should be able to be used to generate possible combinations of plugin options', () => {
const possibleOptions = generateCombinations({
buildTargetName: ['build', 'vite:build', 'vite-build'],
testTargetName: ['test', 'vite:test', 'vite-test'],
serveTargetName: ['serve', 'vite:serve', 'vite-serve'],
previewTargetName: ['preview', 'vite:preview', 'vite-preview'],
serveStaticTargetName: [
'serve-static',
'vite:serve-static',
'vite-serve-static',
],
});
expect(possibleOptions.length).toEqual(3 ** 5);
expect(possibleOptions[0]).toEqual({
buildTargetName: 'build',
previewTargetName: 'preview',
testTargetName: 'test',
serveTargetName: 'serve',
serveStaticTargetName: 'serve-static',
});
// The first defined option is the first to be changed
expect(possibleOptions[1]).toEqual({
buildTargetName: 'vite:build',
previewTargetName: 'preview',
testTargetName: 'test',
serveTargetName: 'serve',
serveStaticTargetName: 'serve-static',
});
expect(possibleOptions[possibleOptions.length - 1]).toEqual({
buildTargetName: 'vite-build',
previewTargetName: 'vite-preview',
testTargetName: 'vite-test',
serveTargetName: 'vite-serve',
serveStaticTargetName: 'vite-serve-static',
});
});
});

View File

@ -1,16 +1,123 @@
import type {
CreateNodes,
CreateNodesFunction,
CreateNodesResult,
NxJsonConfiguration,
Tree,
import {
type CreateNodes,
type ProjectConfiguration,
type ProjectGraph,
type Tree,
} from 'nx/src/devkit-exports';
import type { PackageJson } from 'nx/src/utils/package-json';
import { basename, dirname } from 'path';
import * as yargs from 'yargs-parser';
import { requireNx } from '../../nx';
const { glob, readJson, readNxJson, workspaceRoot, writeJson } = requireNx();
const {
readJson,
writeJson,
readNxJson,
updateNxJson,
retrieveProjectConfigurations,
LoadedNxPlugin,
ProjectConfigurationsError,
} = requireNx();
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
/**
* Iterates through various forms of plugin options to find the one which does not conflict with the current graph
*/
export async function addPlugin<PluginOptions>(
tree: Tree,
graph: ProjectGraph,
pluginName: string,
createNodesTuple: CreateNodes<PluginOptions>,
options: Partial<
Record<keyof PluginOptions, PluginOptions[keyof PluginOptions][]>
>,
shouldUpdatePackageJsonScripts: boolean
): Promise<void> {
const graphNodes = Object.values(graph.nodes);
const nxJson = readNxJson(tree);
let pluginOptions: PluginOptions;
let projConfigs: ConfigurationResult;
const combinations = generateCombinations(options);
optionsLoop: for (const _pluginOptions of combinations) {
pluginOptions = _pluginOptions as PluginOptions;
nxJson.plugins ??= [];
if (
nxJson.plugins.some((p) =>
typeof p === 'string' ? p === pluginName : p.plugin === pluginName
)
) {
// Plugin has already been added
return;
}
global.NX_GRAPH_CREATION = true;
try {
projConfigs = await retrieveProjectConfigurations(
[
new LoadedNxPlugin(
{
name: pluginName,
createNodes: createNodesTuple,
},
{
plugin: pluginName,
options: pluginOptions,
}
),
],
tree.root,
nxJson
);
} catch (e) {
// Errors are okay for this because we're only running 1 plugin
if (e instanceof ProjectConfigurationsError) {
projConfigs = e.partialProjectConfigurationsResult;
} else {
throw e;
}
}
global.NX_GRAPH_CREATION = false;
for (const projConfig of Object.values(projConfigs.projects)) {
const node = graphNodes.find(
(node) => node.data.root === projConfig.root
);
if (!node) {
continue;
}
for (const targetName in projConfig.targets) {
if (node.data.targets[targetName]) {
// Conflicting Target Name, check the next one
pluginOptions = null;
continue optionsLoop;
}
}
}
break;
}
if (!pluginOptions) {
throw new Error(
'Could not add the plugin in a way which does not conflict with existing targets. Please report this error at: https://github.com/nrwl/nx/issues/new/choose'
);
}
nxJson.plugins.push({
plugin: pluginName,
options: pluginOptions,
});
updateNxJson(tree, nxJson);
if (shouldUpdatePackageJsonScripts) {
updatePackageScripts(tree, projConfigs);
}
}
type TargetCommand = {
command: string;
@ -18,35 +125,20 @@ type TargetCommand = {
configuration?: string;
};
export async function updatePackageScripts(
function updatePackageScripts(
tree: Tree,
createNodesTuple: CreateNodes
): Promise<void> {
const nxJson = readNxJson(tree);
const [pattern, createNodes] = createNodesTuple;
const matchingFiles = glob(tree, [pattern]);
for (const file of matchingFiles) {
const projectRoot = getProjectRootFromConfigFile(file);
await processProject(
tree,
projectRoot,
file,
createNodes,
nxJson,
matchingFiles
);
projectConfigurations: ConfigurationResult
) {
for (const projectConfig of Object.values(projectConfigurations.projects)) {
const projectRoot = projectConfig.root;
processProject(tree, projectRoot, projectConfig);
}
}
async function processProject(
function processProject(
tree: Tree,
projectRoot: string,
projectConfigurationFile: string,
createNodesFunction: CreateNodesFunction,
nxJsonConfiguration: NxJsonConfiguration,
configFiles: string[]
projectConfiguration: ProjectConfiguration
) {
const packageJsonPath = `${projectRoot}/package.json`;
if (!tree.exists(packageJsonPath)) {
@ -57,17 +149,7 @@ async function processProject(
return;
}
const result = await createNodesFunction(
projectConfigurationFile,
{},
{
nxJsonConfiguration,
workspaceRoot,
configFiles,
}
);
const targetCommands = getInferredTargetCommands(result);
const targetCommands = getInferredTargetCommands(projectConfiguration);
if (!targetCommands.length) {
return;
}
@ -187,34 +269,14 @@ async function processProject(
}
}
if (process.env.NX_RUNNING_NX_INIT === 'true') {
// running `nx init` so we want to exclude everything by default
packageJson.nx ??= {};
packageJson.nx.includedScripts = [];
} else if (replacedTargets.size) {
/**
* Running `nx add`. In this case we want to:
* - if `includedScripts` is already set: exclude scripts that match inferred targets that were used to replace a script
* - if `includedScripts` is not set: set `includedScripts` with all scripts except the ones that match an inferred target that was used to replace a script
*/
const includedScripts =
packageJson.nx?.includedScripts ?? Object.keys(packageJson.scripts);
const filteredScripts = includedScripts.filter(
(s) => !replacedTargets.has(s)
);
if (filteredScripts.length !== includedScripts.length) {
packageJson.nx ??= {};
packageJson.nx.includedScripts = filteredScripts;
}
}
writeJson(tree, packageJsonPath, packageJson);
}
function getInferredTargetCommands(result: CreateNodesResult): TargetCommand[] {
function getInferredTargetCommands(
project: ProjectConfiguration
): TargetCommand[] {
const targetCommands: TargetCommand[] = [];
for (const project of Object.values(result.projects ?? {})) {
for (const [targetName, target] of Object.entries(project.targets ?? {})) {
if (target.command) {
targetCommands.push({ command: target.command, target: targetName });
@ -253,16 +315,40 @@ function getInferredTargetCommands(result: CreateNodesResult): TargetCommand[] {
}
}
}
}
return targetCommands;
}
function getProjectRootFromConfigFile(file: string): string {
let projectRoot = dirname(file);
if (basename(projectRoot) === '.storybook') {
projectRoot = dirname(projectRoot);
}
export function generateCombinations<T>(
input: Record<string, T[]>
): Record<string, T>[] {
// This is reversed so that combinations have the first defined property updated first
const keys = Object.keys(input).reverse();
return _generateCombinations(Object.values(input).reverse()).map(
(combination) => {
const result = {};
combination.reverse().forEach((combo, i) => {
result[keys[keys.length - i - 1]] = combo;
});
return projectRoot;
return result;
}
);
}
/**
* Generate all possible combinations of a 2-dimensional array.
*
* Useful for generating all possible combinations of options for a plugin
*/
function _generateCombinations<T>(input: T[][]): T[][] {
if (input.length === 0) {
return [[]];
} else {
const [first, ...rest] = input;
const partialCombinations = _generateCombinations(rest);
return first.flatMap((value) =>
partialCombinations.map((combination) => [value, ...combination])
);
}
}

View File

@ -1,394 +0,0 @@
import { createTreeWithEmptyWorkspace } from 'nx/src/generators/testing-utils/create-tree-with-empty-workspace';
import type { Tree } from 'nx/src/generators/tree';
import { readJson, writeJson } from 'nx/src/generators/utils/json';
import type { PackageJson } from 'nx/src/utils/package-json';
import { updatePackageScripts } from './update-package-scripts';
describe('updatePackageScripts', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
test.each`
script
${'next-remote-watch'}
${'anext build'}
${'next builda'}
`('should not replace "$script"', async ({ script }) => {
tree.write('app1/next.config.js', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
build: script,
},
});
await updatePackageScripts(tree, [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'next build' },
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.build).toBe(script);
});
test.each`
script | expected
${'next build'} | ${'nx build'}
${'npx next build'} | ${'npx nx build'}
${'next build --debug'} | ${'nx build --debug'}
${'NODE_OPTIONS="--inspect" next build'} | ${'NODE_OPTIONS="--inspect" nx build'}
${'NODE_OPTIONS="--inspect" npx next build --debug'} | ${'NODE_OPTIONS="--inspect" npx nx build --debug'}
${'next build && echo "Done"'} | ${'nx build && echo "Done"'}
${'echo "Building..." && next build'} | ${'echo "Building..." && nx build'}
${'echo "Building..." && next build && echo "Done"'} | ${'echo "Building..." && nx build && echo "Done"'}
${'echo "Building..." &&next build&& echo "Done"'} | ${'echo "Building..." &&nx build&& echo "Done"'}
${'echo "Building..." && NODE_OPTIONS="--inspect" npx next build --debug && echo "Done"'} | ${'echo "Building..." && NODE_OPTIONS="--inspect" npx nx build --debug && echo "Done"'}
`(
'should replace "$script" with "$expected"',
async ({ script, expected }) => {
tree.write('app1/next.config.js', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
build: script,
},
});
await updatePackageScripts(tree, [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'next build' },
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.build).toBe(expected);
}
);
test.each`
script | expected
${'cypress run --e2e --config-file cypress.config.ts'} | ${'nx e2e'}
${'echo "Starting..." && cypress run --e2e --config-file cypress.config.ts && echo "Done"'} | ${'echo "Starting..." && nx e2e && echo "Done"'}
`(
'should replace "$script" with "$expected"',
async ({ script, expected }) => {
tree.write('app1/cypress.config.ts', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
e2e: script,
},
});
await updatePackageScripts(tree, [
'**/cypress.config.{js,ts,mjs,mts,cjs,cts}',
() => ({
projects: {
app1: {
targets: {
e2e: {
command: 'cypress run --config-file cypress.config.ts --e2e',
},
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.e2e).toBe(expected);
}
);
it('should handle scripts with name different than the target name', async () => {
tree.write('app1/next.config.js', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
'build:dev': 'next build',
},
});
await updatePackageScripts(tree, [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
targets: {
build: {
command: 'next build',
},
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts['build:dev']).toBe('nx build');
});
it('should support replacing multiple scripts', async () => {
tree.write('app1/next.config.js', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
dev: 'PORT=4000 next dev --experimental-https',
start: 'next build && PORT=4000 next start --experimental-https',
},
});
await updatePackageScripts(tree, [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'next build' },
dev: { command: 'next dev' },
start: { command: 'next start' },
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.dev).toBe('PORT=4000 nx dev --experimental-https');
expect(scripts.start).toBe(
'nx build && PORT=4000 nx start --experimental-https'
);
});
it('should support multiple occurrences of the same command within a script', async () => {
tree.write('app1/tsconfig.json', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
typecheck: 'tsc -p tsconfig.lib.json && tsc -p tsconfig.spec.json',
},
});
await updatePackageScripts(tree, [
'**/tsconfig.json',
() => ({
projects: {
app1: {
targets: {
build: { command: 'tsc' },
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.typecheck).toBe(
'nx build -p tsconfig.lib.json && nx build -p tsconfig.spec.json'
);
});
it('should support multiple occurrences of the same command within a script with extra commands', async () => {
tree.write('app1/tsconfig.json', '');
writeJson(tree, 'app1/package.json', {
name: 'app1',
scripts: {
typecheck:
'echo "Typechecking..." && tsc -p tsconfig.lib.json && tsc -p tsconfig.spec.json && echo "Done"',
},
});
await updatePackageScripts(tree, [
'**/tsconfig.json',
() => ({
projects: {
app1: {
targets: {
build: { command: 'tsc' },
},
},
},
}),
]);
const { scripts } = readJson<PackageJson>(tree, 'app1/package.json');
expect(scripts.typecheck).toBe(
'echo "Typechecking..." && nx build -p tsconfig.lib.json && nx build -p tsconfig.spec.json && echo "Done"'
);
});
it('should set "includedScripts" to an empty array when running "nx init"', async () => {
const originalEnvVarValue = process.env.NX_RUNNING_NX_INIT;
process.env.NX_RUNNING_NX_INIT = 'true';
tree.write('vite.config.ts', '');
writeJson(tree, 'package.json', {
name: 'app1',
scripts: {
build: 'vite build',
serve: 'vite',
test: 'vitest',
coverage: 'vitest run --coverage',
foo: 'echo "foo"',
},
});
await updatePackageScripts(tree, [
'**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'vite build' },
serve: { command: 'vite serve' },
test: { command: 'vitest run' },
},
},
},
}),
]);
const packageJson = readJson<PackageJson>(tree, 'package.json');
expect(packageJson.scripts).toStrictEqual({
build: 'nx build',
serve: 'vite',
test: 'vitest',
coverage: 'nx test --coverage',
foo: 'echo "foo"',
});
expect(packageJson.nx.includedScripts).toStrictEqual([]);
process.env.NX_RUNNING_NX_INIT = originalEnvVarValue;
});
it('should set "includedScripts" to all scripts except the ones matching inferred target names when "includedScripts" is not set', async () => {
tree.write('vite.config.ts', '');
writeJson(tree, 'package.json', {
name: 'app1',
scripts: {
build: 'vite build',
serve: 'vite',
test: 'vitest',
coverage: 'vitest run --coverage',
foo: 'echo "foo"',
},
});
await updatePackageScripts(tree, [
'**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'vite build' },
serve: { command: 'vite serve' },
test: { command: 'vitest run' },
},
},
},
}),
]);
const packageJson = readJson<PackageJson>(tree, 'package.json');
expect(packageJson.scripts).toStrictEqual({
build: 'nx build',
serve: 'vite',
test: 'vitest',
coverage: 'nx test --coverage',
foo: 'echo "foo"',
});
// "build": excluded because a script (itself) was replaced with a target "build"
// "serve" not excluded even though it matches the name of an inferred target because no script was replaced with a target "serve"
// "test" excluded even though it was not replaced because another script was replaced with a target "test"
// "coverage" not excluded even though it was replaced because it does not match a target
// "foo" not excluded because it does not match a target
expect(packageJson.nx.includedScripts).toStrictEqual([
'serve',
'coverage',
'foo',
]);
});
it('should not set "nx.includedScripts" when no script matched an inferred target', async () => {
tree.write('vite.config.ts', '');
writeJson(tree, 'package.json', {
name: 'app1',
scripts: {
serve: 'vite',
coverage: 'vitest run --coverage',
foo: 'echo "foo"',
},
});
await updatePackageScripts(tree, [
'**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'vite build' },
serve: { command: 'vite serve' },
test: { command: 'vitest run' },
},
},
},
}),
]);
const packageJson = readJson<PackageJson>(tree, 'package.json');
expect(packageJson.scripts).toStrictEqual({
serve: 'vite',
coverage: 'nx test --coverage',
foo: 'echo "foo"',
});
expect(packageJson.nx).toBeUndefined();
});
it('should exclude replaced package.json scripts from nx if they are initially included', async () => {
tree.write('next.config.js', '');
writeJson(tree, 'package.json', {
name: 'app1',
scripts: {
build: 'next build',
foo: 'echo "foo"',
},
nx: {
includedScripts: ['build', 'foo'],
},
});
await updatePackageScripts(tree, [
'**/next.config.{js,cjs,mjs}',
() => ({
projects: {
app1: {
targets: {
build: { command: 'next build' },
},
},
},
}),
]);
const { nx } = readJson<PackageJson>(tree, 'package.json');
expect(nx.includedScripts).toStrictEqual(['foo']);
});
});

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
NxJsonConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { NxJsonConfiguration, readJson, Tree, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { LinterInitOptions, lintInitGenerator } from './init';

View File

@ -1,15 +1,17 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { eslintVersion, nxVersion } from '../../utils/versions';
import { findEslintFile } from '../utils/eslint-file';
import { EslintPluginOptions, createNodes } from '../../plugins/plugin';
import { createNodes } from '../../plugins/plugin';
import { hasEslintPlugin } from '../utils/plugin';
export interface LinterInitOptions {
@ -47,29 +49,6 @@ function addTargetDefaults(tree: Tree) {
updateNxJson(tree, nxJson);
}
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
options: {
targetName: 'lint',
} as EslintPluginOptions,
});
updateNxJson(tree, nxJson);
}
export async function initEsLint(
tree: Tree,
options: LinterInitOptions
@ -82,12 +61,28 @@ export async function initEsLint(
const hasPlugin = hasEslintPlugin(tree);
const rootEslintFile = findEslintFile(tree);
if (rootEslintFile && options.addPlugin && !hasPlugin) {
addPlugin(tree);
const graph = await createProjectGraphAsync();
if (options.updatePackageScripts) {
await updatePackageScripts(tree, createNodes);
}
const lintTargetNames = [
'lint',
'eslint:lint',
'eslint-lint',
'_lint',
'_eslint:lint',
'_eslint-lint',
];
if (rootEslintFile && options.addPlugin && !hasPlugin) {
await addPlugin(
tree,
graph,
'@nx/eslint/plugin',
createNodes,
{
targetName: lintTargetNames,
},
options.updatePackageScripts
);
return () => {};
}
@ -99,7 +94,16 @@ export async function initEsLint(
updateProductionFileset(tree);
if (options.addPlugin) {
addPlugin(tree);
await addPlugin(
tree,
graph,
'@nx/eslint/plugin',
createNodes,
{
targetName: lintTargetNames,
},
options.updatePackageScripts
);
} else {
addTargetDefaults(tree);
}
@ -121,10 +125,6 @@ export async function initEsLint(
);
}
if (options.updatePackageScripts) {
await updatePackageScripts(tree, createNodes);
}
return runTasksInSerial(...tasks);
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { lintWorkspaceRuleGenerator } from './workspace-rule';

View File

@ -1,8 +1,9 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
NxJsonConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateJson,
} from '@nx/devkit';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
getProjects,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { logger, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';

View File

@ -1,4 +1,6 @@
import { Tree, readJson, updateJson } from '@nx/devkit';
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { expoInitGenerator } from './init';

View File

@ -1,15 +1,15 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
formatFiles,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createNodes, ExpoPluginOptions } from '../../../plugins/plugin';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../../plugins/plugin';
import {
expoCliVersion,
expoVersion,
@ -18,7 +18,6 @@ import {
reactNativeVersion,
reactVersion,
} from '../../utils/versions';
import { hasExpoPlugin } from '../../utils/has-expo-plugin';
import { addGitIgnoreEntry } from './lib/add-git-ignore-entry';
import { Schema } from './schema';
@ -37,7 +36,29 @@ export async function expoInitGeneratorInternal(host: Tree, schema: Schema) {
addGitIgnoreEntry(host);
if (schema.addPlugin) {
addPlugin(host);
await addPlugin(
host,
await createProjectGraphAsync(),
'@nx/expo/plugin',
createNodes,
{
startTargetName: ['start', 'expo:start', 'expo-start'],
buildTargetName: ['build', 'expo:build', 'expo-build'],
prebuildTargetName: ['prebuild', 'expo:prebuild', 'expo-prebuild'],
serveTargetName: ['serve', 'expo:serve', 'expo-serve'],
installTargetName: ['install', 'expo:install', 'expo-install'],
exportTargetName: ['export', 'expo:export', 'expo-export'],
submitTargetName: ['submit', 'expo:submit', 'expo-submit'],
runIosTargetName: ['run-ios', 'expo:run-ios', 'expo-run-ios'],
runAndroidTargetName: [
'run-android',
'expo:run-android',
'expo-run-android',
],
},
schema.updatePackageScripts
);
}
const tasks: GeneratorCallback[] = [];
@ -46,10 +67,6 @@ export async function expoInitGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(updateDependencies(host, schema));
}
if (schema.updatePackageScripts) {
await updatePackageScripts(host, createNodes);
}
if (!schema.skipFormat) {
await formatFiles(host);
}
@ -79,29 +96,4 @@ function moveDependency(host: Tree) {
return removeDependenciesFromPackageJson(host, ['@nx/react-native'], []);
}
function addPlugin(host: Tree) {
const nxJson = readNxJson(host);
if (hasExpoPlugin(host)) {
return;
}
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/expo/plugin',
options: {
startTargetName: 'start',
serveTargetName: 'serve',
runIosTargetName: 'run-ios',
runAndroidTargetName: 'run-android',
exportTargetName: 'export',
prebuildTargetName: 'prebuild',
installTargetName: 'install',
buildTargetName: 'build',
submitTargetName: 'submit',
} as ExpoPluginOptions,
});
updateNxJson(host, nxJson);
}
export default expoInitGenerator;

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
getProjects,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';

View File

@ -8,8 +8,6 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createNodes } from '../../plugin/nodes';
import { nxVersion } from '../../utils/versions';
import { InitGeneratorSchema } from './schema';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
@ -34,10 +32,6 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
addPlugin(tree);
addProjectReportToBuildGradle(tree);
if (options.updatePackageScripts && tree.exists('package.json')) {
await updatePackageScripts(tree, createNodes);
}
if (!options.skipFormat) {
await formatFiles(tree);
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
type NxJsonConfiguration,
readJson,

View File

@ -7,33 +7,13 @@ import {
updateNxJson,
type GeneratorCallback,
type Tree,
createProjectGraphAsync,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createNodes } from '../../plugins/plugin';
import { jestVersion, nxVersion } from '../../utils/versions';
import { isPresetCjs } from '../../utils/config/is-preset-cjs';
import type { JestInitSchema } from './schema';
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
if (
!nxJson.plugins.some((p) =>
typeof p === 'string'
? p === '@nx/jest/plugin'
: p.plugin === '@nx/jest/plugin'
)
) {
nxJson.plugins.push({
plugin: '@nx/jest/plugin',
options: {
targetName: 'test',
},
});
}
updateNxJson(tree, nxJson);
}
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
function updateProductionFileSet(tree: Tree, presetExt: 'cjs' | 'js') {
const nxJson = readNxJson(tree);
@ -121,7 +101,16 @@ export async function jestInitGeneratorInternal(
if (!tree.exists('jest.preset.js') && !tree.exists('jest.preset.cjs')) {
updateProductionFileSet(tree, presetExt);
if (options.addPlugin) {
addPlugin(tree);
await addPlugin(
tree,
await createProjectGraphAsync(),
'@nx/jest/plugin',
createNodes,
{
targetName: ['test', 'jest:test', 'jest-test'],
},
options.updatePackageScripts
);
} else {
addJestTargetDefaults(tree, presetExt);
}
@ -133,10 +122,6 @@ export async function jestInitGeneratorInternal(
tasks.push(updateDependencies(tree, options));
}
if (options.updatePackageScripts) {
await updatePackageScripts(tree, createNodes);
}
if (!options.skipFormat) {
await formatFiles(tree);
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { join } from 'path';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
getPackageManagerCommand,
getProjects,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
addProjectConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readJson, updateJson } from '@nx/devkit';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
readProjectConfiguration,
Tree,

View File

@ -29,7 +29,7 @@ export function nxComponentTestingPreset(
pathToConfig: string,
options?: NxComponentTestingOptions
) {
if (global.NX_GRAPH_CREATION || global.NX_CYPRESS_INIT_GENERATOR_RUNNING) {
if (global.NX_GRAPH_CREATION) {
// this is only used by plugins, so we don't need the component testing
// options, cast to any to avoid type errors
return nxBaseCypressPreset(pathToConfig) as any;

View File

@ -1,12 +1,23 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { readJson, NxJsonConfiguration, Tree } from '@nx/devkit';
import { readJson, Tree, ProjectGraph } from '@nx/devkit';
import { nextInitGenerator } from './init';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('init', () => {
let tree: Tree;
beforeEach(() => {
projectGraph = {
nodes: {},
dependencies: {},
};
tree = createTreeWithEmptyWorkspace();
});

View File

@ -5,12 +5,12 @@ import {
type GeneratorCallback,
type Tree,
readNxJson,
createProjectGraphAsync,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { reactDomVersion, reactVersion } from '@nx/react/src/utils/versions';
import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry';
import { nextVersion, nxVersion } from '../../utils/versions';
import { addPlugin } from './lib/add-plugin';
import type { InitSchema } from './schema';
function updateDependencies(host: Tree, schema: InitSchema) {
@ -52,7 +52,24 @@ export async function nextInitGeneratorInternal(
schema.addPlugin ??= addPluginDefault;
if (schema.addPlugin) {
addPlugin(host);
const { createNodes } = await import('../../plugins/plugin');
await addPlugin(
host,
await createProjectGraphAsync(),
'@nx/next/plugin',
createNodes,
{
startTargetName: ['start', 'next:start', 'next-start'],
buildTargetName: ['build', 'next:build', 'next-build'],
devTargetName: ['dev', 'next:dev', 'next-dev'],
serveStaticTargetName: [
'serve-static',
'next:serve-static',
'next-serve-static',
],
},
schema.updatePackageScripts
);
}
addGitIgnoreEntry(host);
@ -62,11 +79,6 @@ export async function nextInitGeneratorInternal(
installTask = updateDependencies(host, schema);
}
if (schema.updatePackageScripts) {
const { createNodes } = await import('../../plugins/plugin');
await updatePackageScripts(host, createNodes);
}
return installTask;
}

View File

@ -1,28 +0,0 @@
import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
export function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/next/plugin'
: plugin.plugin === '@nx/next/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/next/plugin',
options: {
buildTargetName: 'build',
devTargetName: 'dev',
startTargetName: 'start',
serveStaticTargetName: 'serve-static',
},
});
updateNxJson(tree, nxJson);
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readJson, readProjectConfiguration } from '@nx/devkit';
import { applicationGenerator } from './application';
@ -57,6 +59,37 @@ describe('app', () => {
expect(tree.read('my-app/project.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('my-app/tsconfig.json', 'utf-8')).toMatchSnapshot();
});
it('should add the nuxt and vitest plugins', () => {
const nxJson = readJson(tree, 'nx.json');
expect(nxJson.plugins).toMatchObject([
{
plugin: '@nx/eslint/plugin',
options: { targetName: 'lint' },
},
{
plugin: '@nx/vite/plugin',
options: { testTargetName: 'test' },
},
{
plugin: '@nx/nuxt/plugin',
options: { buildTargetName: 'build', serveTargetName: 'serve' },
},
{
plugin: '@nx/cypress/plugin',
options: { targetName: 'e2e' },
},
]);
expect(
nxJson.plugins.indexOf(
nxJson.plugins.find((p) => p.plugin === '@nx/nuxt/plugin')
)
).toBeGreaterThan(
nxJson.plugins.indexOf(
nxJson.plugins.find((p) => p.plugin === '@nx/vite/plugin')
)
);
});
});
describe('styles setup', () => {

View File

@ -40,11 +40,6 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
skipFormat: true,
});
tasks.push(jsInitTask);
const nuxtInitTask = await nuxtInitGenerator(tree, {
...options,
skipFormat: true,
});
tasks.push(nuxtInitTask);
tasks.push(ensureDependencies(tree, options));
addProjectConfiguration(tree, options.name, {
@ -113,6 +108,12 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
tasks.push(await addVitest(tree, options));
}
const nuxtInitTask = await nuxtInitGenerator(tree, {
...options,
skipFormat: true,
});
tasks.push(nuxtInitTask);
tasks.push(await addE2e(tree, options));
if (options.js) toJS(tree);

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readJson, readNxJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { nuxtInitGenerator } from './init';
@ -31,10 +33,6 @@ describe('init', () => {
options: { buildTargetName: 'build', serveTargetName: 'serve' },
plugin: '@nx/nuxt/plugin',
},
{
options: { testTargetName: 'test' },
plugin: '@nx/vite/plugin',
},
]);
});
});

View File

@ -1,22 +1,28 @@
import { GeneratorCallback, Tree } from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createProjectGraphAsync, GeneratorCallback, Tree } from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin';
import { InitSchema } from './schema';
import { addPlugin, updateDependencies } from './lib/utils';
import { updateDependencies } from './lib/utils';
export async function nuxtInitGenerator(host: Tree, schema: InitSchema) {
addPlugin(host);
await addPlugin(
host,
await createProjectGraphAsync(),
'@nx/nuxt/plugin',
createNodes,
{
buildTargetName: ['build', 'nuxt:build', 'nuxt-build'],
serveTargetName: ['serve', 'nuxt:serve', 'nuxt-serve'],
},
schema.updatePackageScripts
);
let installTask: GeneratorCallback = () => {};
if (!schema.skipPackageJson) {
installTask = updateDependencies(host, schema);
}
if (schema.updatePackageScripts) {
await updatePackageScripts(host, createNodes);
}
return installTask;
}

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { logger, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';

View File

@ -96,10 +96,7 @@ export async function addNxToMonorepo(options: Options) {
);
if (!options.legacy) {
packageJsonFiles.forEach((packageJsonPath) => {
markPackageJsonAsNxProject(
join(repoRoot, packageJsonPath),
cacheableOperations
);
markPackageJsonAsNxProject(join(repoRoot, packageJsonPath));
});
}

View File

@ -85,10 +85,7 @@ export async function addNxToNpmRepo(options: Options) {
if (options.legacy) {
markRootPackageJsonAsNxProjectLegacy(repoRoot, cacheableOperations, pmc);
} else {
markPackageJsonAsNxProject(
join(repoRoot, 'package.json'),
cacheableOperations
);
markPackageJsonAsNxProject(join(repoRoot, 'package.json'));
}
output.log({ title: '📦 Installing dependencies' });

View File

@ -194,21 +194,13 @@ export function markRootPackageJsonAsNxProjectLegacy(
writeJsonFile(`package.json`, json);
}
export function markPackageJsonAsNxProject(
packageJsonPath: string,
cacheableScripts: string[]
) {
export function markPackageJsonAsNxProject(packageJsonPath: string) {
const json = readJsonFile<PackageJson>(packageJsonPath);
if (!json.scripts) {
return;
}
json.nx = { includedScripts: [] };
for (let script of cacheableScripts) {
if (json.scripts[script]) {
json.nx.includedScripts.push(script);
}
}
json.nx = {};
writeJsonFile(packageJsonPath, json);
}

View File

@ -74,21 +74,21 @@ export async function initHandler(options: InitArgs): Promise<void> {
const { plugins, updatePackageScripts } = await detectPlugins();
if (!plugins.length) {
// If no plugins are detected/chosen, guide users to setup
// their targetDefaults correctly so their package scripts will work.
const packageJson: PackageJson = readJsonFile('package.json');
if (isMonorepo(packageJson)) {
await addNxToMonorepo({ interactive: options.interactive });
await addNxToMonorepo({
interactive: options.interactive,
nxCloud: false,
});
} else {
await addNxToNpmRepo({ interactive: options.interactive });
await addNxToNpmRepo({
interactive: options.interactive,
nxCloud: false,
});
}
} else {
const useNxCloud =
options.nxCloud ??
(options.interactive
? await connectExistingRepoToNxCloudPrompt()
: false);
(options.interactive ? await connectExistingRepoToNxCloudPrompt() : false);
const repoRoot = process.cwd();
const pmc = getPackageManagerCommand();
@ -102,6 +102,7 @@ export async function initHandler(options: InitArgs): Promise<void> {
runInstall(repoRoot, pmc);
if (plugins.length > 0) {
output.log({ title: '🔨 Configuring plugins' });
for (const plugin of plugins) {
execSync(
@ -114,12 +115,6 @@ export async function initHandler(options: InitArgs): Promise<void> {
}
);
}
if (!updatePackageScripts) {
const rootPackageJsonPath = join(repoRoot, 'package.json');
const json = readJsonFile<PackageJson>(rootPackageJsonPath);
json.nx = { includedScripts: [] };
writeJsonFile(rootPackageJsonPath, json);
}
if (useNxCloud) {
@ -132,7 +127,6 @@ export async function initHandler(options: InitArgs): Promise<void> {
}
);
}
}
output.log({
title: '👀 Explore Your Workspace',
@ -220,9 +214,8 @@ async function detectPlugins(): Promise<{
{
name: 'plugins',
type: 'multiselect',
message: `Which plugins would you like to add?`,
message: `Which plugins would you like to add? Press <Space> to select and <Enter> to submit.`,
choices: plugins.map((p) => ({ name: p, value: p })),
initial: plugins.map((_, i) => i) as unknown as number, // casting to avoid type error due to bad d.ts file from enquirer
},
]).then((r) => r.plugins);

View File

@ -20,4 +20,7 @@ export {
createProjectRootMappingsFromProjectConfigurations,
findProjectForPath,
} from './project-graph/utils/find-project-for-path';
export { retrieveProjectConfigurations } from './project-graph/utils/retrieve-workspace-files';
export { LoadedNxPlugin } from './project-graph/plugins/internal-api';
export * from './project-graph/error-types';
export { registerTsProject } from './plugins/js/utils/register';

View File

@ -0,0 +1,9 @@
jest.doMock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return {
nodes: {},
dependencies: {},
};
}),
}));

View File

@ -1,9 +1,70 @@
import { CreateNodesResultWithContext } from './plugins/internal-api';
import { ConfigurationResult } from './utils/project-configuration-utils';
import { ProjectConfiguration } from '../config/workspace-json-project-json';
export class ProjectsWithConflictingNamesError extends Error {
constructor(
conflicts: Map<string, string[]>,
public projects: Record<string, ProjectConfiguration>
) {
super(
[
`The following projects are defined in multiple locations:`,
...Array.from(conflicts.entries()).map(([project, roots]) =>
[`- ${project}: `, ...roots.map((r) => ` - ${r}`)].join('\n')
),
'',
"To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.",
].join('\n')
);
this.name = this.constructor.name;
}
}
export function isProjectsWithConflictingNamesError(
e: unknown
): e is ProjectsWithConflictingNamesError {
return (
e instanceof ProjectsWithConflictingNamesError ||
(typeof e === 'object' &&
'name' in e &&
e?.name === ProjectsWithConflictingNamesError.prototype.name)
);
}
export class ProjectsWithNoNameError extends Error {
constructor(
projectRoots: string[],
public projects: Record<string, ProjectConfiguration>
) {
super(
`The projects in the following directories have no name provided:\n - ${projectRoots.join(
'\n - '
)}`
);
this.name = this.constructor.name;
}
}
export function isProjectsWithNoNameError(
e: unknown
): e is ProjectsWithNoNameError {
return (
e instanceof ProjectsWithNoNameError ||
(typeof e === 'object' &&
'name' in e &&
e?.name === ProjectsWithNoNameError.prototype.name)
);
}
export class ProjectConfigurationsError extends Error {
constructor(
public readonly errors: Array<MergeNodesError | CreateNodesError>,
public readonly errors: Array<
| MergeNodesError
| CreateNodesError
| ProjectsWithNoNameError
| ProjectsWithConflictingNamesError
>,
public readonly partialProjectConfigurationsResult: ConfigurationResult
) {
super('Failed to create project configurations');

View File

@ -173,6 +173,7 @@ export function readPackageJson(): any {
return {}; // if package.json doesn't exist
}
}
// Original Exports
export { FileData };
// TODO(17): Remove these exports

View File

@ -34,6 +34,8 @@ import {
CreateNodesError,
MergeNodesError,
ProjectConfigurationsError,
ProjectsWithNoNameError,
ProjectsWithConflictingNamesError,
} from './error-types';
import { DaemonProjectGraphError } from '../daemon/daemon-project-graph-error';
import { loadNxPlugins, LoadedNxPlugin } from './plugins/internal-api';
@ -179,7 +181,12 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
export class ProjectGraphError extends Error {
readonly #errors: Array<
CreateNodesError | ProcessDependenciesError | ProcessProjectGraphError
| CreateNodesError
| MergeNodesError
| ProjectsWithNoNameError
| ProjectsWithConflictingNamesError
| ProcessDependenciesError
| ProcessProjectGraphError
>;
readonly #partialProjectGraph: ProjectGraph;
readonly #partialSourceMaps: ConfigurationSourceMaps;
@ -188,6 +195,8 @@ export class ProjectGraphError extends Error {
errors: Array<
| CreateNodesError
| MergeNodesError
| ProjectsWithNoNameError
| ProjectsWithConflictingNamesError
| ProcessDependenciesError
| ProcessProjectGraphError
>,

View File

@ -1555,7 +1555,11 @@ describe('project-configuration-utils', () => {
undefined,
{},
['libs/a/project.json', 'libs/b/project.json'],
[new LoadedNxPlugin(fakeTagPlugin, { plugin: fakeTagPlugin.name })]
[
new LoadedNxPlugin(fakeTagPlugin, {
plugin: fakeTagPlugin.name,
}),
]
);
expect(projectConfigurations.projects).toEqual({

View File

@ -26,6 +26,10 @@ import {
MergeNodesError,
ProjectConfigurationsError,
isAggregateCreateNodesError,
ProjectsWithNoNameError,
ProjectsWithConflictingNamesError,
isProjectsWithConflictingNamesError,
isProjectsWithNoNameError,
} from '../error-types';
export type SourceInformation = [file: string, plugin: string];
@ -321,7 +325,12 @@ export async function createProjectConfigurations(
performance.mark('build-project-configs:start');
const results: Array<Promise<Array<CreateNodesResultWithContext>>> = [];
const errors: Array<CreateNodesError | MergeNodesError> = [];
const errors: Array<
| CreateNodesError
| MergeNodesError
| ProjectsWithNoNameError
| ProjectsWithConflictingNamesError
> = [];
// We iterate over plugins first - this ensures that plugins specified first take precedence.
for (const {
@ -425,7 +434,21 @@ export async function createProjectConfigurations(
Object.assign(externalNodes, pluginExternalNodes);
}
const projects = readProjectConfigurationsFromRootMap(projectRootMap);
let projects: Record<string, ProjectConfiguration>;
try {
projects = readProjectConfigurationsFromRootMap(projectRootMap);
} catch (e) {
if (
isProjectsWithNoNameError(e) ||
isProjectsWithConflictingNamesError(e)
) {
projects = e.projects;
errors.push(e);
} else {
throw e;
}
}
const rootMap = createRootMap(projectRootMap);
performance.mark('createNodes:merge - end');
@ -467,7 +490,8 @@ export function readProjectConfigurationsFromRootMap(
// If there are projects that have the same name, that is an error.
// This object tracks name -> (all roots of projects with that name)
// to provide better error messaging.
const errors: Map<string, string[]> = new Map();
const conflicts = new Map<string, string[]>();
const projectRootsWithNoName: string[] = [];
for (const [root, configuration] of projectRootMap.entries()) {
// We're setting `// targets` as a comment `targets` is empty due to Project Crystal.
@ -478,31 +502,26 @@ export function readProjectConfigurationsFromRootMap(
const { name } = readJsonFile(join(root, 'package.json'));
configuration.name = name;
} catch {
throw new Error(`Project at ${root} has no name provided.`);
projectRootsWithNoName.push(root);
}
}
if (configuration.name in projects) {
let rootErrors = errors.get(configuration.name) ?? [
let rootErrors = conflicts.get(configuration.name) ?? [
projects[configuration.name].root,
];
rootErrors.push(root);
errors.set(configuration.name, rootErrors);
conflicts.set(configuration.name, rootErrors);
projects[configuration.name] = configuration;
} else {
projects[configuration.name] = configuration;
}
}
if (errors.size > 0) {
throw new Error(
[
`The following projects are defined in multiple locations:`,
...Array.from(errors.entries()).map(([project, roots]) =>
[`- ${project}: `, ...roots.map((r) => ` - ${r}`)].join('\n')
),
'',
"To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.",
].join('\n')
);
if (conflicts.size > 0) {
throw new ProjectsWithConflictingNamesError(conflicts, projects);
}
if (projectRootsWithNoName.length > 0) {
throw new ProjectsWithNoNameError(projectRootsWithNoName, projects);
}
return projects;
}

View File

@ -6,8 +6,8 @@ import {
} from '../../adapter/angular-json';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import {
createProjectConfigurations,
ConfigurationResult,
createProjectConfigurations,
} from './project-configuration-utils';
import { LoadedNxPlugin, loadNxPlugins } from '../plugins/internal-api';
import {
@ -16,7 +16,6 @@ import {
} from '../../utils/workspace-context';
import { buildAllWorkspaceFiles } from './build-all-workspace-files';
import { join } from 'path';
import { NxPlugin } from '../plugins';
/**
* Walks the workspace directory to create the `projectFileMap`, `ProjectConfigurations` and `allWorkspaceFiles`
@ -60,17 +59,21 @@ export async function retrieveWorkspaceFiles(
/**
* Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed.
*/
export async function retrieveProjectConfigurations(
export function retrieveProjectConfigurations(
plugins: LoadedNxPlugin[],
workspaceRoot: string,
nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> {
const projects = await _retrieveProjectConfigurations(
const globPatterns = configurationGlobs(plugins);
const workspaceFiles = globWithWorkspaceContext(workspaceRoot, globPatterns);
return createProjectConfigurations(
workspaceRoot,
nxJson,
workspaceFiles,
plugins
);
return projects;
}
export async function retrieveProjectConfigurationsWithAngularProjects(
@ -95,27 +98,11 @@ export async function retrieveProjectConfigurationsWithAngularProjects(
workspaceRoot
);
const res = _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins);
const res = retrieveProjectConfigurations(plugins, workspaceRoot, nxJson);
cleanup();
return res;
}
function _retrieveProjectConfigurations(
workspaceRoot: string,
nxJson: NxJsonConfiguration,
plugins: LoadedNxPlugin[]
): Promise<ConfigurationResult> {
const globPatterns = configurationGlobs(plugins);
const workspaceFiles = globWithWorkspaceContext(workspaceRoot, globPatterns);
return createProjectConfigurations(
workspaceRoot,
nxJson,
workspaceFiles,
plugins
);
}
export function retrieveProjectConfigurationPaths(
root: string,
plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown>

View File

@ -5,7 +5,6 @@ import ProjectJsonProjectsPlugin from '../plugins/project-json/build-nodes/proje
import TargetDefaultsPlugin from '../plugins/target-defaults/target-defaults-plugin';
import * as PackageJsonWorkspacesPlugin from '../plugins/package-json-workspaces';
import { NxPluginV2 } from '../project-graph/plugins';
import { LoadedNxPlugin } from '../project-graph/plugins/internal-api';
/**
* @deprecated Add targets to the projects in a {@link CreateNodes} function instead. This will be removed in Nx 19

View File

@ -1,12 +1,24 @@
import { readNxJson, Tree, updateNxJson } from '@nx/devkit';
import { ProjectGraph, readNxJson, Tree, updateNxJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { initGenerator } from './init';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('@nx/playwright:init', () => {
let tree: Tree;
beforeEach(() => {
projectGraph = {
nodes: {},
dependencies: {},
};
tree = createTreeWithEmptyWorkspace();
});

View File

@ -1,13 +1,13 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
formatFiles,
GeneratorCallback,
readNxJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin';
import { nxVersion, playwrightVersion } from '../../utils/versions';
import { InitGeneratorSchema } from './schema';
@ -45,11 +45,14 @@ export async function initGeneratorInternal(
}
if (options.addPlugin) {
addPlugin(tree);
}
if (options.updatePackageScripts) {
await updatePackageScripts(tree, createNodes);
await addPlugin(
tree,
await createProjectGraphAsync(),
'@nx/playwright/plugin',
createNodes,
{ targetName: ['e2e', 'playwright:e2e', 'playwright-e2e'] },
options.updatePackageScripts
);
}
if (!options.skipFormat) {
@ -59,25 +62,4 @@ export async function initGeneratorInternal(
return runTasksInSerial(...tasks);
}
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
if (
!nxJson.plugins.some((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
nxJson.plugins.push({
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
},
});
updateNxJson(tree, nxJson);
}
}
export default initGenerator;

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
joinPathFragments,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
Tree,
addProjectConfiguration,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree, readJson, readProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { executorGenerator } from './executor';

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
GeneratorsJson,
readJson,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,

View File

@ -1,3 +1,5 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readJson, readProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { migrationGenerator } from './migration';

Some files were not shown because too many files have changed in this diff Show More