26 May 2015
Back in January, I heard about DuoCode and signed up to be part of the beta program when they started it. It takes C# code, analyses it with Roslyn and then generates JavaScript to run in the browser. It promises to offer all of the benefits of statically-typed C# in Visual Studio for writing browser code. I didn't actually get round to doing any more than installing an early version during the closed beta period, but recently they opened up the beta product to the general public and I got around to trying it out properly. Trying to not go over the top here.. it's absolutely fantastic!
The compilation process has been really fast on the (admittedly small) projects I've been trying out and the translation itself seems faultless - I've tried to catch it out in a variety of ways, seeing if it could handle "ref" or "out" arguments, for example (since JavaScript has no concept of this), playing with generics, trying out LINQ statements, generating anonymous types.. it really does seem to do it all! Suffice to say, I'm very impressed with what I've seen.
But.. (there's always a but).. they currently say on their FAQ page, under "How much does DuoCode cost?" -
A pricing model will be introduced in the future, after the Beta period is over.
This is quite a concern since there's literally no indication as to whether they will be expecting to charge for personal use, for commercial use, per-seat or per-company for commercial use, whether there will be a freemium model where the "basics" are free but additional features are paid-for.. or something else entirely. And there's also no indication of when the beta period will actually be over. Bummer. Particularly since I'm so excited by it that I seriously want to push it for a project at work, but if it's going to be prohibitively expensive or if its stability is going to be a problem (or if there's a chance that it will never reach 1.0) then it's going to be difficult to do some in good conscience.
But let's brush all that aside for now and get back to some positives. Like I said, the way that this all works is phenomenal (even the JavaScript itself that is generated is really good - both from the source code you write and in the support library that recreates System, System.Collections, System.Linq, etc..) and I wanted to try it out in a few scenarios.. particularly with my other current technical loves; React and Flux.
Spoiler alert: DuoCode and React are an incredible match and I can't recommend highly enough that you try them out!
I've written previously about using TypeScript with React (in TypeScript / ES6 classes for React components and TypeScript classes for (React) Flux actions) so I've got some experience that I'd hoped to be able to apply. Integrating React with TypeScript, there are two major challenges: Firstly, creating a base "React Component" class that may be derived from to create custom components. And secondly, getting TypeScript definitions for the React library.
The same two challenges apply to working with React from DuoCode.
The biggest issue with using classes as React components is that React's render methods all expect a React "Element" and not just a generic instance of whatever. So we can't just define a class with a "render" method and pass it into React; the React library's "createElement" method must be used to prepare an instance for use as a React component.
In TypeScript, I addressed this by having each component file be an AMD module that defined a class for the component model but that actually exported a "Factory" function that would instantiate the class, given the "props" reference it would need - eg.
import React = require('react');
interface Props { name: string; role: string; }
class PersonDetailsComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
public render() {
return React.DOM.div(null, this.props.name + " is a " + this.props.role);
}
}
function Factory(props: Props) {
return React.createElement(PersonDetailsComponent, props);
}
export = Factory;
The "React.Component" class that it is derived from is a class within the React library, exposed as a generic class (whose type parameter is the data type of the "props" reference for the component). This base class is accessible due to the third party TypeScript definition (I'll talk about this more shortly).
The gist is that React library function "createElement" must be used to initialise Components - it gets passed the Component class' constructor and the "props" reference to pass into the constructor, but the actually calling of that constructor is not performed explicitly.
Doing exactly this in C# would be.. challenging. And not particularly idiomatic.
What I've ended up with instead is the following:
public class ExampleComponent : ComponentWrapper<ExampleComponent.Props>
{
public static Element New(Props props) { return Ele.Props(props).As<ExampleComponent>(); }
private ExampleComponent(ComponentProps<Props> props) : base(props) { }
public override Element Render()
{
return DOM.div(null, props.Props.Name);
}
public class Props
{
public string Name;
}
}
A Component class that inherits from a base class, again with a generic type parameter for the "props" type. The constructor is private since it will never be called directly from consuming code - instead there is a static "New" function that does.. magic. There's actually several things to walk through here, so let me take it one step at a time.
The "ComponentWrapper" is a DuoCode binding, meaning it's not a class that the DuoCode compiler has to translate into JavaScript. Instead, it's a way to tell the type system about a type that will be available at runtime. It looks like this:
[Js(Extern = true, Name = "ReactComponentWrapper")]
public abstract class ComponentWrapper<TProps>
{
protected readonly ComponentProps<TProps> props;
protected ComponentWrapper(ComponentProps<TProps> props) { }
[Js(Name = "render")]
public abstract Element Render();
}
[Js(Extern = true, Name = "ReactComponentWrapper")]
public static class ComponentWrapper
{
[Js(OmitGenericArgs = true)]
public extern static Element GetElement<TProps>(Type componentType, TProps props);
}
The C# extern keyword "is used to declare a method that is implemented externally" - usually this is used to import functions from dlls, but here it's used to indicate that it will be implemented by runtime JavaScript. The "Js" attribute is a DuoCode construct that allows structures to be identified as being implemented externally; in other words, that the DuoCode compiler need not try to generate corresponding JavaScript. It also allows for a different reference name to be used at runtime than the class would otherwise indicate - so instead of "MyDuoCodeProject.ComponentWrapper", it will use the name "ReactComponentWrapper" in the final JavaScript.
So what is this "ReactComponentWrapper" class that will be present at runtime? Well, you'll have to reference an additional JavaScript file in your index.html, with the following content:
window.ReactComponentWrapper = (function (_super) {
function Component(props) {
_super.call(this, props, null);
}
Component.ctor = Component;
Component.GetElement = function (componentType, props) {
return React.createElement(componentType.self.ctor, { Props: props });
};
return Component;
})(React.Component);
This base class hooks up the C# Component class so that it inherits from the "React.Component" class in the React library, just like how the TypeScript class is derived from the React.Component library class. There are two things to note; when DuoCode generates JavaScript that instantiates classes, it always does it through a "ctor" function, so a "ctor" is required on the "ReactComponentWrapper" that is an alias onto its primary constructor. And a "GetElement" function is defined that takes a DuoCode Component class and the props reference that should be passed into a constructor on that type - from these it returns a React "Element" by calling the library's "createElement" method. This method is also declared in the "ComponentWrapper" binding.
With all this, it would be possible to create a React "Element" from the Component class by calling
var element = ComponentWrapper.GetElement<ExampleComponent.Props>(
typeof(ExampleComponent),
new ExampleComponent.Props { Name = "test" }
);
.. but I thought that would be a bit unwieldy every time it was required - it needs "ExampleComponent" to be typed three times!
To make it a little less arduous, I've created a helper class -
public static class Ele
{
public static ElementFactory<TProps> Props<TProps>(TProps props)
{
if (props == null)
throw new ArgumentNullException("props");
return new ElementFactory<TProps>(props);
}
public class ElementFactory<TProps>
{
private readonly TProps _props;
public ElementFactory(TProps props)
{
if (props == null)
throw new ArgumentNullException("props");
_props = props;
}
public Element As<TComponent>() where TComponent : ComponentWrapper<TProps>
{
return ComponentWrapper.GetElement(typeof(TComponent), _props);
}
}
}
C# supports generic type parameter inference in method calls, so long as only a single type parameter needs to be resolved. This means that the call
Ele.Props<TProps>(props)
can be reduced to
Ele.Props(props)
and "TProps" will be inferred to be whatever the type of "props" is. Then the call
Ele.Props(props).As<TComponent>()
will know that "TProps" in the type constraint "TComponent : ComponentWrapper<TProps>" is whatever the type of "props" was.
It's a bit back-to-front, specifying the "props" before declaring the type of the Component class, but it's succinct at least! And it's what allows the Component classes' static "New" factory methods to be declared thusly:
public class ExampleComponent : ComponentWrapper<ExampleComponent.Props>
{
public static Element New(Props props) { return Ele.Props(props).As<ExampleComponent>(); }
private ExampleComponent(ComponentProps<Props> props) : base(props) { }
// .. rest of the Component goes here ..
In other words, instead of having to type "ExampleComponent" three times, you only to do so once :)
On the whole, I've found this to be a really good system. But there are a couple of small compromises. You need to explicitly reference the "ReactComponentWrapper.js" file in your page. I wish that there was a way to declare some JavaScript within .net code that will be included in the final output unaltered, this would make it really easy to have a separate project (or to create a NuGet package) for the React integration containing the base "ReactComponentWrapper" JavaScript class, the C# "ComponentWrapper" class, the helper classes (such as "Ele") and the bindings to the React library (again, more on this coming up). I haven't found a way to do this yet, though.
Secondly, due to the way that the "createElement" function works in the React library, the Component class constructors may only take a single argument (the "props" data). This means that all configuration data must be included in the "props" type (but this is the standard arrangement for React component classes, so it's no biggie).
The third and final compromise is that the "createElement" function does something a bit wonky with the "props" reference handed to it, it basically strips it down and rebuilds it, maintaining only properties declared on the object itself and not on a prototype of that object. So if your props object was a class with fields - eg.
public class Props
{
public string Name { get; set; }
}
then you would have problems, since DuoCode will generate a JavaScript object with get_Name and set_Name functions on the Props prototype (which is best practices for JavaScript, it means that the functions themselves are shared between all instance of the class, rather than there being identical copies of the functions present on every Props instance) - "createElement" will loop over the properties and anything that doesn't return true for "hasOwnProperty" will be lost, so the "get_Name" and "set_Name" functions will go astray. This will only become clear when code in your C# Components tries to access the Name property and fails at runtime.
The workaround for this is to wrap the "props" reference in a container object, since "createElement" only enumerates (and interferes with!) the top level properties. That's why the "GetElement" function in the "ReactComponentWrapper" looks like this:
Component.GetElement = function (componentType, props) {
return React.createElement(componentType.self.ctor, { Props: props });
};
It gets a "props" reference and it wraps it up so that React doesn't try to hurt it. And that's why the C# "ComponentWrapper" exposes the "props" data through a protected property thusly:
// Note: The "props" property is a wrapped ComponentProps<TProps> instance,
// rather than just being TProps
protected readonly ComponentProps<TProps> props;
The "ComponentProps" class looks like this:
[Js(Extern = true)]
public sealed class ComponentProps<T>
{
private ComponentProps() { }
[Js(Extern = true, Name = "Props")]
public T Props { get; }
}
It's just a way to tell the C# type system about this level of indirection around the "props" data, and it's why the "render" method in the example Component above looks like this:
public override Element Render()
{
return DOM.div(null, props.Props.Name);
}
But, really, you don't need to worry about this! With this system in place, you can just take it for granted that it works, and if you forget that you need to access "props.Props.Name" instead of "props.Name" then the compiler will give you an error reminding you that you've made a mistake! I do love static analysis :)
If you've got through the above, then you can probably imagine where I'm going next. We can define and create Components now, but we haven't got any way to actually call "React.render" or to use any of the builtin Component initialisers, such as "React.DOM.div".
To do this, more "bindings" are required - these are the classes and functions marked as "extern" / "[Js(extern = true)]" that tell the C# type system how to connect to JavaScript that is known to be present at runtime. As I said before, they're basically the equivalent of TypeScript type definitions but with the added bonus that it's possible to give the classes and functions aliases so that they fit more neatly into your project structure and naming convention (the DuoCode compiler will ensure that these aliases are mapped back to the original function names in the final JavaScript).
So let's deal with the most obvious one first:
[Js(Extern = true, Name = "React")]
public static class React
{
[Js(Extern = true, Name = "render")]
public extern static void Render(Element element, HTMLElement container);
}
There's a few things to talk about here. I've named the function "Render" and used the "Js" attribute to map it back on to the library function "render" - note the change in case. In the code that I write, C# functions are pascal-cased, so I wanted to be able to call "Render" from my C# code, rather than "render". The "HTMLElement" type is a DuoCode class, used to describe DOM elements (if you call "Global.window.document.getElementById(x)" then you get one of these back). This is just what React wants for the second argument of the "render" call; a DOM element, so that's great. But for the first argument, the "Element" type is a React type that needs a binding -
[Js(Extern = true)]
public sealed class Element
{
private Element() { }
public extern static implicit operator Element(string text);
}
This is not something that we're ever going to instantiate directly, so it is sealed and its constructor is private. This is used to describe the return type of the "createElement" function, and so is used as the return type from the "Ele.Props(props).As
There is one other way that it comes into play, however - there is an implicit conversion from the string type. This is required because strings are frequently used as child elements in React - eg.
React.DOM.span({ className: "greeting" }, "Hi!")
The implicit operator here, as part of an "extern" class, simply serves to tell the compiler that it's ok to use a string anywhere that a React Element is required - it doesn't have to do anything special, it just has to allow it. The consistency of the DuoCode translation process and the flexibility of the C# language really play together beautifully here and I was delighted to see how easily it was possible to dictate this to the type system.
Speaking of the React.Dom class.. So far, I've only covered a fraction of the total library, but hopefully it's enough to make it clear how it may be expanded on (and I intend to add support for element types as and when I need them).
[Js(Extern = true, Name = "React.DOM")]
public static class DOM
{
public extern static Element div(HTMLAttributes properties, params Element[] children);
public extern static Element h1(HTMLAttributes properties, params Element[] children);
public extern static Element input(InputAttributes properties, params Element[] children);
public extern static Element span(HTMLAttributes properties, params Element[] children);
}
public class HTMLAttributes
{
public string className;
}
public class InputAttributes : HTMLAttributes
{
public Action<FormEvent<InputEventTarget>> onChange;
public string value;
}
[Js(Extern = true)]
public class FormEvent<T> : FormEvent where T : EventTarget
{
[Js(Name = "target")]
public new T target;
}
[Js(Extern = true)]
public class FormEvent : SyntheticEvent
{
[Js(Name = "target")]
public EventTarget target;
}
[Js(Extern = true)]
public class SyntheticEvent
{
public bool bubbles;
public bool cancelable;
public bool defaultPrevented;
public Action preventDefault;
public Action stopPropagation;
public string type;
}
[Js(Extern = true)]
public class InputEventTarget : EventTarget
{
public string value;
}
[Js(Extern = true)]
public class EventTarget { }
Note that some of these types are external and some aren't. The simple rule is that if they must be explicitly created by C# code then they are not external, and if they are only received by C# code then the are external. To illustrate:
DOM.input(new InputAttributes {
className = "message",
onChange = ev => Global.console.log("Input.onChange: " + ev.target.value)
})
The "InputAttributes" class is explicitly instantiated by this C# code and so "InputAttributes" must not be an external class, it requires that there be an "InputAttributes" class generated as JavaScript for use at runtime. In the "onChange" callback, "ev" is an instance of "FormEvent
The reasoning behind the "FormEvent<T>" / "FormEvent" / "SyntheticEvent" hierarchy is that I thought it would make sense to try to imitate the TypeScript React definitions, and they use types with these names.
However, one of things that I dislike about the TypeScript version is that the typing could be even stronger. For example, in the "onChange" callback from a React input element in TypeScript, the "ev.target" has the non-specific type of "EventTarget" and so you have to skirt around the type system to access to input's value property:
// TypeScript
React.DOM.input({
className = "message",
onChange = ev => console.log("Input.onChange: " + (<any>ev.target).value)
})
With the DuoCode bindings above, the "ev.target" reference in the "onChange" callback is known to be an "InputEventTarget" and so the "value" property is explicitly declared as a string. This is significant improvement, I feel!
When I was first investigating all this, I encountered a problem with trying to have a "Props" class nested within the "ExampleComponent" class - it seemed to be a very specific combination of a class (Props) nested within another class (ExampleComponent) that was a non-generic specialisation of a generic base class (ComponentWrapper<TProps>). It's perfectly valid C# and there were no compiler / translation errors, but it would fail at runtime. I found that if I went to "Managed NuGet package" for my project and added the "DuoCode Compiler" package (selecting "Include Prelease", rather than "Stable Only", since DuoCode is still in beta) then a new version of the compiler was installed (0.6.1253.0 instead of 0.3.878.0). This gave me a confusing compile error that was cleared by closing the solution and opening it again. But rebuilding it then resulted in the problem going away, so clearly this is something that was fixed in the compiler at some point.
I just tried to recreate this earlier in order to get the precise error, but it seems that creating a new DuoCode project now gets the newer version of the compiler immediately, so I was unable to reproduce. It's possible that this was an artifact of an earlier installation of DuoCode that wasn't cleared properly - thinking about it now, it was a different PC where I had the problem, so this seems very feasible. I do kinda wish there was a more detailed changelog available for DuoCode - maybe they will be freer with details once they decide upon their licensing model!
One other oddity is that when I go to add a new C# class to a project, I have two options for "Class" - one is described as being "An empty class declaration" and one as an "An empty class definition". I'm not sure why I've got two, it may well be another leftover from something I've installed in the past. The annoying thing, though, is that one of them always tries to add the "System" reference to the project. When this happens, the project will no longer build since
Referenced assembly 'System' is not compatible with DuoCode
This error is entirely reasonable and descriptive, DuoCode translates C# into JavaScript but can't translate just any arbitrary .net binary. And the System library is re-implemented in the DuoCode "mscorlib" which does get translated into JavaScript. All you have to do, if you suffer this problem too, is remove the System reference from the project - and try to remember to use the "correct" C# class option next time! :)
If all of this sounds interesting to you, but you don't want to go through the hard work of piecing together the various code snippets in this article, then check out my sample ReactDuoCode Bitbucket project. It's a Visual Studio 2013 solution (the DuoCode site mentions Visual Studio 2015 in a few places but it's not a requirement).
Not only does it demonstrate using React with DuoCode but it also illustrates how you could use the Flux architecture! There's an implementation of a Dispatcher (which was incredibly easy using C# events, which are translated perfectly by DuoCode) and some actions implemented as C# classes. Then all changes to state are handled in a one-way-message-passing arrangement. There's a reason that this is the architecture recommended by Facebook for use with React; because it's awesome! :D I've not seen any other framework, library or approach which really makes such a concerted effort to manage state as this - it's why I've been going on about immutability all these years, since it's a way to keep the accidental complexity down since the essential complexity is hard enough work on its own. And now there's a well-used and well-supported UI framework based around this gaining real traction! Being able to use it with the C# language and with Visual Studio for the tooling just might make it the perfect combination.
But maybe I'm getting a bit carried away.. why don't you give it a try and let me know what you think :)
Posted at 11:42
5 May 2015
For a while now, I've been meaning to try writing a TypeScript extension for Adobe Brackets - I like the editor, I like the fact that extensions are written in JavaScript, I like TypeScript; it seemed like an ideal combination!
But to really crank it up, I wanted to see if I could put Visual Studio aside for a while (my preferred editor for writing TypeScript) and trying writing the extension for Brackets with Brackets. I'd written an extension before and I was sure that I'd heard about some sort of extension for Brackets to support TypeScript, so I got stuck in..
The short answer is that this is possible. The slightly longer answer is that it's possible but with a bit of work and the process is a bit rough around the edges.
What I'm using for editing is the extension brackets-typescript, which appears as "Brackets TypeScript" when you search for "TypeScript" in the Extension Manager. It's written by fdecampredon (whose work I also relied upon last year for "Writing React components in TypeScript" - a busy guy!).
This is the best extension for TypeScript but the TypeScript version is out of date in the released version of the extension - it doesn't yet use 1.4 and so some nice features such as union types and const enums are not available. The GitHub code has been updated to use 1.4.1, but that version of the extension has not been released yet. I contacted the author and he said that he intends to continue work on the extension soon but he's been sidelined with a pull request for the TypeScript Team to handle React's JSX format (see JSX Support #2673 - like I said, he's a busy guy :)
I tried cloning the repo and building it myself, but one of the npm dependencies ("typescript-project-services") is not available and I gave up.
So, for now, I'm having to live with an old version of the TypeScript compiler for editing purposes. I've been unable to determine precisely what version is being used, I tried looking through the source code but couldn't track it down. I suspect it's 0.9 or 1.0 since it supports generics but not the changes listed for 1.1 in the TypeScript Breaking Changes documentation.
Another gotcha with this extension is that it does not appear to work correctly if you directly open a single TypeScript file. Occasionally it will appear to work but the majority of the time you will not get any intellisense or other features, even if you have the expected ".brackets.json" file (see below) alongside the file or in a parent folder. The way that you can get this to work is to decide where the base folder for your work is going to be, to put the ".brackets.json" file in there and then to open that folder in Brackets. Then you can add / open individual files within that folder as required and the TypeScript integration will work. I couldn't find this documented or described anywhere, and came to this conclusion through trial-and-error*.
* Maybe this is the common workflow for people who use Brackets a lot; maybe I'm the strange one that goes around opening individual files ad hoc all the time..?
The other thing you need is a ".brackets.json" file alongside your source to specify some configuration for the extension.
If you're creating an extension of your own, I would recommend a basic folder structure of
/build
/src
where the TypeScript files live within "src". And so "src" is the folder that would be opened within Brackets while writing the extension, and is also the folder in which to place the following ".brackets.json" file:
{
"typescript": {
"target": "ES5",
"module": "amd",
"noImplicitAny": true,
"sources" : [
"**/*.d.ts",
"**/*.ts"
]
}
}
For a Brackets extension, supporting ES5 (rather than ES3) and using the "AMD" module loading mechanism make sense (and are consistent with the environment that Brackets extensions operate in). Setting "noImplicitAny" to "true" is a matter of taste, but I think that the "any" concept in TypeScript should always be explicitly opted into since you're sacrificing compiler safety, which you should only do intentionally.
So now we can start writing TypeScript in Brackets! But we are far from done..
The next problem is that there don't appear to be any TypeScript definitions available for writing Brackets extensions.
What I particularly want to do with my extension is write a linter for less stylesheets. In order to do this, I need to do something such as:
var AppInit = brackets.getModule("utils/AppInit"),
CodeInspection = brackets.getModule("language/CodeInspection");
function getBrokenRuleDetails(text: string, fullPath: string) {
var errors = [{
pos: { line: 4, ch: 0 },
message: "Example error on line 5",
type: CodeInspection.Type.ERROR
}];
return { errors: errors }
}
AppInit.appReady(() => {
CodeInspection.register(
"less",
{ name: "Example Linting Results", scanFile: getBrokenRuleDetails }
);
});
This means that TypeScript needs to know that there is a module "brackets" available at runtime and that it has a module-loading mechanism based upon strings identifiers (such as "utils/AppInit" and "language/CodeInspection"). For this, a "brackets.d.ts" needs to be created in the "src" folder (for more details than I'm going to cover here, see my post from earlier in year: Simple TypeScript type definitions for AMD modules).
Conveniently, TypeScript has the ability to "Overload on Constants", which means that a method can be specified with different return types for known constants for argument(s). This is an unusual feature (I can't immediately think of another statically-typed language that supports this; C# definitely doesn't, for example). The reason that it exists in TypeScript is interoperability with JavaScript. The example from the linked article is:
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: 'canvas'): HTMLCanvasElement;
createElement(tagName: 'div'): HTMLDivElement;
createElement(tagName: 'span'): HTMLSpanElement;
// + 100 more
}
This means that "Document.createElement" is known to return different types based upon the "tagName" value. It's clear how it is useful for "createElement" (since different node types are returned, based upon the tagName) and it should be clear how it will be helpful here - the "brackets.getModule" function will return different types based upon the provided module identifier.
I'm a long way from having a comprehensive type definition for Brackets' API, I've written just enough to integrate with it's linting facilities. The type definition required for that is as follows:
declare module brackets {
function getModule(name: "utils/AppInit"): AppInit;
function getModule(name: "language/CodeInspection"): CodeInspection;
function getModule(name: string): void;
interface AppInit {
appReady: (callback: () => void) => void;
}
interface CodeInspection {
register: (extension: string, lintOptions: LintOptions) => void;
Type: CodeInspectionTypeOptions
}
interface LintOptions {
name: string;
scanFile: (text: string, fullPath: string) => LintErrorSet
}
interface LintErrorSet { errors: LintErrorDetails[] }
interface LintErrorDetails {
pos: { line: number; ch: number };
message: string;
type: string
}
interface CodeInspectionTypeOptions {
WARNING: string;
ERROR: string
}
}
The "Overload on Constants" functionality has a limitation in that a method signature is required that does not rely upon a constant value, so above there is a "getModule" method that handles any unsupported module name and returns void. It would be nice if there was a way to avoid this and to only define "getModule" methods for known constants, but that is not the case and so a void-returning "catch all" variation must be provided.
There is another limitation that is unfortunate. The LintErrorDetails interface has had to be defined with a string "type" property, it would have been better if this could have been an enum. However, the constants within Brackets are within the "CodeInspection" module - eg.
CodeInspection.Type.ERROR
The "CodeInspection" reference is returned from a "getModule" call and so must be an interface or class, and an enum may not be nested within an interface or class definition. If "CodeInspection" was identified as a module then an enum could be nested in it, but then the getModule function definition would complain that
Type reference cannot refer to container 'brackets.CodeInspector'
.. which is a pity. So the workaround is to have LintErrorDetails take a string "type" property but for a non-nested enum to be exposed from "CodeInspection" that may be used for those values. So it's valid to define error instances with the following:
var errors = [{
pos: { line: 4, ch: 0 },
message: "Example error on line 5",
type: CodeInspection.Type.ERROR
}];
but unfortunately it's also valid to use nonsense string "type" values, such as:
var errors = [{
pos: { line: 4, ch: 0 },
message: "Example error on line 5",
type: "BlahBlahBlah"
}];
So, at this point, we can actually start writing a linter extension in TypeScript. However, the Brackets TypeScript extension doesn't support compiling this to JavaScript. So we can write as much as we like, it's not going to be very useful!
This is another to-do item for the Brackets TypeScript extension (according to a discussion on CodePlex) and so hopefully the following will not be required forever. However, right now, some extra work is needed..
The go-to solution for compiling TypeScript seems to be to use Grunt and grunt-ts.
If you have npm installed then this is fairly easy. However there are - again - some gotchas. In the "grunt-ts" readme, it says you can install it using
npm install grunt-ts
"in your project directory". I would recommend that this "project directory" be the root where the "src" and "build" folders that I suggested live. However, when I tried this, it created the "grunt-ts" folder in a "node_modules" folder in a parent a couple of levels up from the current directory! Probably I'd done something silly with npm. But a way to avoid this is to not specify npm packages individually at the command line and to instead create a "package.json" file in your project root (again, I'm referring to the folder that contains the "src" and "build" folders) - eg.
{
"name": "example.less-linter",
"title": "Example LESS Linter",
"description": "Extension for linting LESS stylesheets",
"version": "0.1.0",
"engines": {
"brackets": ">=0.40.0"
},
"devDependencies": {
"grunt-ts": ">= 4.0.1",
"grunt-contrib-watch": ">= 0.6.1",
"grunt-contrib-copy": ">= 0.8.0"
}
}
This will allow you to run
npm install
from the project folder and have it pull in everything you'll need into the appropriate locations.
The plan is to configure things such that any TypeScript (or TypeScript definition) file change will result in them all being re-compiled and then the JavaScript files copied into the "build" folder, along with this package.json file. That way, the "build" folder can be zipped up and distributed (or dropped into Bracket's "extensions" folder for immediate testing).
Here's the "gruntfile.js" that I use (this needs to be present in the project root, alongside the "package.json" file and "src" / "build" folders) -
/*global module */
module.exports = function (grunt) {
"use strict";
grunt.initConfig({
ts: {
"default": {
src: ["src/**/*.d.ts", "src/**/*.ts"]
},
options: {
module: "amd",
target: "es5",
sourceMap: true,
noImplicitAny: true,
fast: "never"
}
},
copy: {
main: {
files: [
{ expand: true, cwd: "src/", src: ["**.js"], dest: "build/" },
{ src: ["package.json"], dest: "build/" }
]
}
},
watch: {
scripts: {
files: ["src/**/*.d.ts", "src/**/*.ts"],
tasks: ["ts", "copy"],
options: { spawn: false }
}
}
});
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-ts");
grunt.registerTask("default", ["ts", "copy", "watch"]);
};
There is some repeating of configuration (such as "es5" and "amd" TypeScript options) since this does not share any configuration with the Brackets TypeScript extension. The idea is that you fire up Brackets and open the "src" folder of the extension that you're writing. Then open up a command prompt and navigate to the project directory root and execute Grunt. This will compile your current TypeScript files and copy the resulting JavaScript from "src" into "build", then it will wait until any of the .ts
(or .d.ts
) files within the "src" folder are changed and repeat the build & copy process.
It's worth noting that grunt-ts has some file-watching logic built into it, but if you want the source and destination folders to be different then it uses a hack where it injects a .basedir.ts
file into the source, resulting in a .basedir.js
in the destination - which I didn't like. It also doesn't support additional actions such as copying the "package.json" from the root into the "build" folder. The readme for grunt-ts recommends using grunt-contrib-watch for more complicated watch configurations, so that's what I've done.
One other issue I had with grunt-ts was with its "fast compile" option. This would always work the first time, but subsequent compilations would seem to lose the "brackets.d.ts" file and so claim that "brackets" was not a known module. This was annoying but easy to fix - the gruntfile.js above sets ts.options.fast
to "never". This may mean that the compilation step will be a bit slower, but unless you're extension is enormous then this shouldn't be an issue.
And with that, we're basically done! We can write TypeScript against the Brackets API (granted, if you want to use more functions in the API than I've defined then you'll have to get your hands dirty with the "brackets.d.ts" file) and this code can be compiled into JavaScript and copied into a "build" folder along with the package definition.
The only other thing I'd say is that I found the "smart indenting" in Brackets to be appalling with TypeScript - it moves things all over the place as you go from one line to another! It's easily disabled, though, thankfully. There's a configuration file that needs editing - see the comment by "rkn" in Small little Adobe Brackets tweak – remove Smart Indent. Once you've done this, you don't need to restart Brackets; it will take effect immediately.
And now we really are done! Happy TypeScript Brackets Extension writing! Hopefully I'll have my first TypeScript extension ready to release in an early state soon.. :)
(For convenience junkies, I've created a Bitbucket repo with everything that you need; the "Example TypeScript Brackets Extension").
Posted at 22:50
Dan is a big geek who likes making stuff with computers! He can be quite outspoken so clearly needs a blog :)
In the last few minutes he seems to have taken to referring to himself in the third person. He's quite enjoying it.