I wrote a while ago about how I was using some different tools in a current project:
- React with JSX
- ES6 with Babel
- Karma for unit testing
I have fully come to love and appreciate all of the above. I really like working with them. However. There was still an ache in my soul and a thorn in my side. Whilst I love the syntax of ES6 and even though I've come to appreciate the clarity of JSX, I have been missing something. Perhaps you can guess? It's static typing.
It's actually been really good to have the chance to work without it because it's made me realise what a productivity boost having static typing actually is. The number of silly mistakes burning time that a compiler could have told me.... Sigh.
But the pain is over. The dark days are gone. It's possible to have strong typing, courtesy of TypeScript, plugged into this workflow. It's yours for the taking. Take it. Take it now!
I decided a couple of months ago what I wanted to have in my setup:
- I want to be able to write React / JSX in TypeScript. Naturally I couldn't achieve that by myself but handily the TypeScript team decided to add support for JSX with TypeScript 1.6. Ooh yeah.
- I wanted to be able to write ES6. When I realised the approach for writing ES6 and having the transpilation handled by TypeScript wasn't clear I had another idea. I thought "what if I write ES6 and hand off the transpilation to Babel?" i.e. Use TypeScript for type checking, not for transpilation. I realised that James Brantly had my back here already. Enter Webpack and ts-loader.
- Debugging. Being able to debug my code is non-negotiable for me. If I can't debug it I'm less productive. (I'm also bitter and twisted inside.) I should say that I wanted to be able to debug my original source code. Thanks to the magic of sourcemaps, that mad thing is possible.
- Karma for unit testing. I've become accustomed to writing my tests in ES6 and running them on a continual basis with Karma. This allows for a rather good debugging story as well. I didn't want to lose this when I moved to TypeScript. I didn't.
So I've talked about what I want and I've alluded to some of the solutions that there are. The question now is how to bring them all together. This post is, for the most part, going to be about correctly orchestrating a number of gulp tasks to achieve the goals listed above. If you're after the Blue Peter "here's one I made earlier" moment then take a look at the es6-babel-react-flux-karma repo in the Microsoft/TypeScriptSamples repo on Github.
Let's start picking this apart; what do we actually have here? Well, we have 2 gulp tasks that I want you to notice:
This is likely the task you would use when deploying. It takes all of your source code, builds it, provides cache-busting filenames (eg
main.dd2fa20cd9eac9d1fb2f.js), injects your shell SPA page with references to the files and deploys everything to the
./dist/directory. So that's TypeScript, static assets like images and CSS all made ready for Production.
The build task also implements this advice:
When deploying your app, set the
NODE_ENVenvironment variable to
productionto use the production build of React which does not include the development warnings and runs significantly faster.
This task represents "development mode" or "debug mode". It's what you'll likely be running as you develop your app. It does the same as the build task but with some important distinctions.
- As well as building your source it also runs your tests using Karma
- This task is not triggered on a once-only basis, rather your files are watched and each tweak of a file will result in a new build and a fresh run of your tests. Nice eh?
- It spins up a simple web server and serves up the contents of
./dist(i.e. your built code) in order that you can easily test out your app.
- In addition, whilst it builds your source it does not minify your code and it emits sourcemaps. For why? For debugging! You can go to http://localhost:8080/ in your browser of choice, fire up the dev tools and you're off to the races; debugging like gangbusters. It also doesn't bother to provide cache-busting filenames as Chrome dev tools are smart enough to not cache localhost.
- Oh and Karma.... If you've got problems with a failing test then head to http://localhost:9876/ and you can debug the tests in your dev tools.
- Finally, it runs ESLint in the console. Not all of my files are TypeScript; essentially the build process (aka "gulp-y") files are all vanilla JS. So they're easily breakable. ESLint is there to provide a little reassurance on that front.
Now let's dig into each of these in a little more detail
Let's take a look at what's happening under the covers of
WebPack with ts-loader and babel-loader is what we're using to compile our ES6 TypeScript. ts-loader uses the TypeScript compiler to, um, compile TypeScript and emit ES6 code. This is then passed on to the babel-loader which transpiles it from ES6 down to ES-old-school. It all gets brought together in 2 files;
main.js which contains the compiled result of the code written by us and
vendor.js which contains the compiled result of 3rd party / vendor files. The reason for this separation is that vendor files are likely to change fairly rarely whilst our own code will constantly be changing. This separation allows for quicker compile times upon file changes as, for the most part, the vendor files will not need to included in this process.
gulpfile.js above uses the following task:
Hopefully this is fairly self-explanatory; essentially
buildDevelopment performs the development build (providing sourcemap support) and
buildProduction builds for Production (providing minification support). Both are driven by this
Your compiled output needs to be referenced from some kind of HTML page. So we've got this:
Which is no more than a boilerplate HTML page with a couple of key features:
- a single
<div />element in the
<body />which is where our React app is going to be rendered.
<!-- inject:css -->and
<!-- inject:js -->placeholders where css and js is going to be injected by
- a single
<link />to the Bootstrap CDN. This sample app doesn't actually serve up any css generated as part of the project. It could but it doesn't. When it comes to injection time no css will actually be injected. This has been left in place as, more typically, a project would have some styling served up.
This is fed into our inject task in
This also triggers the server to serve up the new content.
Your app will likely rely on a number of static assets; images, fonts and whatnot. This script picks up the static assets you've defined and places them in the
dist folder ready for use:
Finally, we're ready to get our tests set up to run continually with Karma.
tests.watch() triggers the following task:
When running in watch mode it's possible to debug the tests by going to:
<a href="http://localhost:9876/">http://localhost:9876/</a>. It's also possible to run the tests standalone with a simple
npm run test. Running them like this also outputs the results to an XML file in JUnit format; this can be useful for integrating into CI solutions that don't natively pick up test results.
Whichever approach we use for running tests, we use the following
karma.conf.js file to configure Karma:
As you can see, we're still using our webpack configuration from earlier to configure much of how the transpilation takes place.
And that's it; we have a workflow for developing in TypeScript using React with tests running in an automated fashion. I appreciated this has been a rather long blog post but I hope I've clarified somewhat how this all plugs together and works. Do leave a comment if you think I've missed something.
This post has actually been sat waiting to be published for some time. I'd got this solution up and running with Babel 5. Then they shipped Babel 6 and (as is the way with "breaking changes") broke sourcemap support and thus torpedoed this workflow. Happily that's now been resolved. But if you should experience any wonkiness - it's worth checking that you're using the latest and greatest of Babel 6.