Angular 6 Workspace :: Test-Drive
All The Cool Cats are Using Workspaces Now, Are YOU?
Github.com: https://github.com/buildmotion/workspace-demo
The most recent version of Angular is a significant release — version 6.0.3 as of this post. We get a lot of new tools and features that provide a more efficient develoment environment.
When we use the new Angular 6 CLI, the default environment is now a workspace (well, maybe not — more later) that allows for the development of multiple applications and libraries in a single workspace. When you add new applications and/or libraries, you are creating project
items. A project item has a specific type to indicate whether it is an application
or a library
.
application: is a single-page web application.
library: is a module that is shared by multiple applications
The workspace tooling for Angular development was introduced awhile back by NRWL.io’s Nx extensions. A set of schematics that allow you to create a workspace
using custom configuration provided by NRWL. If you use Nx, you create apps
that are either a lib
or an app
(Angular Single Page Web application). It seems like I just started using NRWL's Nx Workspace 1.0 - on May 22, 2018 NRWL announced the release of version 6.0 built on top of Angular CLI 6.0. The tooling for creating and using libraries is moving pretty fast these days.
Being able to reuse libraries across multiple applications is a great feature. It was definitely possible before. However, the workflow was much more intensive and required lots of configuration of the libraries. It is now much easier with version 6 of the Angular CLI.
Uses for an Angular Workspace
I am going to assume the default use of the new Angular Workspace is to support the ability to share Angular libraries amongst multiple web applications. The previous Angular CLI allows us to create an @NgModule
within a web application with the following command.
ng generate module MyNewModule
The new Angular 6 CLI will you to create an Angular library, which is really an @NgModule
, outside of the specified web application. Since we have this new capability, we have more options. Now we have to decide which modules are candidates for reuse and sharing verses which ones should be contained in the specified application. My opinion is that modules are great way to organize your code and applications. They provide encapsulation and support many good programming principles. Organizing related things into a single module provides many benefits. Now, if that module is a candidate for reuse by other applications - we can now create it as a library in the new workspace. Think of it as an additional code organziation strategy. Use the following to create a new library.
ng generate library MyNewLibrary
So, what is the difference between a module
and a library
? If the module can be used by more than one consumer (i.e., Angular web applications or other modules) it can be considered a library. A module describes the ability or mechanism to group related things. And a library indicates that it contains things that are useful to consumers of the library - think of a real physical library where you can visit and checkout items for your enjoyment. Now your library can be shared by many consumers - kind of like a book in the library.
So, what is the difference between a library
and a package
? I'll assume if the module is published to a repository for consumption via a package manager (think NPM) that it is a package. However, many recent blog posts and community-speak is referring to these as libraries. The remainder of this article will refer to published or non-published items as libraries.
Non-Published Libraries
The current implementation of the Angular Workspace is for developing multiple web appications sharing libraries in a single development environment. The current implementation of the the Angular Workspace is NOT a development environment (by default) for creating libraries to be published on NPM. Although the environment can use ng-packagr
to build and output a library project to a dist
folder completely ready for publishing - it is not the default use of the workspace environment. In fact, when you run the default build script, only the default application is output to the dist
folder. The build strategy is application-centric. It will build the applications and include any other modules referenced and used by the specified application.
Publishing packages is a very different workflow. If you want to compile and publish one of the libraries in the new workspace, you can use the Angular CLI command ng build --project=lib-one
. The new project configuration in the angular.json
knows that this type of project is library
it can now use different build strategies for different project types - library projects will use ng-packagr
to build and create distributable output ready for publishing. NPM requires additional management of the peerDependencies
in the library's package.json file. This is not done by default during the build process of libraries or applications in the new workspace. You will need to manually set these peer dependencies. Additionally, NPM requires semantic versioning of the published libraries. Before publishing packages to NPM, you will need to update the semantic version of the library. The web applications in the workspace are not concerned with the package
notion of the library - they are not referenced in the package.json by name/version; and they are not installed in the node_modules
folder of the workspace. Applications in the worksapce reference libraries by file path as we shall see.
There is no build or output of the library projects in the dist
folder when you build an application project. To accomplish a distribution build of the libraries, you will need to add a new build script to the package.json file. You will also want to update the version of your package before creating a distribution build for publishing. The following script below shows how to build (2) libraries - this specific build process will use ng-packagr to create distribution version of the library for: UMD, esm5, esm2015, fesm5, and fesm2015. This special build process follows the Angular Package Format. Nice!
"package": "ng build --project=libOne && ng build --project=libTwo",
Run the script to build the libraries with the output going to the dist
folder.
npm run package
Building and outputting the libraries to the dist
folder is really only for publishing packages (libraries) to NPM. None of the applications are using or importing these packages from the local dist
folder.
workspace-demo
I’m creating a workspace-demo
workspace available on Github to show the basic usage of a development workflow for multiple libraries and applications. The default workspace contains a default web application. Therefore, to demonstrate the multiple libraries, I'll add (2) new library projects to the workspace uskng the CLI.
ng generate library libOne
ng generate library libTwo
Using a Library
As I add the service to the constructor of the AppComponent
, the editor will add the import statement to the LibOneService
. Notice that the right-side of the import is using a filepath reference to the public_api
of the specified library - there is no package name usage here.
A simplified developer workflow here does not require us to add an entry in the dependencies
section of the package.json
. The library is not published and is not contained in the node_modules
folder as most packages are. Remember that these are shared libraries in a workspace environment.
The service will have a simple SayHello
method - we just want to see how things work here.
I’ll update the launch.json
to add a configuration to use localhost and port 4200.
Now we can run ng serve
and then press F5 to run the application.
That was easy. Now let’s try something a little more real world.
Application-to-ServiceOne-to-ServiceTwo
In the real-world, it is not uncommon for a library to use other libraries. It is all about reuse. Our packages that we install from NPM have dependencies on other packages. We can also do the same with our shared libraries.
We’ll just extend the implementation of the services so that LibOneService
has a dependency on the LibTwoService
which is contained in the lib-two
library. We'll inject the service into the constructor of the LibOneService
and update the SayHello
method to use the LibTwoService
instance. Note that the import
statement is a file path reference to the public_api
of the LibTwoService
.
Create and implement a SayHello
method in the LibTwoService
.
Serve and launch the application to view the usage of multiple services by a single application.
ng serve
As you can see, the development workflow is much different and really efficient. There is no need to publish the libraries and then install/update them for use in an application.
- more efficient development workflow
- ability to share libraries from application consumers
- ability for libraries to consume other libraries in the workspace
Angular 6 Workspace for Publishing Libraries
Now we understand how to use the default workspace environment for sharing libraries with applications. The next scenario we want to explore is how can we use the new Angular workspace to publish libraries to NPM.
I want to test-drive the new Angular project workspace and kick the tires a little. And, also compare it to NRWL.io’s Nx workspace. The following are my expectations:
- Use a @scope for libraries — to help manage and organize the libraries. For example,
@buildmotion/security
where my scope is@buildmotion
- Use the scope name if there are any library candidates for publishing to NPM.
- Consumers (i.e., application components or other modules) of the library should be able to
import
the library using its scope and name:@buildmotion/security
. - Easy configuration of library and application projects.
Angular 6 has a new angular.json
configuration that replaces the old .angular-cli.json
file. This new configuration contains a list of projects
to allow for individual configuration of multiple projects.
In order to be able to use these new features, you should:
- Update your
global
development environment to use Angular 6.
npm install -g @angular/cli@latest
- Create a new
workspace
for development.
ng new buildmotion
If you created the new workspace, you will note that there is a src
folder in the root of the workspace with a default web application. This is the same for previsous version of the Angular CLI.
The configuration of the default web application is contained in the angular.json
projects section. If you use the CLI to create additional projects (library or application), the CLI will create a new folder in the projects
folder for each new project.
I would prefer to have all of my projects, including the default application, in the projects folder. Is the default application in the root’s src
folder different or more special? It is a little confusing to have applications in (2) different locations - I prefer consistency. It feels like that the new environment supports workspace, but the default project setup is still using the previous folder structure for a single web application. And by the way, if you want to add more projects to the workspace
that is supported too.
New Configurations using angular.json
You can review the schema to see all of the available configuration settings allowed. You can view a more detailed description at https://github.com/angular/angular-cli/wiki/angular-workspace.
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
- $schema: references the specified
schema.json
file used by the cli. - version (integer): File format version. This is currently “1”.
- newProjectRoot (string): Path where new projects will be created.
- projects: A list of
project
items. - defaultProject (string): Default project name used in commands.
- cli: Workspace configuration options for Angular CLI.
- schematics (object): Workspace configuration options for Schematics.
- projects: Configuration options for each project in the workspace.
Application Project
When a new project of type application
is created, the projects
section in angular.json
adds configuration for the following items.
- root: Root of the project files.
- sourceRoot: The root of the source files, assets and index.html file structure.
- projectType: An enum to specify the project type:
application
,library
- prefix: The prefix to apply to generated selectors.
- schematics: Project configuration options for Schematics. Has the same format as top level Schematics configuration).
- architect: Project configuration for Architect targets.
- build
- serve
- extract-i18n
- test
- lint
There are (2) project types that are allowed by the angular.json schema. The projectType
and root
properties are required for a projects item. The enum
values are show below.
Library Project
When a new project of type library
is created, the projects
section in angular.json
adds configuration for the following items. Libraries are consumed and not served - therefore, there is no serve
architect configuration.
- root: Root of the project files.
- sourceRoot: The root of the source files, assets and index.html file structure.
- projectType: An enum to specify the project type:
application
,library
- prefix: The prefix to apply to generated selectors.
- architect: Project configuration for Architect targets.
- build
- test
- lint
Create Workspace Items
I will use the CLI to generate additional web applications and new libraries in the workspace. We will use this application to consume our libraries.
Web Application
Create a new application (web) called webOne
. A new project folder will be created for the application.
ng generate application webOne
Build the application using the CLI command:
ng build --project=webOne
Run the application using the CLI command:
ng serve webOne
Library
It is a good practice to scope your libraries that you are responsible for. In this example, I would like to have all of libraries scoped using @buildmotion
. This is a convenient way to organize your libraries. Scopes are a way of grouping related packages together. I'm not sure how practical this is for a development workflow where libraries are shared by applications, but not published or distributed to NPM (for use by other applications not contained in the workspace).
Create a new library project with the @scope name along with the name of the library. This will actually create a folder called buildmotion
in projects
with a new library folder of lib-one
.
Ex: ng generate library @SCOPE-NAME-HERE/LIBRARY-NAME-HERE
ng generate library @buildmotion/libOne
The name of the package is @buildmotion/lib-one
- note, that it also contains the scope name of @buildmotion
. The name
value in the project root package.json
of the library contains the correct value if you want to publish the package to NPM.
{
"name": "@buildmotion/lib-one",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^6.0.0-rc.0 || ^6.0.0",
"@angular/core": "^6.0.0-rc.0 || ^6.0.0"
}
}
You will need to modify the name of the project in the angular.json file to remove the @buildmotion/
scope. A name value of @buildmotion/libOne
contains invalid characters as defined by the schema for the angular.json.
"libOne": {
"root": "projects/buildmotion/lib-one",
"sourceRoot": "projects/buildmotion/lib-one/src",
"projectType": "library",
"prefix": "lib",
...
}
Add a Service to the Library
Use the CLI to create a service in the new library. We want to be able to reference the library and service using the @buildmotion/libOne
.
ng generate service hello --project=libOne
Interestingly, the schematic that generates a new library also creates a default service for each library. Are assuming that all libraries require a service to act as an API for module?
import { Injectable } from '@angular/core';@Injectable({
providedIn: 'root'
})
export class LibOneService { constructor() { }
}
Additionally, the schematic generates a default component for the library as well. I would prefer to add services and components to my libraries as needed for the desired implementation and purpose of the library. Not all libraries will need/use services and components. But it is a good start.
import { Component, OnInit } from '@angular/core';@Component({
selector: 'lib-libOne',
template: `
<p>
lib-one works!
</p>
`,
styles: []
})
export class LibOneComponent implements OnInit { constructor() { } ngOnInit() {
}}
Use the Library in the Application
Now that we have some components and services in our library we are ready to use them in the application. I can inject the service into the constructor. Visual Studio Code will create the import statement for the component. However, notice that we are not using the @buildmotion/lib-one
scope and library name. This is disappointing. The NRWL.io Nx workspace does this by default. It is not recommended to publish libraries with file path references/imports - the dependency will need to be installed from NPM and will not be in the specified file path.
import { LibOneService } from 'dist/@buildmotion/lib-one/public_api';
I would prefer to see what is below. Because the context of this exercise is to publish libraries that can be consumed by other applications. The consumers of your library may not be applications in the same project workspace. It may be a different team or company if the libraries are public. Keep in mind that we are extending the default use of the Angular 6 workspace environment to allow for publishing libraries for general consumption. You may not need to do this.
import { LibOneService } from '@buildmotion/lib-one';
However, we get a path
reference to the LibOneService
. Update the path value to use the scope name value: @buildmotion/lib-one
. We'll provide a work-around to make this happen.
import { Component, OnInit } from '@angular/core';
import { LibOneService } from '@buildmotion/lib-one';@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit { title = 'app'; constructor(
private oneService: LibOneService
) { } ngOnInit(): void {
this.title = this.oneService.SayHello("Angular 6 Workspace");
}
}
Import Reference Fix
Update the workspace’s root tsconfig.json
file to include (2) new items for each of our libraries. Basically, we are telling the compilers where to look for the module
when it imports @buildmotion/lib-one
or @buildmotion/lib-two
.
"paths": {
"@buildmotion/lib-two":["dist/@buildmotion/lib-two"],
"@buildmotion/lib-one":["dist/@buildmotion/lib-one"]
}
Note that the folder is the dist
folder. This means that if want the consuming applications to get the latest changes to any library project, you will need to build the libraries. I created a new script
item in the workspace package.json file to do this.
"package": "ng build --project=libTwo && ng build --project=libOne",
Execute the script to build and output the libraries into the dist
folder - ready for consumption. Although this is an extra step, it is much more efficient than going through the publishing process when they are separate projects. Set the order precendence based on the dependencies of the libraries. In our example, libOne depends on libTwo. Therefore, libTwo is compiled before libOne. Got it?
npm run package
However, if you publish one or more of your custom libraries to NPM and one of them has a dependency on an existing library in the same scope (i.e., @buildmotion), it will need the dependency defined using import statements that use the correct scope and name of the package. This will be the case when you publish package groups using the same scope name.
ng generate library @buildmotion/libTwo
You can now build the new library in the workspace by indicating which library by name using the --project
switch.
ng build --project=libTwo
We’ll modify the SayHello
method in the LibOneService
to use and call a method on the LibTwoService
.
SayHello(message: string): string {
if(message) {
this.libTwoService.SayHello(message);
}
return 'Next time send in a message.';
}
Implement the SayHello
method on the service. This service is consumed by another library/module. This should be supported, right?
import { Injectable } from '@angular/core';@Injectable({
providedIn: 'root'
})
export class LibTwoService {
constructor() { } SayHello(message: any): any {
return `Hello ${message}.`;
}
}
Build the new libaries. You should see the output in the dist
folder under the @buildmotion
folder.
npm run package
Library Peer Dependencies
If you are going to publish your libraries created in the Angular 6 workspace, you will need to update the peerDependencies
in the library's package.json
file. This is what gets published to NPM. When consumers of your library install the package, the peerDependencies
are listed for the developer to manually install.
We should be able to consume the library that consumes another library from the webOne
application.
ng serve --project=webOne
Wrapping Up
The new Angular 6 Workspace is really intended as an enhanced developer workflow to allow multiple applications and libraries to be developed together more efficiently. It is not intended for publishing libraries for general consumption.
- The workspace environment supports sharing multiple libraries by multiple consuming applications.
- It is application-centric in the build process. There is no build or output of libraries by default.
- The workspace environment does not appear to support @scope names and references from consumers of libraries.
- It may not be a good choice for developing libraries that have a primary destination of a package manager repository (think NPM).
- The default
application
is created in the rootsrc
folder when a new workspace is created.
If you want to have the advantage of a workspace environment that provides enhanced configuration of projects and dependencies, along with full-support for publishing libraries; I would suggest using NRWL.io Nx extensions versions 1.0 or 6.0. If you are not publishing your libraries for general consumption, you would do well to use either the Angular or NRWL workspace environments. Both environments provide excellent opportunities to organize your code using modules and this is a good thing!
I publishing a new book on Angular Architecture. Learn how to take advantage of library projects to create better architectures by organizing and reusing your code. Available on Leanpub.com.