In this article, I will walk you through how to fix an error that many people are encountering after the release of Angular CLI v6.

The Problem

After upgrading one of my Angular applications, from version 5 to version 6, I tried to build it (using ng build), but had the following error:

ERROR in ./node_modules/contentful-sdk-core/dist/es-modules/get-user-agent.js Module not found: Error: Can't resolve 'os' in 'path/to/project/node_modules/contentful-sdk-core/dist/es-modules'

For more details, see this issue I created in the Contentful SDK repository.

I was not alone, many people ran into similar issues.

The Cause

When I checked the source code of get-user-agent.js I found this:

import { platform, release } from 'os';

The os module is a built-in Node.js module that provides utilities to work with the operating system. I was building for the web where nothing from the os module could run. The library I installed was using–like many libraries–was built for both the web, and Node. But it didn't provide a specific version for the web.

But It Used to Work Before Version 5 of the CLI

It used to work before Angular CLI 6 because the Angular CLI team provided support for it:

In Angular CLI we never provided a browser version of node built-ins. But we did:

  • provide a shim for global and process,
  • supply an empty module when fs, crypto, tls and net were requested.

The Angular CLI team believes that libraries that are meant to run inside the browser are not supposed to use Node API–and I totally agree with them. Here is a quote from Filipe Silva.

We understand that this isn't great if your code relies, directly or indirectly, on a library that makes incorrect assumptions about browser environments. The best I can say is that you should bring this problem to their attention via an issue on their tracker. Maybe newer versions of that library don't have this behaviour anymore.
But although it is inconvenient to address these problems, I hope we can agree that the current behaviour is incorrect. Browser code should not rely on things that are not available in browser environments.

Therefore, they removed support for this kind of behavior via this Pull Request which introduced a breaking change. That's why many applications broke after upgrading to version 6.

The Solution

If we had access to the internal Webpack configuration of the CLI, we could use Webpack's node configuration options to tell it to replace those modules with empty objects. But the ng eject command has been removed since Angular CLI 6...

The easy workaround I found was to use TypeScript path mapping:

  1. Create an empty file src/empty.ts.
  2. Add the "paths" property to the tsconfig.json file.
tsconfig.json

This tells the TypeScript compiler that imports from os should be looked in the file src/empty.ts.

This will fix the issue for our application. But we are not done yet. There is one more step to make it work for our unit tests too: we have to add empty.ts to the files array of tsconfig.spec.json.

tsconfig.spec.json

In Node.js, global variables are attached to a special object called global–which is the equivalent to the window object in the Browser. When we call setTimeout() in Node.js, we are in fact calling global.setTimeout().

Some libraries rely on this global object which doesn't exist in the Browsers leading to the following error:

Uncaught ReferenceError: global is not defined

Because the window object of the Browser and the global object of Node.js are pretty much the same, we can trick the browser to fake the global object with the window object by adding the following code to your polyfills.ts file:

(window as any).global = window;

I hope that you enjoyed this article and that it has helped find a solution to your issue. If so, please let me know in the comment section below.


References

If you want more context about this issue, check out the following links:


If you enjoyed this article, follow @ahasall on Twitter for more content like this.