Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Rick Stumbo
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Rick Stumbo

Building JavaScript Demos With System.js, TypeScript 2.2.1, And Angular 2.4.9

By on

For about a year, I've been writing my Angular 2 demos using System.js and TypeScript. But, more specifically, I've been using a TypeScript loader that allows me to perform both transpiling and type-checking in the browser. This means that I can build my demos with vanilla TypeScript files and System.js does everything else, on the fly. The other day, however, I went to upgrade my version of TypeScript and I discovered that the TypeScript plugin is dropping support for in-browser type-checking. Since type-checking is one of the main reasons I use TypeScript, I decided it was time to learn how to compile TypeScript off-line.

Run this demo in my JavaScript Demos project on GitHub.

The main complication with my JavaScript Demos Project is that the demos share vendor files. After all, I don't want to have to npm-install for every individual demo - that would be crazy pants. As such, the vendor files exist in a sibling directory structure to the actual demos:

  • /project/demos/...
  • /project/demos/some-demo/index.htm
  • /project/demos/...
  • /project/vendor/...
  • /project/vendor/angular2/2.4.9-tsc/node_modules/
  • /project/vendor/...

For Angular 2.4.9 with TypeScript 2.2.1, here's the vendor package.json file that I came up with for the next series of demos:

{
	"name": "angular2-v2-4-9-tsc",
	"version": "2.4.9-tsc",
	"license": "ISC",
	"dependencies": {
		"@angular/common": "2.4.9",
		"@angular/compiler": "2.4.9",
		"@angular/core": "2.4.9",
		"@angular/forms": "2.4.9",
		"@angular/http": "2.4.9",
		"@angular/platform-browser": "2.4.9",
		"@angular/platform-browser-dynamic": "2.4.9",
		"@angular/router": "3.4.9",
		"@types/core-js": "0.9.35",
		"@types/node": "7.0.5",
		"core-js": "2.4.1",
		"reflect-metadata": "0.1.10",
		"rxjs": "5.2.0",
		"systemjs": "0.20.10",
		"typescript": "2.2.1",
		"web-animations-js": "2.2.2",
		"zone.js": "0.8.4"
	}
}

Notice that I am installing TypeScript as an npm dependency. This way, if someone were to download this demo and run it locally, they wouldn't need to also install TypeScript - they could just run tsc (the TypeScript compiler) out of the vendor directory. Of course, doing that is kind of a pain, so within the actual demo directory, I created another local package.json file that does nothing be define a few "run scripts".

npm "run scripts" provide, among other things, a way to invoke binary executables that are installed as "local" dependencies in the node_modules directory. npm does this by adding "node_modules/.bin" to the PATH on which it will search for binaries. Of course, since my demos all share vendor files, the directory "node_modules/.bin" doesn't exist - at least not locally. As such, I have to put the actual .bin path in the npm run scripts:

{
	"scripts": {
		"build": "../../vendor/angular2/2.4.9-tsc/node_modules/.bin/tsc --project tsconfig.json",
		"version": "../../vendor/angular2/2.4.9-tsc/node_modules/.bin/tsc --version",
		"watch": "../../vendor/angular2/2.4.9-tsc/node_modules/.bin/tsc --watch --project tsconfig.json"
	}
}

The file is a little noisy. But, with this local package.json file in place, I can now run:

  • npm run build - Compiles the .ts files into .js files.
  • npm run watch - Compiles the .ts files and then watches them for changes.
  • npm run version - Outputs the version of the tsc compiler (for debugging purposes).

While the tsc binary will automatically look for a tsconfig.json file in the same directory, I am explicitly providing the path using the --project runtime argument. The tsconfig.json file tells the TypeScript compiler how to process the local demo files:

{
	"compilerOptions": {
		"baseUrl": "../../vendor/angular2/2.4.9-tsc/node_modules/",
		"emitDecoratorMetadata": true,
		"experimentalDecorators": true,
		"lib": [
			"DOM",
			"ES6"
		],
		"module": "commonjs",
		"moduleResolution": "node",
		"noImplicitAny": true,
		"pretty": true,
		"removeComments": false,
		"sourceMap": true,
		"suppressImplicitAnyIndexErrors": true,
		"target": "es5",
		"typeRoots": [
			"../../vendor/angular2/2.4.9-tsc/node_modules/@types/"
		],
		"types": [
			"node"
		]
	},
	"include": [
		"./app/main.ts"
	]
}

