Recently I’ve been migrating all of my Angular applications from a custom Webpack configuration to ng-cli. For the most part the transition has been seamless. However, there is one case in particular that has posed a large problem for me due to open issues in the ng-cli project.

Creating reusable, packaged modules for a closed-source application is unnecessarily difficult with ng-cli, at least in its current iteration.

Locally-distributed Feature Modules

Before I discuss the painpoints, let me first elaborate on why I want to support this architecture. For the closed-source application that I am working on at my company, we want to have a re-usable Angular module that we can share across a wide variety of applications. These “core” components and providers should be available to all web applications in an easy-to-distribute, easy-to-update form. To achieve this, I found two different approaches:

  1. Make a private NPM server to host internally. Web applications would reference this new registry directly.
  2. Refer to the project files using relative URLS and npm link.

Now, this second option sounded appealing because it meant I could get the project rolling with an insignificant level of overhead in a way that meshed well with my company’s source control, Perforce.

The main crux of this method of package sharing is mentioned in this Stack Overflow post. Essentially, due to the way that ng-cli compiles projects with the ng serve command, loading a linked library results in the static code analysis failing. This issue is even stranger considering that any builds triggered by changes during Webpack’s watch cycle would compile without error. Only the initial ng serve build would fail. I believe this problem roots back to how ng-cli is parsing dependencies contained within the linked project’s node_modules/ folder, as mentioned in this issue.

Solution

Since the problem revolves around npm link, generate your own packages (.tgz files) with npm pack.

Of course, this is not an ideal solution. Changes made to the shared module will not be immediately reflected in all consuming applications. Instead, you will be forced to recompile and update consumers manually. Additionally, unless you version control the .tgz packages, distributing your application requires an additional build step to provide the initial library files.

The complete steps are as follows:

  1. Build the Angular code for production: ng build --env=prod
  2. Package the library into a .tgz file: npm pack
  3. Use this .tgz package to install dependencies (within package.json): "@company/core": "file:../company-core-1.0.0.tgz"

Note that there are no necessary changes to tsconfig.json or the Angular environment settings.

Reference

Build:

ng build --env=prod
npm pack

index.ts:

export { SharedModule } from './src/app/shared/shared.module';

package.json:

"dependencies": {
	...,
	"@company/core": "file:../company-core-1.0.0.tgz",
	...
}

tsconfig.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "lib": [
      "es2016",
      "dom"
    ]
  }
}