Search code examples
angularsystemjs

angular2 TS import cdn js with system JS


I'm having problems installing properly js that comes from a cdn url, and after looking through 10+ articles or posts, I can't find a good way to do it.

The js file I want to install to be used in my angular project is http://player.twitch.tv/js/embed/v1.js. Currently, I just have a script tag in my index.html which loads it.

In a component file I have code such as this.player = new Twitch.Player("video-player", options);, but when I run npm start, I get an error error TS2304: Cannot find name 'Twitch'.

The weird thing is that if I comment out the lines that use 'Twitch', I can start my server, and then I uncomment out the lines while the server is running and then the app works fine, utilizing the Twitch player library without any problem.

I believe I need to install it in system.config.js file, but I can't find how to do that for js that comes from a cdn (this is not available through npm). I have done this for multiple npm packages, but from cdn I'm a bit lost.

I'm not 100% sure if I need to do this in systemjs, so if that's not correct please disregard the rest of this post and let me know your suggestion - I just need the Twitch in the js file to be recognized and usable throughout my app.

I've found a couple different ways to try this. One is to load the module directly from the url.

// index.html
<!-- 2. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
  System.import('app').catch(function(err){ console.error(err); });
  System.import('http://player.twitch.tv/js/embed/v1.js');
</script>

// main.js
import {bootstrap}    from '@angular/platform-browser-dynamic';
import {AppComponent} from './app.component';
import "angular2-materialize";
import "twitch-api";

With this approach, I'm not sure how to load it into my application. the import "twitch-api" does not work like it does with an npm package such as angular2-materialize, because it's not mapped in the system.config.js file. Not sure if this way is possible.

The other way is to load it in the system.config.js file. With this method, I add a map object with the url to the js file. I think maybe the whole system.config.js file is relevant, so I'll paste it all below.

In the main.ts I do the same thing as above where I add import "twitch-api"; After doing these changes I still get the same error message when trying to start server TS2304: Cannot find name 'Twitch'.

var map = {
    'app':                        'app', // 'dist',
    'rxjs':                       'node_modules/rxjs',
    'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
    '@angular':                   'node_modules/@angular',
    "materialize-css":            "node-modules/materialize-css",
    "materialize":                "node_modules/angular2-materialize",
    "angular2-materialize":       "node_modules/angular2-materialize",
    "angular2-localstorage":      "node_modules/angular2-localstorage",
    "twitch-api":                 "http://player.twitch.tv/js/embed/v1.js"
  };
  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { defaultExtension: 'js' },
    "materialize-css":            { main: "dist/js/materialize" },
    "materialize": { main: "dist/materialize-directive", defaultExtension: "js" },
    "angular2-localstorage": {main: "index.js", defaultExtension: 'js'},
    "twitch-api": {}
  };
  var packageNames = [
    '@angular/common',
    '@angular/compiler',
    '@angular/core',
    '@angular/http',
    '@angular/platform-browser',
    '@angular/platform-browser-dynamic',
    '@angular/router',
    '@angular/router-deprecated',
    '@angular/testing',
    '@angular/upgrade',
    ''
  ];
  // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' }
  packageNames.forEach(function(pkgName) {
    packages[pkgName] = { main: 'index.js', defaultExtension: 'js' };
  });
  var config = {
    map: map,
    packages: packages
  }
  System.config(config);

Solution

  • Final Update So after doing a bit more research and gaining a better understanding of what was going on (to a degree), I wanted to put in an update that might make a little more sense. As to some degree both the original and update had some false information in it.

    One thing from the update that was wrong, is my statement about VS Code treating the 'import' instruction differently. The correct statement is SystemJS and the TypeScript compiler treat the 'import' instruction differently. In the basic case of SystemJS(when it's loading JS files) it doesn't even see these import statements, it does see the product of the import statements (based on how Typescript is configured in the tsconfig.json file).

    So in this question the only "link" between TypeScript and SystemJS in the "import" statement is "twitch-api". TypeScript will transpile the .ts file into a .js file which SystemJS will read when the page is loaded and it will go look in its config settings for a package named "twitch-api". This is why your site would work if you commented out uses of the "Twitch" object to get the site to start, then uncommented the "Twitch" object, SystemJS already loaded that package into your site, so it would just work.

    So on to the TypeScript compiler, when the compiler comes across the import statement it has its own set of rules(look for the 'How TypeScript resolves modules' section). Based on that we see that TypeScript is oblivious of SystemJS, it's only looking in specific locations in your folder structure.

    Since the import is a non-relative type, it will look in your node_modules for a match to "twitch-api", it will also look for files with the extension ['.ts','.tsx','.d.ts']. The interesting bit is this is a URL, there are no files in the folder structure. In normal circumstances, TypeScript is needs this so it can build the JS file correctly. Unfortunately Twitch doesn't offer a .ts/.tsx/.d.ts file, and DefinitelyTyped doesn't offer a .d.ts file for Twitch.

    So what I did to work around this was to create my own .d.ts file for Twitch(not complete and it's not correct to what Twitch actually does). So in my "node_modules" folder I created a folder called "twitch-api" and then created the file "index.d.ts", in that file I put this definition in (to be clear again, this isn't the definition code as I don't know what the method "Twitch.Player("video-player", options);" actually returns but it still works because SystemJS isn't using the definition file.)

    export class Twitch{
        static Player(type:string,options:{}):void;    
    }
    
    export class Player{
        constructor(type:string,options:{});
    }
    

    Once those two things were in place I was able to use a complete import statement like

    import {Twitch,Player} from 'twitch-api'
    

    In your case you will probably have to add more to your definition file for any other methods or properties that you use for the Twitch/Player object.

    Finally, I'm not sure why you were able to work around the issue by just declaring a variable with "Twitch: any" worked. I think it's because "import 'twitch-api'" causes the TypeScript compiler to not do certain checks it would normally do. This would imply that it just needs to know what "Twitch" is and won't look to deep. That's my best guess.


    Update So my first guess was completely wrong, I started playing around with the pluker based on your setup and it became clear that this is something else. I forked the plunker into a new one and if I knew how to properly instantiate the Twitch.Player it would work in the example.

    This should have been obvious when it would work after performing a change while it was running. The error you are seeing is in the Typescript compiler. I don't know what IDE you are using but I am using VS code and I run into these because the "import" statement works differently than it would in Plunker.


    I think you're almost there but you need a kind of combination of both. The reason the first example failed is because "twitch-api" isn't a recognized package from the import command.

    I think the reason your second attempt fails is how you implement the map and package objects. Here is a plunker that shows how to implement your config using an external URL. Here is an excerpt of the config.js file. I'm not sure on all the rules for when you need to use the "main" property, I think you have to use it if the file is not named "index" or it doesn't follow the name of the package?

    var  map = {
    'app':                        'src', // 'dist',
    'rxjs':                       'https://npmcdn.com/[email protected]',
    'ng2-toastr':                 'https://npmcdn.com/[email protected]'
    };
    
    var packages = {
    'app':                        { main: 'app.ts',  defaultExtension: 'ts' },
    'rxjs':                       { defaultExtension: 'js' },
    'ng2-toastr':                 { main: 'bundles/ng2-toastr.js', defaultExtension: 'js' }
    };