As with the npm run scripts, I have to tell the TypeScript compiler about the shared vendor folder. In this case, I'm using the "baseUrl" to tell TypeScript how to resolve non-relative imports. This way, it will look locally for modules like "./app.component" and, for non-relative imports like "@angular/core", it will look in the shared vendor node_modules folder.

For now, I'm not providing any "outDir" to define the compilation target. Instead, I'm letting the TypeScript compiler save the generated .js files right alongside the .ts files. This way, all of the relative paths for CSS and HTML files work without further intervention:

Using tsc TypeScript compiler to compile .ts. files to .js files.

With the TypeScript compilation and type-checking happening offline, my System.js configuration file becomes much more straightforward - no more messing around "typings" or "typescriptOptions" - all I have to do is tell System.js where to locate my application modules:

(function( global ) {

	System.config({
		warnings: true,
		map: {
			"@angular/": "../../vendor/angular2/2.4.9-tsc/node_modules/@angular/",
			"rxjs/": "../../vendor/angular2/2.4.9-tsc/node_modules/rxjs/"
		},
		packages: {
			"@angular/common": {
				main: "bundles/common.umd.js"
			},
			"@angular/compiler": {
				main: "bundles/compiler.umd.js"
			},
			"@angular/core": {
				main: "bundles/core.umd.js"
			},
			"@angular/forms": {
				main: "bundles/forms.umd.js"
			},
			"@angular/http": {
				main: "bundles/http.umd.js"
			},
			"@angular/platform-browser": {
				main: "bundles/platform-browser.umd.js"
			},
			"@angular/platform-browser-dynamic": {
				main: "bundles/platform-browser-dynamic.umd.js"
			},
			"@angular/router": {
				main: "bundles/router.umd.js"
			},
			"app": {
				main: "main",
				defaultExtension: "js"
			},
			"rxjs": {
				defaultExtension: "js"
			}
		}
	});

	global.bootstrapping = System
		.import( "app" )
		.then(
			function handleResolve() {

				console.info( "System.js successfully bootstrapped app." );

			},
			function handleReject( error ) {

				console.warn( "System.js could not bootstrap the app." );
				console.error( error );

				return( Promise.reject( error ) );

			}
		)
	;

})( window );

The really nice benefit of doing the TypeScript compiling offline is that I no longer have to load all of the .d.ts (type definition) files in the browser. That required - literally - hundreds of AJAX calls. Now, by doing all the TypeScript compiling offline and only loading JavaScript files in the browser, my demo only needs to make a handful of AJAX requests. This leads to a much faster time-to-interaction for the demo itself.

To ensure that this is all working, I put together a simple demo that utilizes the two sets of non-local modules - @angular (d'uh) and RxJS:

// Import the core angular services.
import { Component } from "@angular/core";
import { Observable } from "rxjs/Observable";

// Import these modules to create side-effects.
import "rxjs/add/observable/of";

@Component({
	moduleId: module.id,
	selector: "my-app",
	styleUrls: [ "./app.component.css" ],
	template:
	`
		<p>
			Best <strong>Holly Hunter</strong> movies:
		</p>

		<ul>
			<li *ngFor="let movie of movies | async">
				{{ movie }}
			</li>
		</ul>

		<p>
			<em>This demo was built with the TSC compiler, v2.2.1.</em>
		</p>
	`
})
export class AppComponent {

	public movies: Observable<string[]>;


	// I initialize the app component.
	constructor() {

		this.movies = Observable.of([
			"O Brother, Where Art Thou?",
			"Home for the Holidays",
			"The Firm",
			"Broadcast News",
			"Raising Arizona"
		]);

	}

}

And, when I compile this and load it with System.js, I get the following browser output:

tsc and System.js demo in Angular 2.

Now, if I were to go into the demo and change:

public movies: Observable<string[]>;

... to:

public movies: Observable<string>;

... the TypeScript compiler, watching the files for changes, would complain when it went to compile the update:

tsc complains about type-checking error.

As you can see, I still get all of the wonderful type-checking - just not in the browser.

For a long time, I put off trying to learn the TypeScript compiler since I thought it would make it harder for people to download and run my JavaScript demos. But, by using npm run scripts, I can still get all of the ease-of-use and the type-checking. And, the demo actually loads much faster.

Want to use code from this post? Check out the license.

Reader Comments

15,674 Comments

@All,

As I was investigating this approach, I came across an interesting change in the Angular 2 change-log - the Angular team is no longer using the moduleId meta-data to locate module-relative template and style URLs:

www.bennadel.com/blog/3241-relative-template-and-style-urls-using-system-js-without-moduleid-in-angular-2-4-9.htm

... they do this by translating the module-relative paths into app-relative paths on-the-fly as the application is being loaded by System.js.

15,674 Comments

@All,

I'm actually having trouble getting lodash's @types library to work with this approach. Trying to figure out what is going wrong.

15,674 Comments

@All,

It looks like I had some misunderstanding about "typeRoots" and "types" -- they only apply to global / ambient type declarations. If I needed to include lodash in this example, I would have to include a "paths" definition in the tsconfig.json, which would be *relative to the baseUrl*. Example:

"baseUrl": "../../vendor/angular2/2.4.9-tsc/node_modules/",
"paths": {
. . . . "lodash": [
. . . . . . . . "@types/lodash"
. . . . ]
},

The odd thing about this is that if I run the build-step with --traceResolution, both approaches appear to find the "@types/lodash" folder with the type-declaration file; but, it only avoids the error if I have the "paths" set. Perhaps that is because it uses the declaration file in a different way? I am not really sure - I'm still learning all this TypeScript stuff :D

15,674 Comments

To follow-up on that last comment, here's the GitHub issue that actually helped me figure this out:

https://github.com/Microsoft/TypeScript/issues/13581#issuecomment-273923793

... and here's the module-resolution description for TypeScript:

https://www.typescriptlang.org/docs/handbook/module-resolution.html#node

... because TypeScript will automatically look in the *local* node_modules, you wouldn't need the tsconfig.json "paths" definition if you had a local node_modules folder -- TS would find the module .d.ts file automatically. However, since I am using a non-local, shared vendors folder, I had to tell it where to try and resolve the module definition.

2 Comments

Hi Ben,
I'm Sr. SW Eng and the Angular 2/4 lead. I really enjoy your posts and am confident you can point me in the right direction.

We started the project early in ng2 with CLI and (long story as to why) have found short comings that make us wish we had started with SystemJS.

I used GULP/SystemJS/et al a long time ago (ng1 and ng2 alpha...). I'm hoping there is a CLI equivalent by now for setting up a new project.

Perhaps a nice full-featured, otherwise mostly empty, seed would do the job. Then I can manually port the current project over.

Thanks again for your help and your terrific articles.
Chuck

15,674 Comments

@Chuck,

I'm glad your enjoying the Angular explorations. I haven't personally looked into the CLI very much yet. I've used it in a workshop, but really only in the capacity that I was directed. So, I am not sure how much / how well it can be customized. One of the reasons I haven't looked into it is because I tend to have strong feelings about how I want to organize things and I am not sure if I would feel like the CLI was adding too much friction or not.

I've sorted of taken a detour into Node.js for the last few weeks, but hope to be getting back into Angular v4 shortly. Hopefully more good stuff to come!

2 Comments

@Ben,

Thanks for your input. I used SystemJS a while ago and then switched when Ng2 went to webpack. I can say the one thing it offers is commands that generate the model, component, directives, services, ... for you and plug them together automagically, which is nice.

I found a great SystemJS seed by mgechev (and 154 others) in github that is very robust and I have been able to just copy files across from the webpack implementation and get things working very quickly.

As the Angular lead at Sensitech, I always find myself about 20,000 leagues under water and I constantly running into weird requirements and issues.

Thanks again for your comment. Let me know if there's anything I can help with.

Chuck

15,674 Comments

@Chuck,

Thank you sir, I appreciate the offer of support. I'm starting to get back into the Angular swing-of-things now; so, will digging into this stuff.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel