Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

Client-Side TypeScript without ASP.NET, Angular, etc.

4.32/5 (9 votes)
1 Oct 2019CPOL12 min read 10.3K   50  
Client-side TypeScript and debugging trials and tribulations with VS Code and Visual Studio

Table of Contents

Introduction

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!

Visual Studio Code - Don't Try This at Home

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.

Gulp, WebPack, Grunt

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.

Pre-Setup 1

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:

Image 1

then ran npm init, and it prompts for various things, the only default that I changed is the entry point, to ./dist/main.js.

Image 2

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.

Pre-Setup 2

If you haven't already, install the Debugger for Chrome extension for Visual Studio Code:

Image 3

Create the Launch JSON File

Create the file launch.json at the root of your project folder with the contents:

JavaScript
{
  "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."

Create the Gulp Script

Also in the project root folder, create gulpfile.js with the script to transpile and bundle the client-side application:

JavaScript
var gulp = require('gulp');
// var sourcemaps = require('gulp-sourcemaps');
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, // Setting to false removes the source mapping data.
  entries: [
    // TS files to transpile and bundle.
    '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

Add a tsconfig.json File

More stuff to do:

JavaScript
{
  "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.

Create index.html

A simple HTML file:

HTML
<!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.

A Simple index.ts File

JavaScript
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.

Run It!

From the root of the project folder, run Gulp (from the command line). You should see:

Image 4

Note in Visual Studio Code the files that were placed in the dest folder:

Image 5

Press F5 to start the application in debug mode -- Visual Studio Code should automatically create the "Launch Chrome against localhost" debug configuration for you:

Image 6

You should see:

Image 7

Demonstrating that the page loaded, the TypeScript file got transpiled and bundled, and that jQuery is working.

Debugging - FAIL!

Now set a breakpoint on line 3:

Image 8

Press F5 again and Chrome should break at the breakpoint, allowing you to debug the code in Visual Studio Code:

Image 9

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 Lame Workaround

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:

Image 10

Now right-click at the bottom-left of Visual Studio Code on the breakpoint and select "Reapply All Breakpoints" from the popup menu:

Image 11

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.

Image 12

Conclusion

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!

Using Visual Studio

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.

Create a Visual Studio Project with the HTML Application with TypeScript Template

The first step is to use the "Online" option when creating a new project:

Image 13

And selecting the "HTML Application with TypeScript" template:

Image 14

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:

Image 15

Configure the Project

To get this to work, particularly with regards to the import command, requires, um, require.js and the module system AMD.

Image 16

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.

Setting up require.js

  1. You can download require.js from here: https://requirejs.org/docs/download.html
  2. And the d.ts file (the sourcemap file) from here: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/requirejs
  3. Though as the StackOverflow post that I linked to above points out, save index.d.ts as require.d.ts.
  4. 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.

The index.html File

HTML
<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.

The AppConfig.ts File

Sort of overkill but this illustrates the startup process for JavaScript:

JavaScript
import { AppMain } from "./AppMain"

require(['AppMain'],
  (main: any) => {
    var appMain = new AppMain();
    appMain.run();
  }
);

The AppMain.ts File

JavaScript
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.

The Greeter.ts File

JavaScript
export class Greeter {
  constructor() {
  }

  greet() {
    console.log("Hi!");
  }
}

That's exciting!

Debug It!

Set a breakpoint on the console logging line:

Image 17

Press F5, and lo-and-behold, Chrome breaks and Visual Studio is at the breakpoint line:

Image 18

No muss, no fuss, no grunt, not a lot of configuration files to tweak. Nice!

Bundling with Visual Studio - The Catches

You can bundle the JavaScript into a single file in the project's TypeScript Build configuration options:

Image 19

Catch #1

Once you do this, you can no longer debug the application!

Catch #2

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.

Catch #3

Also, bundling required me to modify the index.html file to use the bundled file:

HTML
<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:

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.

Conclusion

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)