Use Web API as a dynamic runtime Typescript compiler

· 1468 words · 7 minutes to read

There’s been a lot of community buzz about Typescript recently – and rightfully so – as it solves a lot of Javascript’s issues, and despite being in its infancy, shows a tremendous amount of potential already.

One of the features (or side effects, depending on how you look at it) of Typescript, is that your are required to compile your code, to produce JS output. Sure, it is possible to dynamically compile Typescript in the browser (using its JS compiler), but that requires you to reference Typescript.js which is roughly 250 kB, and might be a tough pill to swallow.

However, using the ASP.NET Swiss army knife called Web API, and an approach we already disuussed on this blog before, let me show you how you can quite smoothly leverage on Web API pipeline to dynamically compile your Typescript code into Javascript at runtime.

More after the jump.

The idea 🔗

So, as mentioned, instead of compiling Typescript manually each time you change anything, we will let Web API handle this for us – using a custom MediaTypeFormatter.

All you need to do is reference the JS script through a specifically pre-configured Web API route/controller, and let the Web API pipeline do the heavy lifting via a MediaTypeFormatter.

The route and a the controller 🔗

We would like to be able to reference our dynamically compiled Typescript files from the HTML like this:
[crayon lang=”html”]

[/crayon]

To do that, let’s start with a typical MVC4, Web API project.

We need a custom route, and a simple controller:
[crayon lang=”csharp”]
config.Routes.MapHttpRoute(
name: “DynamicScripts”,
routeTemplate: “dynamic/{controller}/{name}.{ext}”,
defaults: new { name = RouteParameter.Optional, ext = RouteParameter.Optional },
constraints: new { controller = “Scripts” }
);
[/crayon]

[crayon lang=”csharp”]
public class ScriptsController : ApiController
{
public string Get(string name)
{
return name;
}
}
[/crayon]

The route allows us to pass a name parameter and an extension (to match the filename.js path we required), and the controller simply forwards the name of the file to the formatter which will do all the work.

The plugin 🔗

In order to make this work, we need the command-line Typescript compiler. Grab it from the official website. It’s the middle one (plugin) – it says Visual Studio 2012, but that doesn’t matter if you have 2010 – we are really interested in the command line tool anyway.

Once installed, you can find the TCS.exe, the command line Typescript compiler under C:Program Files (x86)Microsoft SDKsTypeScript�.8.0.0. Let’s copy all the compiler files over to our solution, to the TS folder under the root of our project (or anywhere else, but then you need to update the folder references I used in my code).

The formatter/compiler 🔗

Note that the code shown here should be adapted to fit your specific needs (such as persistence mechanisms, error handling and what not) – I doodled this while watching Sunday Night Football so it is by no means perfect – but hopefully points you in a right direction.

[crayon lang=”csharp”]
public class TypeScriptMediaTypeFormatter : MediaTypeFormatter
{
private static readonly ObjectCache Cache = MemoryCache.Default;

public TypeScriptMediaTypeFormatter()
{
this.AddUriPathExtensionMapping(“js”, “text/html”);
}

public override void SetDefaultContentHeaders(Type type, System.Net.Http.Headers.HttpContentHeaders headers, System.Net.Http.Headers.MediaTypeHeaderValue mediaType)
{
headers.ContentType = new MediaTypeHeaderValue(“application/javascript”);
}

public override bool CanReadType(Type type)
{
return false;
}

public override bool CanWriteType(Type type)
{
return type == typeof(string);
}

public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) {
//TODO
}
}
[/crayon]

Before we move on to writing to the stream, which is the heart of everything, notice that we set a few defaults here – we add UriPathExtensionMapping so that the formatter kicks in for all .js requests. All the output produced will be returned as application/javascript as that’s how browsers expect to get their JS files. We support requests for text/html, as that’s how some of the browsers would issue them.

We only support serializing (one way formatter only, no deserializing) and only objects of type string (since really all we are interested in, are the paths to the files).

WriteToStreamAsync 🔗

The MediaTypeFormatter method that writes to the stream will behave accordingly:

  1. Take the name of the file (Typescript file)
  2. Check if the TS file exists
  3. Look into the cache – if the reference to the TS file exists there, use the MD5 checksum to make sure the file TS hasn’t changed since last read
    4a. If it hasn’t, return the contents from the cache
    4b. If it has or if it didn’t exist in the cache in the first place, use tcs.exe (which we copied to our website root earlier) to compile the Typescript file and return JS
  4. Cache the latest JS output and the MD5 checksum of the TS for later

