Table of Contents
Reality - most everything out there seems to be half-baked, in the hot summer sun, piles of cow you-know-what. And such is my experience trying to get client-side TypeScript to work with proper debugging as I ported my HTML editor to TypeScript (note, this is not TypeScript's fault!) using Visual Studio Code.
Cow Pile #1: The Chrome Debugger Extension in Visual Studio Code simply does not work if you put source code into sub-folders. And this problem has been reported since 2018 at least but the only viable solution is, add debugger;
to the entry point of your TypeScript code and then restore all breakpoints. What bullsh...
Cow Pile #2: To even be able to run the code in Visual Studio Code, you'll need some build process that bundles all the transpiled TypeScript. What?
OK, on piles #1 and #2, I was not able to figure out any other solutions.
Cow Pile #3: Visual Studio really wants you to drink the Kool-Aid and use TypeScript in an ASP.NET or .NET Core solution. I however do not want to drink the Kool-Aid and simply want a client-side solution. The only way to create one is to load a TypeScript HTML Template which, after, much Googling, I found.
Cow Pile #4: But it doesn't really work "out of the box." You need require.js. You need to set your module builder to the right setting.
Cow Pile #5: But then, amazingly, it does work. Except you're left figuring out, ok, how do I differentiate between development and production code? How do I choose whether to bundle everything or nothing? Why is my only option to bundle everything or nothing?
Leaving me with the renewed impression: why do we leave software development tooling to gnomes who live in a vast underground cave system filled with bottomless pits, superbats, and the occasional Wumpus, and who clearly require megadoses of Vitamin D because they never see the light of the sun and have a clue as to what "users" (developers trying to get something done) actually need.
So here's my journey in the underground labyrinth of client-side TypeScript tooling and discovering unused tunnels back into the light. Hopefully, it'll save you some time if you decide to go on similar spelunking journeys. I did not explore all the narrow recesses and tunnels, so maybe there are shorter paths out there. If you find one, leave a map for others!
There are so many "editors" out there, but the cave entrance I chose was labeled "Visual Studio Code." I mean, why not? I was already familiar with it, and I really didn't want to spend the time figuring out how to navigate other entrances, like WebStorm (requires an entrance fee after 30 days unless you are a student of arcane magic), Atom, or Sublime (whose entrance I boarded up a few months ago because it became unusable and was subject to cave-ins) and I was dubious about the various trolls one has to befriend to get things beyond simple editing to work. I leave that to other adventurers. I'd already journeyed with some of the trolls I know from the Visual Studio Code tavern, and I'd rather travel with acquaintances than strangers, so the decision was made.
So the first thing you need is some sort of bundler and transpiler executor, and I opted for Gulp. WebPack seems popular (at least it keeps getting mentioned on StackOverflow) but I was turned off by the multi-page "getting started" guide and "First we'll tweak our directory structure slightly..." Seriously? You can't create a getting started guide without having to do some tweaking? Riiight. Grunt - well, I'd had some experience with it a few years ago and, I don't know, I just wasn't thrilled. So Gulp it was, especially since I found some decent examples of working with client-side TypeScript, and I suppose the "streaming" concept is more modern than Grunt.
First, create, well, a package.json file using npm init
. From the command line. How quaint.
I started in my projects folder and created a directory called tsvscdemo
and cd
'd into it:
then ran npm init
, and it prompts for various things, the only default that I changed is the entry point, to ./dist/main.js
.
Next, install various packages. I included jquery because I wanted to learn how to use third party packages like jquery with TypeScript. This is a bit of a can of worms because if you want the intellisense in your editor, the third party vendor needs to provide a sourcemap file. Speaking of source maps, take a gander at http://definitelytyped.org/, "The repository for high quality TypeScript type definitions." I think this is a link worth noting!
Anyways, do this:
npm install -g gulp-cli
npm install --save-dev typescript gulp gulp-typescript
npm install --save-dev browserify tsify vinyl-source-stream
npm install --save-dev watchify fancy-log
npm install --save-dev @types/jquery
npm install --save jquery
The browserify
package is a bundler which will take your separate TypeScript .ts files that have been transpiled into .js files and bundles them into a single .js file that your browser can then load. Why do this? Good question. I suppose the answer is that it means you don't have to add script
tags in your HTML for each of the separate .js files. I'm not sure I can think of a better reason, but everyone seems to do this without much explanation, they just expect you to drink the Kool-Aid.
The watchify
package monitors your .ts files for changes (and other files if you wish) and automatically executes the transpiler and bundler and moves the resulting files to the dist folder. Useful.
If you haven't already, install the Debugger for Chrome extension for Visual Studio Code:
Create the file launch.json at the root of your project folder with the contents:
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Program",
"file": "${workspaceFolder}/dest/index.html",
}
]
}
This tells Visual Studio code how to launch the "program."
Also in the project root folder, create gulpfile.js with the script to transpile and bundle the client-side application:
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');
var fancy_log = require('fancy-log');
var paths = {
pages: [
'*.html',
'*.css'
]
};
var br = browserify({
basedir: '.',
debug: true,
entries: [
'index.ts',
],
cache: {},
packageCache: {}
}).plugin(tsify);
gulp.task('copy-html', function () {
return gulp.src(paths.pages)
.pipe(gulp.dest('./dest'));
});
function bundle() {
return br
.bundle()
.on('error', fancy_log)
.pipe(source('bundle.js'))
.pipe(gulp.dest('./dest'));
}
gulp.task('default', gulp.series(gulp.parallel('copy-html'), bundle));
The important parts here is specifying:
- where the source files are that need to be bundled - the
entries
array - the destination folder as ./dest
- additional files that need to be moved to the destination in the
pages
array
More stuff to do:
{
"files": [
"index.ts"
],
"compilerOptions": {
"noImplicitAny": true,
"target": "es6",
"sourceMap": true,
"outDir": "dest"
}
}
I'm not sure if files
needs to be specified here, it seems redundant with regards to the gulp file. The real nightmare is the "target
", which I specified as es6
. This is the only way I was able to get the import
statement to work. Among other things, ECMA Script 6 supports arrow functions which I use quite a bit. Anyways, this was one of the pain points to figure out.
A simple HTML file:
<!DOCTYPE html>
<html>
<body>
<div>Hello World!</div>
<input id='name'/>
<script src="bundle.js"></script>
</body>
</html>
The salient point here is that it loads the script generated by Gulp.
import * as $ from "jquery";
$("#name").val("Marc Clifton");
console.log("Hello World");
console.log("Goodbye Cruel World!");
Here, we're just testing jQuery and adding a couple logging lines to test with breakpoints.
From the root of the project folder, run Gulp (from the command line). You should see:
Note in Visual Studio Code the files that were placed in the dest folder:
Press F5 to start the application in debug mode -- Visual Studio Code should automatically create the "Launch Chrome against localhost" debug configuration for you:
You should see:
Demonstrating that the page loaded, the TypeScript file got transpiled and bundled, and that jQuery is working.
Now set a breakpoint on line 3:
Press F5 again and Chrome should break at the breakpoint, allowing you to debug the code in Visual Studio Code:
It didn't work! Why not? Well, it turns out that because we're outputting to the dest folder, something in Visual Studio Code and its interaction with Chrome causes breakpoints to fail.
The workaround that gets "thumbs up" in the VSCode forums is to set a debugger;
statement somewhere, usually at the start of your application, and then you can either set breakpoints or "reapply all breakpoints", like this (don't forget to Gulp your project again after adding the debugger;
line), then press F5 to start the debugger, and you get this:
Now right-click at the bottom-left of Visual Studio Code on the breakpoint and select "Reapply All Breakpoints" from the popup menu:
Or if that doesn't work, just add the breakpoint manually again. Press F5 to continue debugging, and VS Code / Chrome will now stop at the breakpoint.
I find that to be a totally unacceptable workaround! Why this is still broken in VS Code is beyond me, and if anyone knows how to resolve this problem, please let me know and I'll update this article, removing my rants!
This section relied on the two following posts:
It astounds me that Microsoft doesn't provide a means of creating a client-side TypeScript application, but then again, maybe it doesn't astound me as they want you to drink more of the ASP.NET or .NET Core Kool-Aid. OK, fine, but I find myself creating a variety of small web applications that don't require a server and certainly don't require a framework like Angular.
The first step is to use the "Online" option when creating a new project:
And selecting the "HTML Application with TypeScript" template:
See Rich Newman's post (link is above) on his recreation of this template, which existed in VS2015 but was removed in VS2017.
Create the project so it has the following folder organization, ignore the notes.txt file, that's just something I added to make some notes while I was getting things working:
To get this to work, particularly with regards to the import
command, requires, um, require.js and the module system AMD.
Here's a link to read more about the JavaScript Module System. Sigh. As a side note: AMD (Async Module Definition) is implemented by RequireJS.
- You can download require.js from here: https://requirejs.org/docs/download.html
- And the d.ts file (the sourcemap file) from here: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/requirejs
- Though as the StackOverflow post that I linked to above points out, save index.d.ts as require.d.ts.
- Put these in the folders as illustrated in the screenshot above. You can add other libraries like jQuery and its d.ts file as well as you see fit.
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TypeScript HTML App</title>
<link rel="stylesheet" href="app.css" type="text/css" />
<script data-main="app/AppConfig" type="text/javascript" src="lib/require.js"></script>
</head>
<body>
<h1>TypeScript HTML App</h1>
<div id="content"></div>
</body>
</html>
The salient point here is that the JavaScript entry point is specified to be AppConfig
.
Sort of overkill but this illustrates the startup process for JavaScript:
import { AppMain } from "./AppMain"
require(['AppMain'],
(main: any) => {
var appMain = new AppMain();
appMain.run();
}
);
import { Greeter } from "./classes/Greeter"
export class AppMain {
public run() {
let greeter = new Greeter();
greeter.greet();
}
};
All this does is instantiate the Greeter
class and execute the greet
method. Well, it also imports the class. An important point.
export class Greeter {
constructor() {
}
greet() {
console.log("Hi!");
}
}
That's exciting!
Set a breakpoint on the console logging line:
Press F5, and lo-and-behold, Chrome breaks and Visual Studio is at the breakpoint line:
No muss, no fuss, no grunt, not a lot of configuration files to tweak. Nice!
You can bundle the JavaScript into a single file in the project's TypeScript Build configuration options:
Once you do this, you can no longer debug the application!
And furthermore, to break as little as possible, I had to copy require.js manually into a lib folder as well as index.html. I guess Microsoft figures you can write a post-build step to copy the non-JavaScript files (HTML, CSS, images, icons, etc.) to the "distribution" folder. As well as do things like run a minifier. So there's a lot of work for the developer to do still to create a "production" folder of the application, assuming of course you want all your JavaScript bundled into a single file. Creating separate bundles (say, for different pages of your application) is far beyond what Visual Studio supports but again could be handled with post-build operations. At least, so I figure, since I haven't gone through the full process of creating a production-ready code base with what I've learned here.
Also, bundling required me to modify the index.html file to use the bundled file:
<script data-main="bundle" type="text/javascript" src="lib/require.js"></script>
Interestingly, all the path references in the TypeScript files, like this one: import { Greeter } from "./classes/Greeter"
are irrelevant because of the way the import is transpiled to JavaScript:
define("AppMain", ["require", "exports", "classes/Greeter"]
What the heck is "define
"? Well, it's part of the AMD specification, and guess what, AMD is implemented by RequireJs. Read more here.
So on a positive note, all those path references don't have to be modified in the bundled JavaScript.
It was disappointing to see how Visual Studio Code failed, and I still hold some hope that I was doing something wrong. And it's ironic that in Visual Studio Code, the bundled file can be debugged as long as it is output to the root folder, but in Visual Studio, the bundled file appears not to be able to be debugged, even though VS generated a sourcemap file. Again, I feel like I'm doing something wrong, but I also feel like this stuff should just work without my taking hours of time to peruse StackOverflow for a possible solution.
Regardless, I've achieved what I wanted to: creating client-side applications in TypeScript with the ability to debug them. What this means for me is that I can write the server-side without having to buy into ASP.NET or .NET Core. I can write the client-side in TypeScript and the server-side in whatever I want -- for example, Python.