Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to import a node module inside an angular web worker?

I try to import a node module inside an Angular 8 web worker, but get an compile error 'Cannot find module'. Anyone know how to solve this?

I created a new worker inside my electron project with ng generate web-worker app, like described in the above mentioned ng documentation.

All works fine until i add some import like path or fs-extra e.g.:

/// <reference lib="webworker" />    
import * as path from 'path';

addEventListener('message', ({ data }) => {    
  console.log(path.resolve('/'))
  const response = `worker response to ${data}`;
  postMessage(response);
});

This import works fine in any other ts component but inside the web worker i get a compile error with this message e.g.

Error: app/app.worker.ts:3:23 - error TS2307: Cannot find module 'path'.

How can i fix this? Maybe i need some additional parameter in the generated tsconfig.worker.json?

To reproduce the error, run:

$ git clone https://github.com/hoefling/stackoverflow-57774039
$ cd stackoverflow-57774039
$ yarn build

Or check out the project's build log on Travis.

Note:

1) I only found this as a similar problem, but the answer handles only custom modules.

2) I tested the same import with a minimal electron seed which uses web workers and it worked, but this example uses plain java script without angular.

like image 918
zerocewl Avatar asked Sep 03 '19 14:09

zerocewl


People also ask

How do I import a module into a service worker?

ES modules can be imported in one of two ways: either statically, using the import ... from '...' syntax, or dynamically, using the import() method. Inside of a service worker, only the static syntax is currently supported. This limitation is analogous to a similar restriction placed on importScripts() usage.

What is the use of web worker in angular?

Web workers lets you run CPU-intensive computations in a background thread, freeing the main thread to update the user interface.

What is web worker node JS?

The node:worker_threads module enables the use of threads that execute JavaScript in parallel. To access it: const worker = require('node:worker_threads'); Workers (threads) are useful for performing CPU-intensive JavaScript operations. They do not help much with I/O-intensive work.


Video Answer


1 Answers

1. TypeScript error

As you've noticed the first error is a TypeScript error. Looking at the tsconfig.worker.json I've found that it sets types to an empty array:

{
  "compilerOptions": {
    "types": [],
    // ...
  }
  // ...
}

Specifying types turns off the automatic inclusion of @types packages. Which is a problem in this case because path has its type definitions in @types/node.

So let's fix that by explicitly adding node to the types array:

{
  "compilerOptions": {
    "types": [
        "node"
    ],
    // ...
  }
  // ...
}

This fixes the TypeScript error, however trying to build again we're greeted with a very similar error. This time from Webpack directly.

2. Webpack error

ERROR in ./src/app/app.worker.ts (./node_modules/worker-plugin/dist/loader.js!./src/app/app.worker.ts)
Module build failed (from ./node_modules/worker-plugin/dist/loader.js):
ModuleNotFoundError: Module not found: Error: Can't resolve 'path' in './src/app'

To figure this one out we need to dig quite a lot deeper...

Why it works everywhere else

First it's important to understand why importing path works in all the other modules. Webpack has the concept of targets (web, node, etc). Webpack uses this target to decide which default options and plugins to use.

Ordinarily the target of a Angular application using @angular-devkit/build-angular:browser would be web. However in your case, the postinstall:electron script actually patches node_modules to change that:

postinstall.js (parts omitted for brevity)

const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';

fs.readFile(f_angular, 'utf8', function (err, data) {
  var result = data.replace(/target: "electron-renderer",/g, '');
  var result = result.replace(/target: "web",/g, '');
  var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');

  fs.writeFile(f_angular, result, 'utf8');
});

The target electron-renderer is treated by Webpack similarily to node. Especially interesting for us: It adds the NodeTargetPlugin by default.

What does that plugin do, you wonder? It adds all known built in Node.js modules as externals. When building the application, Webpack will not attempt to bundle externals. Instead they are resolved using require at runtime. This is what makes importing path work, even though it's not installed as a module known to Webpack.

Why it doesn't work for the worker

The worker is compiled separately using the WorkerPlugin. In their documentation they state:

By default, WorkerPlugin doesn't run any of your configured Webpack plugins when bundling worker code - this avoids running things like html-webpack-plugin twice. For cases where it's necessary to apply a plugin to Worker code, use the plugins option.

Looking at the usage of WorkerPlugin deep within @angular-devkit we see the following:

@angular-devkit/src/angular-cli-files/models/webpack-configs/worker.js (simplified)

new WorkerPlugin({
    globalObject: false,
    plugins: [
        getTypescriptWorkerPlugin(wco, workerTsConfigPath)
    ],
})

As we can see it uses the plugins option, but only for a single plugin which is responsible for the TypeScript compilation. This way the default plugins, configured by Webpack, including NodeTargetPlugin get lost and are not used for the worker.

Solution

To fix this we have to modify the Webpack config. And to do that we'll use @angular-builders/custom-webpack. Go ahead and install that package.

Next, open angular.json and update projects > angular-electron > architect > build:

"build": {
  "builder": "@angular-builders/custom-webpack:browser",
  "options": {
    "customWebpackConfig": {
      "path": "./extra-webpack.config.js"
    }
    // existing options
  }
}

Repeat the same for serve.

Now, create extra-webpack.config.js in the same directory as angular.json:

const WorkerPlugin = require('worker-plugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');

module.exports = (config, options) => {
  let workerPlugin = config.plugins.find(p => p instanceof WorkerPlugin);
  if (workerPlugin) {
    workerPlugin.options.plugins.push(new NodeTargetPlugin());
  }
  return config;
};

The file exports a function which will be called by @angular-builders/custom-webpack with the existing Webpack config object. We can then search all plugins for an instance of the WorkerPlugin and patch its options adding the NodeTargetPlugin.

like image 138
lukasgeiter Avatar answered Nov 14 '22 21:11

lukasgeiter