[crayon lang=”csharp”]
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
var serverPath = HttpContext.Current.Server.MapPath(“~/tsc”);
var filepath = Path.Combine(serverPath, value.ToString() + “.ts”);
var jsfilepath = Path.Combine(serverPath, value.ToString() + “.js”);

var tcs = new TaskCompletionSource

();

if (File.Exists(filepath))
{
string cachedItem = CheckCache(filepath, value as string);

if (cachedItem != null)
{
using (var writer = new StreamWriter(writeStream))
{
writer.Write(cachedItem);
}
}
else
{
var typescriptCompiler = new ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardError = true,
FileName = Path.Combine(serverPath, “tsc.exe”),
Arguments = string.Format(“”{0}””, filepath)
};

var process = Process.Start(typescriptCompiler);
var result = process.StandardError.ReadToEnd();
process.WaitForExit();

if (string.IsNullOrEmpty(result))
{
using (var filestream = new FileStream(jsfilepath, FileMode.Open, FileAccess.Read))
{
filestream.CopyTo(writeStream);
var fileinfo = new Dictionary();
fileinfo.Add(“md5”, ComputeMD5(filepath));

using (var reader = new StreamReader(filestream))
{
filestream.Position = 0;
var filecontent = reader.ReadToEnd();
fileinfo.Add(“content”, filecontent);
}

Cache.Set(value as string, fileinfo, DateTime.Now.AddDays(30));
}
}
else
{
throw new InvalidOperationException(“Compiler error: ” + result);
}
}
}

tcs.SetResult(null);
return tcs.Task;
}

private string CheckCache(string filepath, string filename)
{
var md5 = ComputeMD5(filepath);
var itemFromCache = Cache.Get(filename) as Dictionary;

if (itemFromCache != null)
{
if (itemFromCache[“md5”] == md5)
{
return itemFromCache[“content”];
}
}
return null;
}

private string ComputeMD5(string filename)
{
var sb = new StringBuilder();
using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
var md5 = new MD5CryptoServiceProvider();
var bytes = md5.ComputeHash(file);

for (int i = 0; i < bytes.Length; i++) { sb.Append(bytes[i].ToString("x2")); } } return sb.ToString(); } [/crayon] This is a very similar technique we used in the post a couple of weeks ago – invoking a command line application from C#. The good thing is that we only do it once – I chose to cache the result in memory, but there are several approaches you could take, i.e return a path to the JS file that has been produced by the TCS compiler. In principle, the main thing we want to do is to make sure that by using MD5 we do not unnecessarily invoke the compiler – instead we only do that when the source Typescript file changes.

Notice that we use the StandardError property of the TCS Process to determine whether the compiler ran successfully. If there are any compilation errors, this property will contain the details; otherwise it will stay empty.

Trying it

Let’s imagine I have a demo TS file, called demo.ts:
[crayon lang=”typescript”]
class Person {
constructor(public name) { }
sing(text) {
return this.name + ” sings ” + text;
}
}
[/crayon]

I can now reference it in my HTML like this (recall our routes from earlier) – using same name, only with .js extension (even though such Javascript does not physically exist – all we are interested in is hitting the controller and getting into the Web API pipeline):
[crayon lang=”HTML”]

[/crayon]

Now the Web API will compile this on the fly and produce the expected JS output in the browser – and remember, we didn’t compile the Typescript at all:

The JS is now cached, and all subsequent requests do not result in recompilation as well.
I can now use the JS any way I would like to:

If I change the Typescript code to something else – i.e. let’s modify the sing method:
[crayon lang=”typescript”]
class Person {
constructor(public name) { }
sing(text) {
return this.name + ” sings ” + text + ” and it’s embarassing.”;
}
}
[/crayon]

I don’t need to recompile anything, simply refresh the page – now the MD5 checksums don’t match anymore so it causes TS to recompile and the new version of JS is returned (and cached for future use):

Summary

As mentioned, Typescript can be compiled using purely raw Javascript – and that would be a nice option if you are ready to swallow the additional 250 kB of JS to be included in your page/app (by the way, if someone is interested in how to do that, let me know and I can blog about that).

However, with some Web API hackiness (is that even a word?), using some of the techniques we discussed on this blog before, it is really easy to provide a dynamic runtime compilation of your Typescript code!

About


Hi! I'm Filip W., a software architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP