Nx Workspace

A workspace contains your Angular application (or applications) and any supporting libraries you create. It is a monorepo for your application domain.

With Angular CLI you can add different types of projects to a single workspace (by default you can add applications and libraries). This is great, but is not sufficient to enable the monorepo-style development. Nx adds an extra layer of tooling to make this possible.

Let's start by taking a look at how we can create a new Nx Workspace.

Creating a New Nx Workspace

You can install Nx in the following way:

npm install -g @angular/cli @nrwl/schematics

or

yarn global add @angular/cli @nrwl/schematics

The @nrwl/schematics library contains a binary script that provides the command create-nx-workspace. That command can be used to create a new Nx Workspace:

create-nx-workspace myworkspacename

This will create a new Nx Workspace using a sandboxed environment...running the Angular CLI ng new command under the hood and using the Nx schematics collection.

You can also add Nx capabilities to an existing CLI project by running:

ng add @nrwl/schematics

What you end up with is a Nx Workspace with the following files/folders created.

apps/
libs/
tools/
angular.json
nx.json
tslint.json
tsconfig.json

It's similar to the standard Angular CLI projects with a few changes:

  • It has an apps dir where all applications are placed
  • It has a libs dir where all custom library code is placed

A new Nx Workspace does not set up an initial application, but adding one is simple.

Creating an App

Adding new apps to an Nx Workspace is done by using the Angular CLI generate command. Nx has a schematic named app that can be used to add a new app to our workspace:

ng g app myapp
ng generate app myapp # same thing
ng generate application myapp # same thing

This will create a new app, will place it in the apps directory, and will configure the angular.json and nx.json files to support the new app. It will also configure the root NgModule to import the NxModule code so we can take advantage of things like DataPersistence.

Run ng generate app --help to see the list of available options:

usage: ng generate app <name> [options]
options:
  --directory
    The directory of the new application.
  --dry-run (-d)
    Run through without making any changes.
  --force (-f)
    Forces overwriting of files.
  --inline-style (-s)
    Specifies if the style will be in the ts file.
  --inline-template (-t)
    Specifies if the template will be in the ts file.
  --prefix (-p)
    The prefix to apply to generated HTML selector of components.
  --routing 
    Generates a routing module.
  --skip-package-json 
    Do not add dependencies to package.json.
  --skip-tests (-S)
    Skip creating spec files.
  --style 
    The file extension to be used for style files.
  --tags 
    Add tags to the application (used for linting)
  --view-encapsulation 
    Specifies the view encapsulation strategy.

Most of these options are identical to the ones supported by the default CLI application, but the following are new or different: directory, routing, and tags.

  • ng generate app myapp --directory=myteam will create a new application in apps/myteam/myapp.
  • ng generate app myapp --routing will configure the root NgModule to wire up routing, as well as add a <router-outlet> to the AppComponent template to help get us started.
  • ng generate app myapp --tags=shared,experimental will annotate the created app with the two tags, which can be used for advanced code analysis. Read more below.

Creating a Lib

Adding new libs to an Nx Workspace is done by using the Angular CLI generate command, just like adding a new app.

ng generate lib mylib
ng generate library mylib # same thing

This will create a new lib, will place it in the libs directory, and will configure the angular.json and nx.json files to support the new lib.

Run ng generate lib --help to see the list of available options:

usage: ng generate lib <name> [options]
options:
  --directory 
    A directory where the app is placed
  --dry-run (-d)
    Run through without making any changes.
  --force (-f)
    Forces overwriting of files.
  --lazy 
    Add RouterModule.forChild when set to true, and a simple array of routes when set to false.
  --parent-module 
    Update the router configuration of the parent module using loadChildren or children, depending on what `lazy` is set to.
  --prefix (-p)
    The prefix to apply to generated HTML selector of components.
  --publishable 
    Generate a simple TS library when set to true.
  --routing 
    Add router configuration. See lazy for more information.
  --skip-package-json 
    Do not add dependencies to package.json.
  --skip-ts-config 
    Do not update tsconfig.json for development experience.
  --tags 
    Add tags to the library (used for linting)

Most of these options are identical to the ones supported by the default CLI library, but the following are new or different: directory, routing, lazy, parent-module, publishable, and tags.

  • ng generate lib mylib --directory=myteam will create a new application in libs/myteam/mylib.
  • ng generate lib mylib --routing will configure the lib's NgModule to wire up routing to be loaded eagerly.
  • ng generate lib mylib --routing --lazy will configure the lib's NgModule to wire up routing to be loaded lazily.
  • ng generate lib mylib --routing --parent-module=apps/myapp/src/app/app.module.ts will configure the lib's NgModule to wire up routing and will configure app.module.ts to load the library.
  • ng generate lib mylib --routing --lazy --parent-module=apps/myapp/src/app/app.module.ts will configure the lib's NgModule to wire up routing and will configure app.module.ts to load the library.
  • ng generate lib mylib --publishable will generate a few extra files configuring for ng-packagr. You can then run ng build mylib to create an npm package you can publish to a npm registry. This is very rarely needed when developing in a monorepo. In this case the clients of the library are in the same repository, so no packaging and publishing step is required.
  • ng generate lib mylib --tags=shared,experimental will annotate the created lib with the two tags, which can be used for advanced code analysis. Read more below.

Note when creating lazy-loaded libraries, you need to add an entry to the tsconfig.app.json file of the parent module app, so TypeScript knows to build it as well:

{
  . . .

  "include": [
      "**/*.ts"
      /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */

      , "../../../libs/mymodule/src/index.ts"
  ]
}

In most cases, Nx will do it by default. Sometime, you need to manually add this entry.

This is great, but is not sufficient to enable the monorepo-style development. Nx adds an extra layer of tooling to make this possible.

Analyzing and Visualizing the Dependency Graph

To be able to support the monorepo-style development, the tools must know how different projects in your workspace depend on each other. Nx uses advanced code analysis to build this dependency graph.

You can visualize it by running npm run dep-graph or yarn dep-graph.

dependency-graph

You can also visualize what is affected by your change, by using the affected:dep-graph command.

dependency-graph-affected

Run npm run affected:dep-graph -- --help or yarn affected:dep-graph --help to see the available options:

Graph dependencies affected by changes

Run command using --base=[SHA1] --head=[SHA2]:
  --base  Base of the current branch (usually master)                   [string]
  --head  Latest commit of the current branch (usually HEAD)            [string]

or using:
  --files        A list of files delimited by commas                     [array]
  --uncommitted  Uncommitted changes
  --untracked    Untracked changes

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]
  --file     output file (e.g. --file=.vis/output.json)

First, you need to tell Nx what you changed, and you can do it in one of the following ways:

  • npm run affected:dep-graph -- --base=[SHA1] --base=[SHA2] or yarn affected:dep-graph --base=[SHA1] --base=[SHA2]. Nx will calculate what changed between the two SHAs, and will graph what is affected by the change. For instance, yarn affected:dep-graph --base=origin/master --base=HEAD will show what is affected by a PR.
  • npm run affected:dep-graph -- --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts or yarn affected:dep-graph --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts. Nx will graph what is affected by changing the two index files.
  • npm run affected:dep-graph -- --uncommitted or yarn affected:dep-graph --uncommitted. Nx will graph what is affected by the uncommitted files (this is useful during development).
  • npm run affected:dep-graph -- --untracked or yarn affected:dep-graph --untracked. Nx will graph what is affected by the untracked files (this is useful during development).

By default, the dep-graph and affected:dep-graph commands will open the browser to show the graph, but you can also output the graph into a file by running:

  • npm run dep-graph -- --file=graph.json or yarn dep-graph --file=graph.json will emit a json file.
  • npm run dep-graph -- --file=graph.dot or yarn dep-graph --file=graph.dot will emit a dot file.
  • npm run dep-graph -- --file=graph.svg or yarn dep-graph --file=graph.svg will emit an svg file.

Imposing Constraints on the Dependency Graph

If you partition your code into well-defined cohesive units, even a small organization will end up with a dozen apps and dozens or hundreds of libs. If all of them can depend on each other freely, the chaos will ensue and the workspace will become unmanageable.

Depend on Public API

To help with that Nx uses code analyses to make sure projects can only depend on each other's well-defined public API, so the following is an error:

import { MyService } from '@myworkspace/mylib/src/myservice';

It also allows you to declaratively impose constraints on how projects can depend on each other.

Separate what is team specific from what is shared

Enterprise development teams love this capability to constrain library dependencies. For instance, say you want to separate team-specific libraries from shared ones, so another team won't create a dependency on your internal library. You start by adding tags to your libraries, like this:

ng g lib mylib --directory=myteam --tags=team:myteam
ng g lib anotherlib --directory=anotherteam --tags=team:anotherteam

You can modify the tags after the fact by changing nx.json. Then, add the following to your tslint.json:

"nx-enforce-module-boundaries": [
  true,
  {
    "allow": [],
    "depConstraints": [
      { "sourceTag": "team:myteam", "onlyDependOnLibsWithTags": ["team:myteam", "shared"] },
      { "sourceTag": "team:anotherteam", "onlyDependOnLibsWithTags": ["team:anotherteam", "shared"] }
      { "sourceTag": "shared", "onlyDependOnLibsWithTags": ["shared"] }
      { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
    ]
  }
]

With this setup, projects annotated with team:myteam can only depend on projects annotated with team:myteam or shared.

Define Library Types

You can define which projects contain components, NgRx code, and features, so you, for instance, can disallow projects containing dumb UI components depend on NgRx.

ng g lib user-data --tags=team:myteam,lib:data-access
ng g lib buttons --tags=team:myteam,lib:components

Add the following to your tslint.json:

"nx-enforce-module-boundaries": [
  true,
  {
    "allow": [],
    "depConstraints": [
      { "sourceTag": "lib:data-acess", "onlyDependOnLibsWithTags": ["lib:data-acces", "lib:utils"] },
      { "sourceTag": "lib:components", "onlyDependOnLibsWithTags": ["lib:components", "lib:utils"] },

      { "sourceTag": "team:myteam", "onlyDependOnLibsWithTags": ["team:myteam", "shared"] },
      { "sourceTag": "team:myteam", "onlyDependOnLibsWithTags": ["team:myteam", "shared"] },
      { "sourceTag": "team:anotherteam", "onlyDependOnLibsWithTags": ["team:anotherteam", "shared"] }
      { "sourceTag": "shared", "onlyDependOnLibsWithTags": ["shared"] }
      { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
    ]
  }
]

With this setup, your UI components won't be able to depend on NgRx and vice versa.

The team and lib tags aren't special. The tagging mechanism is completely generic, so you can model what works best for your organization. For instance, you can define which projects are experimental and which are stable, so stable applications cannot depend on experimental projects etc.

Prebuilt Constraints

Nx comes with a few predefined rules that apply to all workspaces:

  • Libs cannot imports apps.
  • A project loading a library via loadChildren cannot also import it using an ESM import.
  • Circular dependencies aren't allowed.
  • Libs cannot be imported using relative imports.

Implicit Dependencies

Nx uses its built-in intelligence to create the dependency graph of the apps and libs, and that gets used to figure out what needs to be rebuilt and retested. There are certain files, however, that Nx cannot analyze. That’s why Nx has support for implicit dependencies. They are defined in nx.json.

{
  "npmScope": "mycompany",
  "implicitDependencies": {
    "package.json": "*",
    "angular.json": "*",
    "tsconfig.json": "*",
    "tslint.json": "*",
    "nx.json": "*"
},
"projects": {}
}

The ”package.json”: “*” line tells Nx that a change to package.json will affect every single project. Since the root-level README file isn’t listed, changing it won’t affect anything.

We can be more specific and list the projects that will be affected by a particular file.

{
  "npmScope": "mycompany",
  "implicitDependencies": {
    "package.json": "*",
    "angular.json": "*",
    "tsconfig.json": "*",
    "tslint.json": "*",
    "nx.json": "*",

    "tools/scripts/app1-rename-bundles.js": ["app1"]
  },
  "projects": {}
}

Exceptions

As with everything there are exceptions, which can also add to your tslint.json.

"nx-enforce-module-boundaries": [
  true,
  {
    "allow": ["@myworkspace/mylib"],
    "depConstraints": [
    ]
  }
]

Rebuilding and Retesting What is Affected

As with a regular CLI project, you can build and test apps and libs.

ng g app myapp
ng g app myapp2 --directory=mydirectory
ng g lib mylib
ng g lib mylib2 --directory=mydirectory

ng build myapp
ng build mydirectory-myapp2
ng build mylib # work if the lib is marked as publishable
ng build mydirectory-mylib2 # work if the lib is marked as publishable

ng test myapp # runs unit tests for myapp
ng test mylib # runs unit tests for mylib
ng e2e myapp-e2e # runs e2e tests for myapp

Now imagine, myapp depends on mylib. If we make a change to mylib, we need to make sure nothing in the workspace is affected. Typically, you would do it like this:

ng build mylib
ng test mylib
ng build myapp
ng test myapp

In many organizations, you would have dozens or hundreds of apps and libs. To be productive in a monorepo, you need to be able to check that your change is safe, and rebuilding and retesting everything on every change won't scale, tracing the dependencies manually (as shown above) wont's scale either. Nx uses code analysis to determine what needs to be rebuild and retested, and it provides the following three commands you can use: affected:build, affected:test, and affected:e2e.

yarn affected:build --base=master --head=HEAD # reruns build for all the projects affected by a PR
yarn affected:test --base=master --head=HEAD  # reruns unit tests for all the projects affected by a PR
yarn affected:e2e --base=master --head=HEAD # reruns e2e tests for all the projects affected by a PR

When executing these commands, Nx will topologically sort the projects, and will run what it can in parallel. But we can also explicitly pass --parallel like so:

yarn affected:build --base=master --parallel
yarn affected:test --base=master --parallel
yarn affected:e2e --base=master --parallel

We can also pass --maxParallel to specify the number of parallel processes.

affected:build

Run npm run affected:build -- --help or yarn affected:build --help to see the available options:

Build applications affected by changes

Run command using --base=[SHA1] (affected by the committed, uncommitted and
untracked changes):
  --base  Base of the current branch (usually master)                   [string]

or using --base=[SHA1] --head=[SHA2] (affected by the committed changes):
  --base  Base of the current branch (usually master)                   [string]
  --head  Latest commit of the current branch (usually HEAD)            [string]

or using:
  --files        A list of files delimited by commas                     [array]
  --uncommitted  Uncommitted changes
  --untracked    Untracked changes

Options:
  --help         Show help                                             [boolean]
  --version      Show version number                                   [boolean]
  --parallel     Parallelize the command              [boolean] [default: false]
  --maxParallel  Max number of parallel processes          [number] [default: 3]
  --all          All projects
  --exclude      Exclude certain projects from being processed
                                                           [array] [default: []]
  --only-failed  Isolate projects which previously failed
                                                      [boolean] [default: false]
  • npm run affected:build -- --base=[SHA1] --base=[SHA2] or yarn affected:build --base=[SHA1] --base=[SHA2]. Nx will calculate what changed between the two SHAs, and will build the apps affected by the change. For instance, yarn affected:build --base=origin/master --base=HEAD will rebuild what is affected by a PR.
  • npm run affected:build -- --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts or yarn affected:build --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts. Nx will build what is affected by changing the two index files.
  • npm run affected:build -- --uncommitted or yarn affected:build --uncommitted. Nx will build what is affected by the uncommitted files (this is useful during development).
  • npm run affected:build -- --untracked or yarn affected:build --untracked. Nx will build what is affected by the untracked files (this is useful during development).

All other options will be passed into the underlying build command (e.g., yarn affected:build --base=origin/master --base=HEAD --prod).

affected:test

Run npm run affected:test -- --help or yarn affected:test --help to see the available options:

Test applications affected by the change

Run command using --base=[SHA1] (affected by the committed, uncommitted and
untracked changes):
  --base  Base of the current branch (usually master)                   [string]

or using --base=[SHA1] --head=[SHA2] (affected by the committed changes):
  --base  Base of the current branch (usually master)                   [string]
  --head  Latest commit of the current branch (usually HEAD)            [string]

or using:
  --files        A list of files delimited by commas                     [array]
  --uncommitted  Uncommitted changes
  --untracked    Untracked changes

Options:
  --help         Show help                                             [boolean]
  --version      Show version number                                   [boolean]
  --parallel     Parallelize the command              [boolean] [default: false]
  --maxParallel  Max number of parallel processes          [number] [default: 3]
  --all          All projects
  --exclude      Exclude certain projects from being processed
                                                           [array] [default: []]
  --only-failed  Isolate projects which previously failed
                                                      [boolean] [default: false]
  • npm run affected:test -- --base=[SHA1] --base=[SHA2] or yarn affected:test --base=[SHA1] --base=[SHA2]. Nx will calculate what changed between the two SHAs, and will test the projects affected by the change. For instance, yarn affected:test --base=origin/master --base=HEAD will retest what is affected by a PR.
  • npm run affected:test -- --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts or yarn affected:test --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts. Nx will test what is affected by changing the two index files.
  • npm run affected:test -- --uncommitted or yarn affected:test --uncommitted. Nx will test what is affected by the uncommitted files (this is useful during development).
  • npm run affected:test -- --untracked or yarn affected:test --untracked. Nx will test what is affected by the untracked files (this is useful during development).

All other options will be passed into the underlying test command (e.g., yarn affected:test --base=origin/master --base=HEAD --sm=false).

affected:e2e

Run npm run affected:e2e -- --help or yarn affected:e2e --help to see the available options:

Run e2e tests for the applications affected by changes

Run command using --base=[SHA1] (affected by the committed, uncommitted and
untracked changes):
  --base  Base of the current branch (usually master)                   [string]

or using --base=[SHA1] --head=[SHA2] (affected by the committed changes):
  --base  Base of the current branch (usually master)                   [string]
  --head  Latest commit of the current branch (usually HEAD)            [string]

or using:
  --files        A list of files delimited by commas                     [array]
  --uncommitted  Uncommitted changes
  --untracked    Untracked changes

Options:
  --help         Show help                                             [boolean]
  --version      Show version number                                   [boolean]
  --all          All projects
  --exclude      Exclude certain projects from being processed
                                                           [array] [default: []]
  --only-failed  Isolate projects which previously failed
                                                      [boolean] [default: false]
  • npm run affected:e2e -- --base=[SHA1] --base=[SHA2] or yarn affected:e2e --base=[SHA1] --base=[SHA2]. Nx will calculate what changed between the two SHAs, and will run e2e test for the apps affected by the change. For instance, yarn affected:test --base=origin/master --base=HEAD will retest what is affected by a PR.
  • npm run affected:e2e -- --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts or yarn affected:e2e --files=libs/mylib/src/index.ts,libs/mylib2/src/index.ts. Nx will run e2e tests for what is affected by changing the two index files.
  • npm run affected:e2e -- --uncommitted or yarn affected:e2e --uncommitted. Nx will run e2e tests for what is affected by the uncommitted files (this is useful during development).
  • npm run affected:e2e -- --untracked or yarn affected:e2e --untracked. Nx will run e2e tests for what is affected by the untracked files (this is useful during development).

All other options will be passed into the underlying test command (e.g., yarn affected:test --base=origin/master --base=HEAD --sm=false).

Workspace-Specific Schematics

Nx helps promote best practices.

Some of the them are well-established in the Angular community. For instance, most non-trivial Angular projects use NgRx, so Nx comes with a set of built-in code generators and runtime libraries to make sure your applications’ state management is consistent. Just run ng g ngrx accounts --module=one/two/app.module.ts to set everything up (read more here).

Some, however, are specific to your organization. But Nx can help with those as well. Say, for example, we want to promote a pattern of encapsulating NgRx-related code into data-access libraries. This is how easy it is to do it with Nx:

Start by generating a new workspace schematic. workspace-schematic-3

Then, provide the implementation. workspace-schematic-2

Finally, invoke it to generate a new data-access lib. workspace-schematic-1

Check out the [Workspace-Specific Schematics](/nx/workspace-specific schematics) guide to learn more.

Formatters

We learned at Google that anything that can be automated should be automated. And one of those things is code formatting. That’s why Nx comes with built-in support for Prettier.

There is absolute nothing you need to configure. Just call format:write to format the affected files, and format:check in the CI to guarantee that everything is formatted consistently.

Run npm run format:write -- --help or yarn format:write --help to see the available options:

nx format:write

Overwrite un-formatted files

Run command using --base=[SHA1] --head=[SHA2]:
  --base  Base of the current branch (usually master)                   [string]
  --head  Latest commit of the current branch (usually HEAD)            [string]

or using:
  --files        A list of files delimited by commas                     [array]
  --uncommitted  Uncommitted changes
  --untracked    Untracked changes

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]

npm run format:write -- --base=[SHA1] --base=[SHA2] or yarn format:write --base=[SHA1] --base=[SHA2]. Nx will calculate what changed between the two SHAs, and will format the changed files. For instance, yarn format:write --base=origin/master --base=HEAD will format what is affected by a PR.

The format:check command accepts the same options, but instead of formatting files, it will throw if any of the files aren't formatted properly. This is useful for CI/CD.

Updating Nx

If you created an Nx Workspace using Nx 6.0.0 and then decided to upgrade the version of Nx, the Nx update command can handle modifying the configuration file and the source files as needed to get your workspace configuration to match the requirements of the new version of Nx.

Leverage the Angular CLI

All of the standard Angular CLI commands are available because Nx simply is an extension to the Angular CLI.

We can serve any of the apps.

ng serve --project=myapp

And we can use the default Angular CLI schematics to generate code for either apps or libs.

ng generate component myButton --project=mylib

Looking for more best practices for enterprise-scale Angular development from the Nrwl team?

Visit Nx Playbook