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:
- Take the name of the file (Typescript file)
- Check if the TS file exists
- 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 - 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