17

I want to use libquassel (https://github.com/magne4000/node-libquassel) in my Angular 2 project. The library is browserified, so in theory it should work, but I'm not really sure how to import it in my project.

I tried adding to my typings.d.ts

declare module 'libquassel';

and then importing the library with

import * as Quassel from 'libquassel';

but I get

EXCEPTION: net.Socket is not a function

when I try to run my code, which I believe is another library that browserify embedded in the client/libquassel.js file.

How can I use this library?

Edit: I'll answer all questions here:

  • My setup is a plain angular-cli project. No fancy stuff, just ng new proj1 and then npm install libquassel --save.
  • My index.html doesn't have anything else that ng new hasn't placed in there.
  • I tried importing the library with import * as Quassel from 'libquassel' and var Quassel = require('quassel') (and permutations of those), without any luck (errors varying from unknown function 'require' to can't find module lib|quassel).
  • Steps to repro my project:

    ng new test
    cd test
    npm install libquassel --save
    ng g s quassel
    

Then add QuasselService to the providers array in app.module.ts. This would be a good demo of my problem, which is how to import libquassel in my QuasselService.

alexandernst
  • 14,352
  • 22
  • 97
  • 197

4 Answers4

5

Update (fix it, this time for real)

There is a very good reason why it doesn't work in your code: because I lied to you. So here is my real cheat in case you didn't "diffed" it yourself yet.

I still need to require libquassel 2 times but the first time is done not by the call to require which is useless but by adding it to angular-cli.json to the scripts section:

  "scripts": [
    "../node_modules/libquassel/client/libquassel.js"
  ],

This is important because "client/libquassel.js" declares its own require but do not explicitly exports it so if you do

require('libquassel/client/libquassel.js');

libquassel's custom require is left stuck inside anonymous namespace and you can't access it. Adding it to the scripts section on the other hand, lets libquassel.js to pollute the global namespace (i.e. window) with its require and thus make it work.

So the actual steps to make it work are:

  1. Add client/libquassel.js to the scripts section of angular-cli.json
  2. Use additional require to actually load Quassel module
let Quassel = window['require']('quassel'); // note that usingg require('quassel') leads to compilation error
let quasselObj = new Quassel(...);

Here is my attempt. It looks like you need to require "quassel" 2 times: first time to load the JS from "../node_modules/libquassel/client/libquassel.js" and second time to let the overridden require from that JS actually resolve "quassel". Here is a trick that seems to work for me in 'main.ts':

require('../node_modules/libquassel/client/libquassel.js');
let Quassel = window['require']('quassel'); // note that using require('quassel') leads to compilation error
let quasselObj = new Quassel(...);

I needed one more trick to not fail with a compilation error: particularly I had to use window['require'] instead of just require for the second call as it is a call for the inner require as defined inside client/libquassel.js and Node/Angular-cli alone can't handle it.

Note that after that setup my application still failed with a runtime error in some AJAX because browsified libquassel.js seem to require a proxy set up on the server-side at URL like /api/vm/net/connect and this is probably what server-side of the net-browsify should do but I didn't try to set it up.

Answer to comments

It looks like you use old version of Angular-CLI and if you upgrade everything should work fine.

Here is what ng --version tells me on my original machine

angular-cli: 1.0.0-beta.28.3
node: 6.10.0
os: win32 x64
@angular/common: 2.4.10
@angular/compiler: 2.4.10
@angular/core: 2.4.10
@angular/forms: 2.4.10
@angular/http: 2.4.10
@angular/platform-browser: 2.4.10
@angular/platform-browser-dynamic: 2.4.10
@angular/router: 3.4.10
@angular/compiler-cli: 2.4.10

When I tried to copy my original project to a different machine I also got a compilation error about require but when I updated Angular-CLI to

@angular/cli: 1.0.0
node: 6.10.0
os: darwin x64
@angular/common: 2.4.10
@angular/compiler: 2.4.10
@angular/core: 2.4.10
@angular/forms: 2.4.10
@angular/http: 2.4.10
@angular/platform-browser: 2.4.10
@angular/platform-browser-dynamic: 2.4.10
@angular/router: 3.4.10
@angular/cli: 1.0.0
@angular/compiler-cli: 2.4.10

it compiled and worked the same. What I did is just followed the instruction

The package "angular-cli" has been deprecated and renamed to "@angular/cli".

Please take the following steps to avoid issues:
"npm uninstall --save-dev angular-cli"
"npm install --save-dev @angular/cli@latest"

and then updated angular-cli.json following the instructions by ng serve

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • This looks good! Don't worry about the AJAX error you're getting, that happens because you need a specially crafted expressjs server listening. – alexandernst Mar 21 '17 at 09:35
  • I'm finally at home and I'm testing your code, but I get this: `src/app/quassel-client.service.ts (6,1): Cannot find name 'require'`. – alexandernst Mar 23 '17 at 23:24
  • @alexandernst, I'm not at my code right now but where do you get it? Is it a compilation error for the line `require('../node_modules/libquassel/client/libquassel.js');` or is it a runtime error for `let Quassel = window['require']('quassel')`? – SergGr Mar 23 '17 at 23:27
  • It's a compilation code at the require line. So, basically, the compiler doesn't know what "require" is. – alexandernst Mar 23 '17 at 23:39
  • @alexandernst, it looks like the difference is that you use some old Angular-CLI and upgrade should fix it. See my update in the main answer. – SergGr Mar 24 '17 at 14:00
  • I updated my angular-cli (it now shows cli 1.0.0, node 7.7.2 and @angular/xyz 2.4.9), but I keep getting the error. – alexandernst Mar 25 '17 at 14:53
  • I also tried http://stackoverflow.com/a/35961176/940158 but I keep getting the same error. – alexandernst Mar 25 '17 at 15:05
  • @alexandernst, to test is that a difference between projects or between environments, could you try my project? I uploaded zip-ed folder to http://dropmefiles.com/X3Sbo (if you update angular, you might need to update angular-cli.json but it is trivial: just change 3 lines as instruction by `ng server` suggests) – SergGr Mar 25 '17 at 15:57
  • It works with your project, so there is definitely something wrong with mine. I'll debug it by diffing yours. Since this is the cleanest solution (imho), I'm accepting it. Thank you for the help and kudos for the great answer! – alexandernst Mar 25 '17 at 16:15
  • 1
    @alexandernst, There is a very good reason why it doesn't work in your code: because I lied to you. I think I found what really makes my project work. See update in the question and let me know if this still doesn't work for you for some reason. – SergGr Mar 25 '17 at 18:01
1

Works like a charm :

declare var require;
const Quassel = require( 'libquassel/lib/libquassel.js' );
console.log( 'Quassel', Quassel );

Looking at the libquassel's folder in node_module, realised that there is no index.js in the root directory.

When there is not index.js , you can't do this :

 require( 'libquassel' );

Simply because node doesn't know what to pull in for you, so you'd need to specify the exact path, which is not that bad in this case.

Also , note that it's better to move declare var require; to your typings file located under the src folder, because you might need to declare it again , so it's better be there.

EDIT : Here is what I found after trying to instantiate the Quassel like bellow :

const Quassel = require( 'libquassel/lib/libquassel.js' );
console.log( 'Quassel', Quassel );
var quassel = new Quassel( "quassel.domain.tld",
    4242,
    { backloglimit : 10 },
    function ( next ) {
        next( "user", "password" );
    } );
console.log( 'quassel', quassel );

And here is my console log :

enter image description here But having said that, I realised that there is a problem inside the libquassel.js , as bellow :

in line 10, they're doing this :

   var net = require('net');

And looking their package.json, there is no such a thing as net and there is net-browserify-alt;

So if they change that import to :

 var net = require('net-browserify-alt'),

Everything will work.

Having said that, obviously you don't want to edit your node_module, but I'm really surprised of how this really works even outside of angular and webpack , because clearly they've mentioned a wrong node_module which if you google , there is only one package named net which I had a look and it's empty and dummy !!!!

** ********** UPDATE : ********** **

What exactly needs to be done :

1- run ng eject

that will generate a webpack.conf.js inside your root directory.

2- inside that find resolve property and add :

 "alias":{
     'net':'net-browserify-alt'
  },

so your resolve will probably look like :

"resolve": {
    "extensions": [
      ".ts",
      ".js"
    ],
      "alias":{
         'net':'net-browserify-alt'
      },
    "modules": [
      "./node_modules"
    ]
  },

3- import and use it :

declare var require;
const Quassel = require( 'libquassel/lib/libquassel.js' );
console.log( 'Quassel', Quassel );
var quassel = new Quassel( "quassel.domain.tld",
    4242,
    { backloglimit : 10 },
    function ( next ) {
        next( "user", "password" );
    } );
console.log( 'quassel', quassel );

NOTE :

Having a look at webpack configuration, seems like webpack likes to override couple of modules :

"node": {
    "fs": "empty",
    "global": true,
    "crypto": "empty",
    "tls": "empty",
    "net": "empty",
    "process": true,
    "module": false,
    "clearImmediate": false,
    "setImmediate": false
  }

I don't exactly know why, but this list is in the webpack config and seems to be making net to be undefiend ( empty ) and that's why we had to create an alias.

Milad
  • 27,506
  • 11
  • 76
  • 85
  • By requiring the node version (`lib/libquassel.js`) instead of the browserified version (`client/libquassel.js`) I'm not really sure typescript´s compiler/webpack/whatever will be able to properly replace `net` with `net-browserify`. That said, did you actually try to instantiate `Quassel` after doing the `require(".....client/libquassel.js")`? I mean, are you able to actually run `var foo = new Quassel();`? – alexandernst Mar 21 '17 at 09:39
  • 3
    `net` is an internal node.js module ( https://nodejs.org/api/net.html ) – Alexus Mar 21 '17 at 10:42
  • @alexandernst , try my update , it works fully in my local – Milad Mar 21 '17 at 11:02
  • Now that looks interesting! Please give me a couple of days to test it (I`m not at home right now). – alexandernst Mar 21 '17 at 15:03
  • There are more than mappings required than just `net-browserify-alt`, so make sure you look in `Gruntfile.js` for the full list – K Scandrett Mar 22 '17 at 09:52
  • @KScandrett , not sure about should we map all of them or not, the problem is that webpack exclude couple of specific node modules and net was one of them – Milad Mar 22 '17 at 09:54
  • 1
    Some of the standard Node modules won't run in a web browser, only in Node. That's why there is the `net` to `net-browserify-alt` mapping - so that it will run within a browser. The OP will need to include all of these mappings – K Scandrett Mar 22 '17 at 10:01
  • If you look at the browserefied https://github.com/magne4000/node-libquassel/blob/master/client/libquassel.js you'll notice that there are more than 100 dependencies inside (search for `124:`). I don't think that mapping them all by hand is a good idea. Actually this is probably why they created a separate browserefied JS in the first place – SergGr Mar 23 '17 at 14:49
  • But do we need to map those? Seems to be working without mapping them though. – Milad Mar 23 '17 at 20:30
  • 1
    @Milad, what do you mean by "working". Are you able to do `quassel.connect` successfully? Or just create a new object (which is by far not enough)? Do you get a real connection? Or at least do you get AJAX requests to your server? From what I see in the code, it looks like you need at least significant part of those dependencies to make it actually work. – SergGr Mar 23 '17 at 23:15
  • 1
    @SergGr has a point here, mapping is probably a bad idea, since that is what Browserify is doing, and it's probably doing it better than us. – alexandernst Mar 23 '17 at 23:18
  • @SergGr , I've never used this library and to be honest I don't exactly know what it does, the question was about not being to import and initialise and that's what I've given an answer for. But I'm more than happy to spend some time to end to end test. Just give a scenario and a working example and I'll try to make it work in here – Milad Mar 23 '17 at 23:19
  • @Milad, you may notice that the example at the GitHub ends up with `quassel.connect();` and it is actually the key method of the API where many dependencies are actually used. I don't have any working example as working example requires also to setup a local server to listen to AJAX request from the browser to "proxy" them as actual Socket connections to IRC. Maybe @alexandernst has some example but still I doubt your code can even get to the part where first AJAX is sent not to mention parsing back "proxied" response – SergGr Mar 23 '17 at 23:24
  • @Milad SergGr has a point here. Even if the map works for the net vs net-browserify module, the entire library is using tons and tons of libraries (mostly socket and net related stuff). I'm sure I'd get up to the point where I won't be able to use the library only by re-mapping modules. (basically, what I'm saying is that I don't think Browserify is just re-mapping modules, which is what we're trying to do here) – alexandernst Mar 23 '17 at 23:27
0

Angular-cli uses webpack

you will need to add the JS file to apps[0].scripts in your angular-cli.json file, which WebPack then bundles as if it were loaded with a tag.

apps:[
{
  ...
  "scripts": [
    "../node_modules/libquassel/client/libquassel.js"
  ],

If you do that, you can get at it by adding declare var Quassel :any; in your src/typings.d.ts or component.

let quassel = new Quassel("localhost", 4242, {}, function(next) {
  next("user", "password");
});
quassel.connect();
fingerpich
  • 8,500
  • 2
  • 22
  • 32
  • 1
    I get `EXCEPTION: Quassel is not defined`. And let me explain (for the 5th time in this post) that this is not a normal JS library that exports stuff, but a Browserify library that is supposed to be `require`d, and that is the reason why your solution doesn't work (and actually that is the reason why I assigned 500pts bounty to this question, as it's very tricky). If I load this library in a script tag, the one and only way I can access it is by doing `var Quassel = require("quassel")`. – alexandernst Mar 19 '17 at 20:21
0

The key issue is the browserified libquassel library contains within it a global require function. That conflicts with standard require function.

There are many ways of handling this but one solution would be to copy /libquassel/client/libquassel.js (and the minified version) to your /src/app/ directory.

Then run a search and replace in these files to change ALL occurrences of require to _quasselRequire_

You would do this for both libquassel.js and libquassel.min.js.

To utilise this you would do something like this in quassel.service.ts:

import { Injectable } from '@angular/core';
import * as dummyQuassel1 from './libquassel.js'

var dummyQuassel2 = dummyQuassel1; // something just to force the import

declare var _quasselRequire_ :any;

@Injectable()
export class QuasselService {

  constructor() { 

    // set up in the constructor as a demo

    let Quassel = _quasselRequire_( 'quassel' );
    console.log('Quassel', Quassel);

    var quassel = new Quassel( 
        "quassel.domain.tld",
        4242,
        { backloglimit : 10 },
        function ( next ) {
            next( "user", "password" );
        } 
    );

    console.log('quassel', quassel);

    quassel.on('network.init', function(networkId) {
        console.log("before net");
        var network = quassel.getNetworks().get(networkId);
        console.log("after net");
        // ...
    });

    console.log("before connect");
    quassel.connect();  
    console.log("after connect");
  }

  getQuassel() {
      return 'My Quassel service';
  }
}

This by itself results in the error:

POST http://localhost:4200/api/vm/net/connect 404 (Not Found)

and

Error: Uncaught, unspecified 'error' event. at new Error (native) at Quassel.n.emit (http://localhost:4200/main.bundle.js:570:4210) [angular] at Socket. (http://localhost:4200/main.bundle.js:810:19931) [angular] at Socket.EventEmitter.emit (http://localhost:4200/main.bundle.js:573:1079) [angular] ...

But as I understand it this is fairly normal given I haven't Express etc. set up.

Note: the name _quasselRequire_ can be anything that's appropriate to you.

K Scandrett
  • 16,390
  • 4
  • 40
  • 65
  • As you probably are aware you'll also need to add the property `"allowJs": true` to `compilerOptions` in `tsconfig.app.json` – K Scandrett Mar 22 '17 at 18:22
  • If you're gonna do this, you'd probably need to write a script that does this because most of the time you install your dependencies, you don't touch them. This is also bad from the upgrade point of view, if you even want to upgrade the package, you're screwed – Milad Mar 23 '17 at 08:59
  • Why would you be screwed? – K Scandrett Mar 23 '17 at 09:27
  • @Milad has a valid point here, replacing stuff inside `node_module` is probably not a good idea. – alexandernst Mar 23 '17 at 23:19
  • @alexandernst I agree, but nowhere do I suggest replacing anything inside `node_module`. My suggestion above is to copy the final browserified script (libquassel/client/libquassel.[min].js) to your project's `/src/app/` first, and edit it there. Of course that could all be automated fairly easily – K Scandrett Mar 24 '17 at 00:13
  • 1
    One could use https://www.npmjs.com/package/string-replace-loader for instance, so Webpack grabs the latest version of the browserified library from `node_modules`, searches and replaces `require` with `_quasselRequire_` as it bundles – K Scandrett Mar 24 '17 at 00:25
  • I think the only change needed above would then be to use something like `import * as dummyQuassel1 from 'libquassel/client/libquassel.js'`, so it doesn't even get copied to your `/src/app` dir, just pulled in, a "find and replace", all during the Webpack's build – K Scandrett Mar 24 '17 at 00:32
  • If you've tried my manual steps in the answer and find it works correctly (like the others I don't have the full setup to test the IRC proxy) let me know and I'll happily update my answer with the automated steps addressing concerns in the comments. – K Scandrett Mar 24 '17 at 02:46