Skip to main content

14 posts tagged with "ts-loader"

View All Tags

ts-loader goes webpack 5

ts-loader has just released v9.0.0. This post goes through what this release is all about, and what it took to ship this version. For intrigue, it includes a brief scamper into my mental health along the way. Some upgrades go smoothly - this one had some hiccups. But we'll get into that.

hello world bicep

One big pull request#

As of v8, ts-loader supported webpack 4 and webpack 5. However the webpack 5 support was best efforts, and not protected by any automated tests. ts-loader has two test packs:

  1. A comparison test pack that compares transpilation and webpack compilation output with known outputs.
  2. An execution test pack that executes Karma test packs written in TypeScript using ts-loader.

The test packs were tightly coupled to webpack 4 (and in the case of the comparison test pack, that's unavoidable). The mission was to port ts-loader to be built against (and have an automated test pack that ran against) webpack 5.

This ended up being a very big pull request. Work on it started back in February 2021 and we're shipping now in April of 2021. I'd initially expected it would take a couple of days at most. I had underestimated.

A number of people collaborated on this PR, either with code, feedback, testing or even just responding to questions. So I'd like to say thank you to:

What's changed#

Let's go through what's different in v9. There's two breaking changes:

  • The minimum webpack version supported is now webpack 5. This simplifies the codebase, which previously had to if/else the various API registrations based on the version of webpack being used.
  • The minimum node version supported is now node 12. Node 10 reaches end of life status at the end of April 2021.

An interesting aspect of migrating to building against webpack 5 was dropping the dependency upon @types/webpack in favour of the types that now ship with webpack 5 itself. This was a mostly great experience; however we discovered some missing pieces.

Most notably, the LoaderContext wasn't strongly typed. LoaderContext is the value of this in the context of a running loader function. So it is probably the most interesting and important type from the perspective of a loader author.

Historically we used our own definition which had been adapted from the one in @types/webpack. I've looked into the possibility of a type being exposed in webpack itself. However, it turns out, it's complicated - with the LoaderContext type being effectively created across two packages. The type is initially created in webpack and then augmented later in loader-runner, prior to being supplied to loaders. You can read more on that here.

For now we've opted to stick with keeping an interface in ts-loader that models what arrives in the loader when executed. We have freshened it up somewhat, to model the webpack 5 world.

Alongside these changes, a number of dependencies were upgraded.

The hole#

By the 19th of February most of the work was done. However, we were experiencing different behaviour between Linux and Windows in our comparison test pack.

As far as I was aware, we were doing all the appropriate work to ensure ts-loader and our test packs worked cross platform. But we were still experiencing problems whenever we ran the test pack on Windows. I'd done no end of tweaking but nothing worked. I couldn't explain it. I couldn't fix it. I was finding that tough to deal with.

I really want to be transparent about the warts and all aspect of open source software development. It is like all other types of software development; sometimes things go wrong and it can be tough to work out why. Right then, I was really quite unhappy. Things weren't working code-wise and I was at a loss to say why. This is not something that I dig.

I also wasn't sleeping amazingly at this point. It was winter and we'd been in lockdown in the UK for three months; as the COVID-19 pandemic ground relentlessly on. I love my family dearly. I really do. With that said, having my children around whilst I attempted to work was remarkably tough. I love those guys but, woah, was it stressful.

I was feeling at a low ebb. And I wasn't sure what to do next. So, feeling tired and pretty fed up, I took a break.

"Anybody down there?"#

Time passed. In March Alexander Akait checked in to see how things were going and volunteered to help. He also suggested what turned out to be the fix; namely replacing usage of '\' with '/' in the assets supplied back to webpack. But crucially I implemented this wrong. Observe this commit:

const assetPath = path
.relative(compilation.compiler.outputPath, outputFile.name)
// According to @alexander-akait we should always '/' https://github.com/TypeStrong/ts-loader/pull/1251#issuecomment-799606985
.replace(/\//g, '/');

If you look closely at the replace you'll see that I'm globally replacing '/' with '/' rather than globally replacing '\' with '/'. The wasted time this caused... I could weep.

I generally thrashed around for a bit after this. Going in circles, like a six year old swimming wearing one armband. Then Tobias kindly volunteered to help. This much I've learned from a career in software: if talented people offer their assistance, grab it with both hands!

I'd been trying be as "learn in public" as possible about the issues I was facing on the pull request. The idea being, to surface the problems in a public forum where others can read and advise. And also to attempt a textual kind of rubber duck debugging.

When Tobias pitched in, I wanted to make it as easy as possible for him to help. So I wrote up a full description of what had changed. What the divergent behaviour in test packs looked like. I shared my speculation for what might be causing the issue (I was wrong by the way). Finally I provided a simple way to get up and running with the broken code. The easier I could make it for others to collaborate on this, I figured, the greater the likelihood of an answer. Tobias got to an answer quickly:

The problem is introduced due to some normalization logic in the test case: see #1273

While the PR fixes the problem, I think the paths should be normalized earlier in the pipeline to make this normalization code unnecessary. Note that asset names should have only / as they are filenames and not paths. Only absolute paths have \.

Tobias had raised a PR which introduced a workaround to resolved things in the test pack. This made me happy. More than that, he also identified that the issue lay in ts-loader itself. This caused me to look again at the changes I'd made, including my replace addition. With fresh eyes, I now realised this was a bug, and fixed it.

I found then that I could revert Tobias' workaround and still have passing tests. Result!

Release details#

Now that we've got there; we've shipped. You can get the latest version of ts-loader on npm and you can find the release details on GitHub.

Thanks everyone - I couldn't have done it without your help. 馃尰鉂わ笍

Create React App with ts-loader and CRACO

Create React App is a fantastic way to get up and running building a web app with React. It also supports using TypeScript with React. Simply entering the following:

npx create-react-app my-app --template typescript

Will give you a great TypeScript React project to get building with. There's two parts to the TypeScript support that exist:

  1. Transpilation AKA "turning our TypeScript into JavaScript". Back since Babel 7 launched, Babel has enjoyed great support for transpiling TypeScript into JavaScript. Create React App leverages this; using the Babel webpack loader, babel-loader, for transpilation.
  2. Type checking AKA "seeing if our code compiles". Create React App uses the fork-ts-checker-webpack-plugin to run the TypeScript type checker on a separate process and report any issues that may exist.

This is a great setup and works very well for the majority of use cases. However, what if we'd like to tweak this setup? What if we'd like to swap out babel-loader for ts-loader for compilation purposes? Can we do that?

Yes you can! And that's what we're going to do using a tool named CRACO - the pithy shortening of "Create React App Configuration Override". This is a tool that allows us to:

Get all the benefits of create-react-app and customization without using 'eject' by adding a single craco.config.js file at the root of your application and customize your eslint, babel, postcss configurations and many more.

babel-loader ts-loader#

So let's do the swap. First of all we're going to need to add CRACO and ts-loader to our project:

npm install @craco/craco ts-loader --save-dev

Then we'll swap over our various scripts in our package.json to use CRACO:

"start": "craco start",
"build": "craco build",
"test": "craco test",

Finally we'll add a craco.config.js file to the root of our project. This is where we swap out babel-loader for ts-loader:

const { addAfterLoader, removeLoaders, loaderByName, getLoaders, throwUnexpectedConfigError } = require('@craco/craco');
const throwError = (message) =>
throwUnexpectedConfigError({
packageName: 'craco',
githubRepo: 'gsoft-inc/craco',
message,
githubIssueQuery: 'webpack',
});
module.exports = {
webpack: {
configure: (webpackConfig, { paths }) => {
const { hasFoundAny, matches } = getLoaders(webpackConfig, loaderByName('babel-loader'));
if (!hasFoundAny) throwError('failed to find babel-loader');
console.log('removing babel-loader');
const { hasRemovedAny, removedCount } = removeLoaders(webpackConfig, loaderByName('babel-loader'));
if (!hasRemovedAny) throwError('no babel-loader to remove');
if (removedCount !== 2) throwError('had expected to remove 2 babel loader instances');
console.log('adding ts-loader');
const tsLoader = {
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('ts-loader'),
options: { transpileOnly: true },
};
const { isAdded: tsLoaderIsAdded } = addAfterLoader(webpackConfig, loaderByName('url-loader'), tsLoader);
if (!tsLoaderIsAdded) throwError('failed to add ts-loader');
console.log('added ts-loader');
console.log('adding non-application JS babel-loader back');
const { isAdded: babelLoaderIsAdded } = addAfterLoader(
webpackConfig,
loaderByName('ts-loader'),
matches[1].loader // babel-loader
);
if (!babelLoaderIsAdded) throwError('failed to add back babel-loader for non-application JS');
console.log('added non-application JS babel-loader back');
return webpackConfig;
},
},
};

So what's happening here? The script looks for babel-loader usages in the default Create React App config. There will be two; one for TypeScript / JavaScript application code (we want to replace this) and one for non application JavaScript code. I'm actually not too clear what non application JavaScript code there is or can be, but we'll leave it in place; it may be important.

You cannot remove a single loader using CRACO, so instead we'll remove both and we'll add back the non application JavaScript babel-loader. We'll also add ts-loader with the transpileOnly: true option set (to ensure ts-loader doesn't do type checking).

Now the next time we run npm start we'll have Create React App running using ts-loader and without having ejected. If we want to adjust the options of ts-loader further then we're completely at liberty to do so, adjusting the options in our craco.config.js.

If you value debugging your original source code rather than the transpiled JavaScript, remember to set the "sourceMap": true property in your tsconfig.json.

Finally, if we wanted to go even further, we could remove the fork-ts-checker-webpack-plugin and move ts-loader to use transpileOnly: false so it performs type checking also. However, generally it may be better to stay with the setup with post outlines for performance reasons.

The Big One Point Oh

It's time for the first major version of fork-ts-checker-webpack-plugin. It's been a long time coming :-)

A Little History#

The fork-ts-checker-webpack-plugin was originally the handiwork of Piotr Ole艣. He raised an issue with ts-loader suggesting it could be the McCartney to ts-loader's Lennon:

Hi everyone!

I've created webpack plugin: fork-ts-checker-webpack-plugin that plays nicely with ts-loader. The idea is to compile project with transpileOnly: true and check types on separate process (async). With this approach, webpack build is not blocked by type checker and we have semantic check with fast incremental build. More info on github repo :)

So if you like it and you think it would be good to add some info in README.md about this plugin, I would be greatful.

Thanks :)

We did like it. We did think it would be good. We took him up on his kind offer.

Since that time many people have had their paws on the fork-ts-checker-webpack-plugin codebase. We love them all.

One Point Oh#

We could have had our first major release a long time ago. The idea first occurred when webpack 5 alpha appeared. "Huh, look at that, a major version number.... Maybe we should do that?" "Great idea chap - do it!" So here it is; fresh out the box: v1.0.0

There are actually no breaking changes that we're aware of; users of 0.x fork-ts-checker-webpack-plugin should be be able to upgrade without any drama.

Incremental Watch API on by Default#

Users of TypeScript 3+ may notice a performance improvement as by default the plugin now uses the incremental watch API in TypeScript.

Should this prove problematic you can opt out of using it by supplying useTypescriptIncrementalApi: false. We are aware of an issue with Vue and the incremental API. We hope it will be fixed soon - a generous member of the community is taking a look. In the meantime, we will not default to using the incremental watch API when in Vue mode.

Compatibility#

As it stands, the plugin supports webpack 2, 3, 4 and 5 alpha. It is compatible with TypeScript 2.1+ and TSLint 4+.

Right that's it - enjoy it! And thanks everyone for contributing - we really dig your help. Much love.

You Might Not Need thread-loader

It all started with a GitHub issue. Ernst Ammann reported:

Without the thread-loader, compilation takes three to four times less time on changes. We could remove it.

If you're not aware of the webpack-config-plugins project then I commend it to you. Famously, webpack configuration can prove tricky. webpack-config-plugins borrows the idea of presets from Babel. It provides a number of pluggable webpack configurations which give a best practice setup for different webpack use cases. So if you're no expert with webpack and you want a good setup for building your TypeScript / Sass / JavaScript then webpack-config-plugins has got your back.

One of the people behind the project is the very excellent Jan Nicklas who is well known for his work on the html-webpack-plugin.

It was Jan who responded to Ernst's issue and decided to look into it.

All I Want For Christmas is Faster Builds#

Everyone wants fast builds. I do. You do. We all do. webpack-config-plugins is about giving these to the user in a precooked package.

There's a webpack loader called thread-loader which spawns multiple processes and splits up work between them. It was originally inspired by the work in the happypack project which does a similar thing.

I wrote a blog post some time ago which gave details about ways to speed up your TypeScript builds by combining the ts-loader project (which I manage) with the fork-ts-checker-webpack-plugin project (which I'm heavily involved with).

That post was written back in the days of webpack 2 / 3. It advocated use of both happypack / thread-loader to drop your build times even further. As you'll see, now that we're well into the world of webpack 4 (with webpack 5 waiting in the wings) the advantage of happypack / thread-loader are no longer so profound.

webpack-config-plugins follows the advice I set out in my post; it uses thread-loader in its pluggable configurations. Now, back to Ernst's issue.

thread-loader: Infinity War#

Jan quickly identified the problem. He did that rarest of things; he read the documentation which said:

// timeout for killing the worker processes when idle
// defaults to 500 (ms)
// can be set to Infinity for watching builds to keep workers alive
poolTimeout: 2000,

The webpack-config-plugins configurations (running in watch mode) were subject to the thread loaders being killed after 500ms. They got resurrected when they were next needed; but that's not as instant as you might hope. Jan then did a test:

(default pool - 30 runs - 1000 components ) average: 2.668068965517241
(no thread-loader - 30 runs - 1000 components ) average: 1.2674137931034484
(Infinity pool - 30 runs - 1000 components ) average: 1.371827586206896

This demonstrates that using thread-loader in watch mode with poolTimeout: Infinity performs significantly better than when it defaults to 500ms. But perhaps more significantly, not using thread-loader performs even better still.

"Maybe You've Thread Enough"#

When I tested using thread-loader in watch mode with poolTimeout: Infinity on my own builds I got the same benefit Jan had. I also got even more benefit from dropping thread-loader entirely.

A likely reason for this benefit is that typically when you're developing, you're working on one file at a time. Hence you only transpile one file at a time:

So there's not a great deal of value that thread-loader can add here; mostly it's twiddling thumbs and adding an overhead. To quote the docs:

Each worker is a separate node.js process, which has an overhead of ~600ms. There is also an overhead of inter-process communication.

Use this loader only for expensive operations!

Now, my build is not your build. I can't guarantee that you'll get the same results as Jan and I experienced; but I would encourage you to investigate if you're using thread-loader correctly and whether it's actually helping you. In these days of webpack 4+ perhaps it isn't.

There are still scenarios where thread-loader still provides an advantage. It can speed up production builds. It can speed up the initial startup of watch mode. In fact Jan has subsequently actually improved the thread-loader to that specific end. Yay Jan!

If this is all too much for you, and you want to hand off the concern to someone else then perhaps all of this serves as a motivation to just sit back, put your feet up and start using webpack-config-plugins instead of doing your own configuration.

ts-loader Project References: First Blood

So project references eh? They shipped with TypeScript 3. We've just shipped initial support for project references in ts-loader v5.2.0. All the hard work was done by the amazing Andrew Branch. In fact I'd recommend taking a gander at the PR. Yay Andrew!

This post will take us through the nature of the support for project references in ts-loader now and what we hope the future will bring. It rips off shamelessly

borrows from the README.md documentation that Andrew wrote as part of the PR. Because I am not above stealing.

TL;DR#

Using project references currently requires building referenced projects outside of ts-loader. We don鈥檛 want to keep it that way, but we鈥檙e releasing what we鈥檝e got now. To try it out, you鈥檒l need to pass projectReferences: true to loaderOptions.

Like tsc, but not like tsc --build#

ts-loader has partial support for project references in that it will load dependent composite projects that are already built, but will not currently build/rebuild those upstream projects. The best way to explain exactly what this means is through an example. Say you have a project with a project reference pointing to the lib/ directory:

tsconfig.json
app.ts
lib/
tsconfig.json
niftyUtil.ts

And we鈥檒l assume that the root tsconfig.json has { "references": { "path": "lib" } }, which means that any import of a file that鈥檚 part of the lib sub-project is treated as a reference to another project, not just a reference to a TypeScript file. Before discussing how ts-loader handles this, it鈥檚 helpful to review at a really basic level what tsc itself does here. If you were to run tsc on this tiny example project, the build would fail with the error:

error TS6305: Output file 'lib/niftyUtil.d.ts' has not been built from source file 'lib/niftyUtil.ts'.

Using project references actually instructs tscnot to build anything that鈥檚 part of another project from source, but rather to look for any .d.ts and .js files that have already been generated from a previous build. Since we鈥檝e never built the project in lib before, those files don鈥檛 exist, so building the root project fails. Still just thinking about how tsc works, there are two options to make the build succeed: either run tsc -p lib/tsconfig.jsonfirst, or simply run tsc --build, which will figure out that lib hasn鈥檛 been built and build it first for you.

Ok, so how is that relevant to ts-loader? Because the best way to think about what ts-loader does with project references is that it acts like tsc, but not like tsc --build. If you run ts-loader on a project that鈥檚 using project references, and any upstream project hasn鈥檛 been built, you鈥檒l get the exact same error TS6305 that you would get with tsc. If you modify a source file in an upstream project and don鈥檛 rebuild that project, ts-loader won鈥檛 have any idea that you鈥檝e changed anything鈥攊t will still be looking at the output from the last time you built that file.

鈥淗ey, don鈥檛 you think that sounds kind of useless and terrible?鈥#

Well, sort of. You can consider it a work-in-progress. It鈥檚 true that on its own, as of today, ts-loader doesn鈥檛 have everything you need to take advantage of project references in webpack. In practice, though, consuming upstream projects and building upstream projects are somewhat separate concerns. Building them will likely come in a future release. For background, see the original issue.

outDir Windows problemo.#

At the moment, composite projects built using the outDir compiler option cannot be consumed using ts-loader on Windows. If you try to, ts-loader throws a "has not been built from source file" error. You can see Andrew and I puzzling over it in the PR. We don't know why yet; it's possible there's a bug in tsc. It's more likely there's a bug in ts-loader. Hopefully it's going to get solved at some point. (Hey, maybe you're the one to solve it!) Either way, we didn't want to hold back from releasing. So if you're building on Windows then avoid building composite projects using outDir.

ts-loader 4 / fork-ts-checker-webpack-plugin 0.4

webpack 4 has shipped!

ts-loader#

The ts-loader 4 is available too. For details see our release here. To start using ts-loader 4:

Remember to use this in concert with the webpack 4. To see a working example take a look at the "vanilla" example.

fork-ts-checker-webpack-plugin#

There's more! You may like to use the <a href="https://github.com/Realytics/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a>, (aka the ts-loader turbo-booster). The webpack compatible version has been released to npm as 0.4.1:

To see a working example take a look at the "fork-ts-checker" example.

webpack 4 - ts-loader / fork-ts-checker-webpack-plugin betas

The first webpack 4 beta dropped on Friday. Very exciting! Following hot on the heels of those announcements, I've some news to share too. Can you guess what it is?

ts-loader#

Yes! The ts-loader beta to work with webpack 4 is available. To get hold of the beta:

Remember to use this in concert with the webpack 4 beta. To see a working example take a look at the "vanilla" example.

fork-ts-checker-webpack-plugin#

There's more! You may like to use the <a href="https://github.com/Realytics/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a>, (which goes lovely with ts-loader and a biscuit). There is a beta available for that too:

  • When using yarn: yarn add johnnyreilly/fork-ts-checker-webpack-plugin#4.0.0-beta.1 -D
  • When using npm: npm install johnnyreilly/fork-ts-checker-webpack-plugin#4.0.0-beta.1 -D

To see a working example take a look at the "fork-ts-checker" example.

PRs#

If you would like to track the progress of these betas then I encourage you to take a look at the PRs they were built from. The ts-loader PR can be found here. The fork-ts-checker-webpack-plugin PR can be found here.

These are betas so things may change further; though hopefully not significantly.

ts-loader 2017 retrospective

2017 is drawing to a close, and it's been a big, big year in webpack-land. It's been a big year for ts-loader too. At the start of the year v1.3.3 was the latest version available, officially supporting webpack 1. (Old school!) We end the year with ts-loader sitting pretty at v3.2.0 and supporting webpack 2 and 3.

Many releases were shipped and that was down to a whole bunch of folk. People helped out with bug fixes, features, advice and docs improvements. All of these help.ts-loader wouldn't be where it is without you so thanks to everyone that helped out - you rock!

I'm really grateful to all of you. Thanks so much! (Apologies for those I've missed anyone out - I know there's more still.)

fork-ts-checker-webpack-plugin build speed improvements#

Alongside other's direct contributions to ts-loader, other projects improved the experience of using ts-loader. Piotr Ole艣 dropped his <a href="https://github.com/Realytics/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a> this year which nicely increased build speed when used with ts-loader.

That opened up the possibility of adding HappyPack support. I had the good fortune to work with webpack's Tobias Koppers and ExtraHop's Alex Birmingham on improving TypeScript build speed further.

So what does the future hold?

ts-loader 4.0 (Live webpack or Die Hard)#

The web marches on and webpack gallops alongside. Here's what's in the pipeline for ts-loader in 2018:

Start using the new watch API#

A new watch API is being made available in the TypeScript API. We have a PR from the amazing Sheetal Nandi which adds support to ts-loader. Given that's quite a big PR we want to merge that before anything else lands. The watch API is still being finalised but once it lands in TypeScript we'll look to merge the PR and ship a new version of ts-loader.

Drop custom module resolution#

Historically ts-loader has had it's own module resolution mechanism in place. We're going to look to move to use the TypeScript mechanism instead. The old module resolution be deprecated but will remain available behind a flag for a time. In future we'll look to drop the old mechanism entirely.

Drop support for TypeScript 2.3 and below#

The codebase can be made simpler if we drop support for older versions of TypeScript so that's what we plan to do with our next breaking changes release.

webpack v4 is in alpha now#

If any changes need to happen to ts-loader to support webpack 4 then they will be. Personally I'm planning to help out with <a href="https://github.com/Realytics/fork-ts-checker-webpack-plugin">fork-ts-checker-webpack-plugin</a> as there will likely be some changes required there.

contextAsConfigBasePath will be replaced with a context#

The option that landed in the last month doesn't quite achieve the aims of the original PR's author Christian Tinauer. Consequently it's going to be replaced with a new option. This is queued up and ready to go here.

reportFiles option to be added#

Michel Rasschaert is presently working on adding a reportFiles option to ts-loader. You can see the PR in progress here.

Merry Christmas!#

You can expect to see the first releases of ts-loader 4.0 in 2018. In the meantime, I'd like to wish you Merry Christmas and a Happy New Year! And once more, thanks and thanks again to all you generous people who help build ts-loader. You're wonderful and so I'm glad you do what you do... joyeux Noel!

Working with Extrahop on webpack and ts-loader

I'm quite proud of this: https://www.extrahop.com/company/blog/2017/extrahop-webpack-accelerating-build-times/

If you didn't know, I spend a good amount of my spare time hacking on open source software. You may not know what that is. I would describe OSS as software made with 鉂 by people, for other people to use.

You are currently reading this on a platform that was built using OSS. It's all around you, every day. It's on your phone, on your computer, on your TV. It's everywhere.

It's my hobby, it's part of my work. This specifically was one of those tremendously rare occasions when I got paid directly to work on my hobby, with people much brighter than me. It was brilliant. I loved it; it was a privilege.

Here's to Open Source!

fork-ts-checker-webpack-plugin code clickability

My name is John Reilly and I'm a VS Code addict. There I said it. I'm also a big fan of TypeScript and webpack. I've recently switched to using the awesome fork-ts-checker-webpack-plugin to speed up my builds.

One thing I love is using VS Code both as my editor and my terminal. Using the fork-ts-checker-webpack-plugin I noticed a problem when TypeScript errors showed up in the terminal:

Take a look at the red file location in the console above. What's probably not obvious from the above screenshot is that it is not clickable. I'm used to being able to click on link in the console and bounce straight to the error location. It's a really productive workflow; see a problem, click on it, be taken to the cause, fix it.

I want to click on "C:/source/ts-loader/examples/fork-ts-checker/src/fileWithError.ts(2,7)" and have VS Code open up fileWithError.ts, ideally at line 2 and column 7. But here it's not working. Why?

Well, I initially got this slightly wrong; I thought it was about the formatting of the file path. It is. I thought that having the line number and column number in parentheses after the path (eg "(2,7)") was screwing over VS Code. It isn't. Something else is. Look closely at the screenshot; what do you see? Do you notice how the colour of the line number / column number is different to the path? In the words of Delbert Wilkins: that's crucial.

Yup, the colour change between the path and the line number / column number is the problem. I've submitted a PR to fix this that I hope will get merged. In the meantime you can avoid this issue by dropping this code into your webpack.config.js:

var chalk = require("chalk");
var os = require("os");
function clickableFormatter(message, useColors) {
var colors = new chalk.constructor({ enabled: useColors });
var messageColor = message.isWarningSeverity() ? colors.bold.yellow : colors.bold.red;
var fileAndNumberColor = colors.bold.cyan;
var codeColor = colors.grey;
return [
messageColor(message.getSeverity().toUpperCase() + " in ") +
fileAndNumberColor(message.getFile() + "(" + message.getLine() + "," + message.getCharacter() + ")") +
messageColor(':'),
codeColor(message.getFormattedCode() + ': ') + message.getContent()
].join(os.EOL);
};
module.exports = {
// Other config...
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: { transpileOnly: true }
}
]
},
resolve: {
extensions: [ '.ts', '.tsx', 'js' ]
},
plugins: [
new ForkTsCheckerWebpackPlugin({ formatter: clickableFormatter }) // Here we get our clickability back
]
};

With that in place, what do you we have? This:

VS Code clickability; it's a beautiful thing.

Under the Duck: An Afternoon in Open Source

Have you ever wondered what happens behind the scenes of open source projects? One that I'm involved with is ts-loader; a TypeScript loader for webpack. Yesterday was an interesting day in the life of ts-loader and webpack; things unexpectedly broke. Oh and don't worry, they're fixed now.

How things panned out reflects well on the webpack community. I thought it might be instructive to take a look at the legs furiously paddling underneath the duck of open source. What follows is a minute by minute account of my life on the afternoon of Wednesday 22nd February 2017:

3:55pm#

I'm sat at my desk in the City of London. I have to leave at 4pm to go to the dentist. I'm working away on a project which is built and bundled using ts-loader and webpack. However, having just npm installed and tried to spin up webpack in watch mode, I discover that everything is broken. Watch mode is not working - there's an error being thrown in ts-loader. It's to do with a webpack property called mtimes. ts-loader depends upon it and it looks like it is no longer always passed through. Go figure. ### 4:01pm

I've got to go. I'm 15 minutes from Bank station. So, I grab my bag and scarper out the door. On my phone I notice an issue has been raised - other people are being affected by the problem too. As I trot down the various alleys that lead to the station I wonder whether I can work around this issue. Using GitHub to fork, edit code and submit a PR on a mobile phone is possible. Just. But it's certainly not easy...

My PR is in, the various test packs are starting to execute somewhere out there in Travis and Appveyor-land. Then I notice Ed Bishop has submitted a near identical PR. Yay Ed! I'm always keen to encourage people to contribute and so I intend to merge that PR rather than my own.

16:12#

Rubbish. The Waterloo and City Line is out of action. I need to get across London to reach Waterloo or I'll miss my appointment. It's time to start running....

16:15#

It's rather nagging at me that behaviour has changed without warning. This has been reliably in place the entire time I've been involved with ts-loader / webpack. Why now? I don't see any obvious mentions on the webpack GitHub repo. So I head over to the webpack Slack channel and ask: (conversation slightly abridged)

johnny_reilly#

Hey all, has something happened to mtimes? Behaviour seems to have changed - now undefined occasionally during watch mode. A PR has been raised against ts-loader to work around this https://github.com/TypeStrong/ts-loader/pull/480#issuecomment-281714600

However I'm wondering if this should actually be merged given behaviour has changed unexpectedly

sokra#

ah...

i removed it. I thought it was unused.

johnny_reilly#

It's definitely not!

sokra#

it's not in the public API^^

Any reason why you are not using getTimes()?

...

johnny_reilly#

Okay, I'm on a train and won't be near a computer for a while. ts-loader is presently broken because it depends on mtimes. Would it be possible for you to add this back at least for now. I'm aware many people depend on ts-loader and are now broken. #### sokra

sure, I readd it but deprecate it.

...

sean.larkin#

@sokra is this the change you just made for that watchpack bug fix? Or unlrelated, just wanted to track if I didn't already have the change/issue #### sokra

https://github.com/webpack/watchpack/pull/48

johnny_reilly#

This is what the present code does:

const watcher = watching.compiler.watchFileSystem.watcher ||
watching.compiler.watchFileSystem.wfs.watcher

And then .mtimes

Should I be able to do .getTimes() instead?

sokra#

actually you can't rely on watchFileSystem being NodeJsWatchFileSystem. But this is another topic

...

but yes

johnny_reilly#

Thanks @sokra - when I get to a keyboard I'll swap mtimes for getTimes() and report back.

17:28#

Despite various trains being out of action / missing in action I've made it to the dentists; phew! I go in for my checkup and plan to take a look at the issue later that evening. In the meantime I've hoping that Tobias (Sokra) will get chance to republish so that ts-loader users aren't too impacted.

18:00#

Done at the dentist and I'm heading home. Whilst I've been opening wide and squinting at the ceiling, TypeScript 2.2 has shipped. Whilst this is super exciting, according to Greenkeeper, the new version has broken the build. Arrrrghhhh...

I start to look into this and realise we're not broken because of TypeScript 2.2; we were broken because of the mtimes. Tobias has now re-added mtimes and published. With that in place I requeue a build and.... drum roll.... we're green!

The good news just keeps on coming as Luka Zakraj拧ek has submitted a PR which uses getTimes() in place of mtimes. And the tests pass. Awesome! MERGE. I just need to cut a release and we're done.

18:15#

I'm home. My youngest son has been suffering from chicken pox all week and as a result my wife has been in isolation, taking care of him. We chat whilst the boys watch Paw Patrol as the bath runs. I flick open the laptop and start doing the various housekeeping tasks around cutting a release. This is interrupted by various bathtime / bedtime activities and I abandon work for now.

19:30#

The boys are down and I get on with the release; updating the changelog, bumping the version number and running the tests. For various reasons this takes longer than it normally does.

20:30#

Finally we're there; ts-loader 2.0.1 ships: https://github.com/TypeStrong/ts-loader/releases/tag/v2.0.1.

I'm tremendously grateful to everyone that helped out - thank you all!

ts-loader 2.0.1 has shipped; thanks @wsokra@bancek and @mredbishop https://t.co/I00c7sJyFo#typescript

鈥 John Reilly (@johnny_reilly) February 22, 2017

Using ts-loader with webpack 2

Hands up, despite being one of the maintainers of ts-loader (a TypeScript loader for webpack) I have not been tracking webpack v2. My reasons? Well, I'm keen on cutting edge but bleeding edge is often not a ton of fun as dealing with regularly breaking changes is frustrating. I'm generally happy to wait for things to settle down a bit before leaping aboard. However, webpack 2 RC'd last week and so it's time to take a look!

Porting our example#

Let's take ts-loader's webpack 1 example and try and port it to webpack 2. Will it work? Probably; I'm aware of other people using ts-loader with webpack 2. It'll be a voyage of discovery. Like Darwin on the Beagle, I shall document our voyage for a couple of reasons:

  • I'm probably going to get some stuff wrong. That's fine; one of the best ways to learn is to make mistakes. So do let me know where I go wrong.
  • I'm doing this based on what I've read in the new docs; they're very much a work in progress and the mistakes I make here may lead to those docs improving even more. That matters; documentation matters. I'll be leaning heavily on the Migrating from v1 to v2 guide.

So here we go. Our example is one which uses TypeScript for static typing and uses Babel to transpile from ES-super-modern (yes - it's a thing) to ES-older-than-that. Our example also uses React; but that's somewhat incidental. It only uses webpack for typescript / javascript and karma. It uses gulp to perform various other tasks; so if you're reliant on webpack for less / sass compilation etc then I have no idea whether that works.

First of all, let's install the latest RC of webpack:

npm install [email protected]2.2.0-rc.1 --save-dev

webpack.config.js#

Let's look at our existing webpack.config.js:

'use strict';
var path = require('path');
module.exports = {
cache: true,
entry: {
main: './src/main.tsx',
vendor: [
'babel-polyfill',
'fbemitter',
'flux',
'react',
'react-dom'
]
},
output: {
path: path.resolve(__dirname, './dist/scripts'),
filename: '[name].js',
chunkFilename: '[chunkhash].js'
},
module: {
loaders: [{
test: /\.ts(x?)$/,
exclude: /node_modules/,
loader: 'babel-loader?presets[]=es2016&presets[]=es2015&presets[]=react!ts-loader'
}, {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2016', 'es2015', 'react']
}
}]
},
plugins: [
],
resolve: {
extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js']
},
};

There's a number of things we need to do here. First of all, we can get rid of the empty extension under resolve; I understand that's unnecessary now. Also, I'm going to get rid of '.webpack.js' and '.web.js'; I never used them anyway. Also, just having 'babel' as a loader won't fly anymore. We need that suffix as well.

Now I could start renaming loaders to rules as the terminology is changing. But I'd like to deal with that later since I know the old school names are still supported at present. More interestingly, I seem to remember hearing that one of the super exciting things about webpack is that it supports modules directly now. (I think that's supposed to be good for tree-shaking but I'm not totally certain.)

Initially I thought I was supposed to switch to a custom babel preset called <a href="https://www.npmjs.com/package/babel-preset-es2015-webpack">babel-preset-es2015-webpack</a>. However it has a big "DEPRECATED" mark at the top and it says I should just use babel-preset-es2015 (which I already am) with the following option specified:

{
"presets": [
[
"es2015",
{
"modules": false
}
]
]
}

Looking at our existing config you'll note that for js files we're using query (options in the new world I understand) to configure babel usage. We're using query parameters for ts files. I have zero idea how to configure preset options using query parameters. Fiddling with query / options didn't seem to work. So, I've decided to abandon using query entirely and drop in a <a href="http://babeljs.io/docs/usage/babelrc/">.babelrc</a> file using our presets combined with the <a href="https://babeljs.io/docs/plugins/#plugin-preset-options">modules</a> setting:

{
"presets": [
"react",
[
"es2015",
{
"modules": false
}
],
"es2016"
]
}

As an aside; apparently these are applied in reverse order. So es2016 is applied first, es2015 second and react third. I'm not totally certain this is correct; the <a href="http://babeljs.io/docs/usage/babelrc/">.babelrc</a> docs are a little unclear.

With our query options extracted we're down to a simpler webpack.config.js:

'use strict';
var path = require('path');
module.exports = {
cache: true,
entry: {
main: './src/main.tsx',
vendor: [
'babel-polyfill',
'fbemitter',
'flux',
'react',
'react-dom'
]
},
output: {
path: path.resolve(__dirname, './dist/scripts'),
filename: '[name].js',
chunkFilename: '[chunkhash].js'
},
module: {
loaders: [{
test: /\.ts(x?)$/,
exclude: /node_modules/,
loader: 'babel-loader!ts-loader'
}, {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
},
plugins: [
],
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
};

plugins#

In our example the plugins section of our webpack.config.js is extended in a separate process. Whilst we're developing we also set the debug flag to be true. It seems we need to introduce a LoaderOptionsPlugin to do this for us.

As we introduce our LoaderOptionsPlugin we also need to make sure that we provide it with options. How do I know this? Well someone raised an issue against ts-loader. I don't think this is actually an issue with ts-loader; I think it's just a webpack 2 thing. I could be wrong; answers on a postcard please.

Either way, to get up and running we just need the LoaderOptionsPlugin in play. Consequently, most of what follows in our webpack.js file is unchanged:

// .....
var webpackConfig = require('../webpack.config.js');
var packageJson = require('../package.json');
// .....
function buildProduction(done) {
// .....
myProdConfig.plugins = myProdConfig.plugins.concat(
// .....
// new webpack.optimize.DedupePlugin(), Not a thing anymore apparently
new webpack.optimize.UglifyJsPlugin(),
// I understand this here matters...
// but it doesn't seem to make any difference; perhaps I'm missing something?
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
failPlugin
);
// .....
}
function createDevCompiler() {
var myDevConfig = webpackConfig;
myDevConfig.devtool = 'inline-source-map';
// myDevConfig.debug = true; - not allowed in webpack 2
myDevConfig.plugins = myDevConfig.plugins.concat(
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }),
new WebpackNotifierPlugin({ title: 'Webpack build', excludeWarnings: true }),
// this is the Webpack 2 hotness!
new webpack.LoaderOptionsPlugin({
debug: true,
options: myDevConfig
})
// it ends here - there wasn't much really....
);
// create a single instance of the compiler to allow caching
return webpack(myDevConfig);
}
// .....

LoaderOptionsPlugin we hardly new ya#

After a little more experimentation it seems that the LoaderOptionsPlugin is not necessary at all for our own use case. In fact it's probably not best practice to get used to using it as it's only intended to live a short while whilst people move from webpack 1 to webpack 2. In that vein let's tweak our webpack.js file once more:

function buildProduction(done) {
// .....
myProdConfig.plugins = myProdConfig.plugins.concat(
// .....
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: true
}
}),
failPlugin
);
// .....
}
function createDevCompiler() {
var myDevConfig = webpackConfig;
myDevConfig.devtool = 'inline-source-map';
myDevConfig.plugins = myDevConfig.plugins.concat(
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }),
new WebpackNotifierPlugin({ title: 'Webpack build', excludeWarnings: true }),
);
// create a single instance of the compiler to allow caching
return webpack(myDevConfig);
}
// .....

karma.conf.js#

Finally Karma. Our karma.conf.js with webpack 1 looked like this:

/* eslint-disable no-var, strict */
'use strict';
var webpackConfig = require('./webpack.config.js');
module.exports = function(config) {
// Documentation: https://karma-runner.github.io/0.13/config/configuration-file.html
config.set({
browsers: [ 'PhantomJS' ],
files: [
// This ensures we have the es6 shims in place and then loads all the tests
'test/main.js'
],
port: 9876,
frameworks: [ 'jasmine' ],
logLevel: config.LOG_INFO, //config.LOG_DEBUG
preprocessors: {
'test/main.js': [ 'webpack', 'sourcemap' ]
},
webpack: {
devtool: 'inline-source-map',
debug: true,
module: webpackConfig.module,
resolve: webpackConfig.resolve
},
webpackMiddleware: {
quiet: true,
stats: {
colors: true
}
},
// reporter options
mochaReporter: {
colors: {
success: 'bgGreen',
info: 'cyan',
warning: 'bgBlue',
error: 'bgRed'
}
}
});
};

We just need to chop out the debug statement from the webpack section like so:

module.exports = function(config) {
// .....
webpack: {
devtool: 'inline-source-map',
module: webpackConfig.module,
resolve: webpackConfig.resolve
},
// .....
});
};

Compare and contrast#

We now have a repo that works with webpack 2 rc 1. Yay! If you'd like to see it then take a look here.

I thought I'd compare performance / output size of compiling with webpack 1 to webpack 2. First of all in debug / development mode:

// webpack 1
Version: webpack 1.14.0
Time: 5063ms
Asset Size Chunks Chunk Names
main.js 37.2 kB 0 [emitted] main
vendor.js 2.65 MB 1 [emitted] vendor
// webpack 2
Version: webpack 2.2.0-rc.1
Time: 5820ms
Asset Size Chunks Chunk Names
main.js 38.7 kB 0 [emitted] main
vendor.js 2.63 MB 1 [emitted] [big] vendor

Size and compilation time is not massively different from webpack 1 to webpack 2. It's all about the same. I'm not sure if that's to be expected or not.... Though I've a feeling in production mode I'm supposed to feel the benefits of tree shaking so let's have a go:

// webpack 1
Version: webpack 1.14.0
Time: 5788ms
Asset Size Chunks Chunk Names
main.269c66e1bc13b7426cee.js 10.5 kB 0 [emitted] main
vendor.269c66e1bc13b7426cee.js 231 kB 1 [emitted] vendor
// webpack 2
Version: webpack 2.2.0-rc.1
Time: 5659ms
Asset Size Chunks Chunk Names
main.33e0d70eeec29206e9b6.js 9.22 kB 0 [emitted] main
vendor.33e0d70eeec29206e9b6.js 233 kB 1 [emitted] vendor

To my surprise this looks pretty much unchanged before and after as well. This may be a sign I have missed something crucial out. Or maybe that's to be expected. Do give me a heads up if I've missed something...

But you can't die... I love you!

That's how I was feeling on the morning of October 6th 2016. I'd been feeling that way for some time. The target of my concern? ts-loader. ts-loader is a loader for webpack; the module bundler. ts-loader allows you use TypeScript with webpack. I'd been a merry user of it for at least a year or so. But, at that point, all was not well in the land of ts-loader. Come with me and I'll tell you a story...

Going Red#

At some point, I became a member of the TypeStrong organisation on GitHub. I'm honestly not entirely sure how. I think it may have been down to the very excellent Basarat (he of ALM / atom-typescript / the list goes on fame) but I couldn't clearly say.

Either way, James Brantly's ts-loader was also one of TypeStrong's projects. Since I used it, I occasionally contributed. Not much to be honest; mostly it was documentation tweaks. I mean I never really looked at the main code at all. It worked (thanks to other people). I just plugged it into my projects and ploughed on my merry way. I liked it. It was well established; with friendly maintainers. It had a continuous integration test pack that ran against multiple versions of TypeScript on both Windows and Linux. I trusted it. Then one day the continuous integration tests went red. And stayed red.

This is where we came in. On the morning of October 6th I was mulling what to do about this. I knew there was another alternative out there (awesome-typescript-loader) but I was a little wary of it. My understanding of ATL was that it targeted webpack 2.0 which has long been in beta. Where I ply my trade (mostly developing software for the financial sector in the City of London) beta is not a word that people trust. They don't do beta. What's more I was quite happy with ts-loader; I didn't want to switch if I didn't have to. I also rather suspected (rightly) that there wasn't much wrong; ts-loader just needed a little bit of love. So I thought: I bet I can help here.

The Statement of Intent#

So that evening I raised an issue against ts-loader. Not a "sort it out chap" issue. No. That wouldn't be terribly helpful. I raised a "here's how I can help" issue. I present an abridged version below:

Okay here's the deal; I've been using ts-loader for a long time but my contributions up until now have mostly been documentation. Fixing of tests etc. As the commit history shows this is @jbrantly's baby and kudos to him.

He's not been able to contribute much of late and since he's the main person who's worked on ts-loader not much has happened for a while; the code is a bit stale. As I'm a member of TypeStrong I'm going to have a go at improving the state of the project. I'm going to do this as carefully as I can. This issue is intended as a meta issue to make it visible what I'm plannning to do / doing.

My immediate goal is to get a newer version of ts-loader built and shipped. Essentially all the bug fixes / tweaks since the last release should ship.

...

I don't have npm publish rights for ts-loader. Fortunately both @jbrantly and @blakeembrey do - and hopefully one of them will either be able to help out with a publish or let me have the requisite rights to do it.

I can't promise this is all going to work; I've got a limited amount of spare time I'm afraid. Whatever happens it's going to take me a little while. But I'm going to see where I can take this. Best foot forward! Please bear with me...

I did wonder what would happen next. This happened next:

My #opensourceguilt has been lifted thanks to @johnny_reilly stepping up to take over ts-loader. Thanks man!

鈥 James Brantly (@jbrantly) October 11, 2016

Caretaker, not BDFL#

So that's how it came to pass that I became the present main caretaker of ts-loader. James very kindly gave me the rights to publish to npm and soon enough I did. I fixed up the existing integration test pack; made it less brittle. I wrote a new integration test pack (that performs a different sort of testing; execution rather than comparison). I merged pull requests, I closed issues. I introduced a regression (whoops!), a community member helped me fix it (thanks Mike Mazmanyan!). In the last month ts-loader has shipped 6 times.

The thing that matters most in the last paragraph are the phrases "I merged pull requests" and "a community member helped me fix it". I'm wary of one man bands; you should be to. I want projects to be a thing communally built and maintained. If I go under a bus I want someone else to be able to carry on without me. So be part of this; I want you to help!

I've got plans to do a lot more. I'm in the process of refactoring ts-loader to make it more modular and hence easier for others to contribute. (Also it must be said, refactoring something is an excellent way to try and learn a codebase.) Version 1.0 of ts-loader should ship this week.

I'm working with Herrington Darkholme (awesome name BTW!) to add a hook-in point that will allow ts-loader to support vuejs. Stuff is happening and will continue to. But don't be shy; be part of this! ts-loader awaits your PRs and is happy to have as many caretakers as possible!

ES6 + TypeScript + Babel + React + Flux + Karma: The Secret Recipe

I wrote a while ago about how I was using some different tools in a current project:

  • React with JSX
  • Flux
  • 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!

What a Guy Wants#

I decided a couple of months ago what I wanted to have in my setup:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

gulpfile.js#

/* eslint-disable no-var, strict, prefer-arrow-callback */
'use strict';
var gulp = require('gulp');
var gutil = require('gulp-util');
var connect = require('gulp-connect');
var eslint = require('gulp-eslint');
var webpack = require('./gulp/webpack');
var staticFiles = require('./gulp/staticFiles');
var tests = require('./gulp/tests');
var clean = require('./gulp/clean');
var inject = require('./gulp/inject');
var lintSrcs = ['./gulp/**/*.js'];
gulp.task('delete-dist', function (done) {
clean.run(done);
});
gulp.task('build-process.env.NODE_ENV', function () {
process.env.NODE_ENV = 'production';
});
gulp.task('build-js', ['delete-dist', 'build-process.env.NODE_ENV'], function(done) {
webpack.build().then(function() { done(); });
});
gulp.task('build-other', ['delete-dist', 'build-process.env.NODE_ENV'], function() {
staticFiles.build();
});
gulp.task('build', ['build-js', 'build-other', 'lint'], function () {
inject.build();
});
gulp.task('lint', function () {
return gulp.src(lintSrcs)
.pipe(eslint())
.pipe(eslint.format());
});
gulp.task('watch', ['delete-dist'], function() {
process.env.NODE_ENV = 'development';
Promise.all([
webpack.watch()//,
//less.watch()
]).then(function() {
gutil.log('Now that initial assets (js and css) are generated inject will start...');
inject.watch(postInjectCb);
}).catch(function(error) {
gutil.log('Problem generating initial assets (js and css)', error);
});
gulp.watch(lintSrcs, ['lint']);
staticFiles.watch();
tests.watch();
});
gulp.task('watch-and-serve', ['watch'], function() {
postInjectCb = stopAndStartServer;
});
var postInjectCb = null;
var serverStarted = false;
function stopAndStartServer() {
if (serverStarted) {
gutil.log('Stopping server');
connect.serverClose();
serverStarted = false;
}
startServer();
}
function startServer() {
gutil.log('Starting server');
connect.server({
root: './dist',
port: 8080
});
serverStarted = true;
}

Let's start picking this apart; what do we actually have here? Well, we have 2 gulp tasks that I want you to notice:

build

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_ENV environment variable to production to use the production build of React which does not include the development warnings and runs significantly faster.
watch-and-serve

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

WebPack#

Let's take a look at what's happening under the covers of webpack.build() and webpack.watch().

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.

Our gulpfile.js above uses the following task:

'use strict';
var gulp = require('gulp');
var gutil = require('gulp-util');
var webpack = require('webpack');
var WebpackNotifierPlugin = require('webpack-notifier');
var webpackConfig = require('../webpack.config.js');
function buildProduction(done) {
// modify some webpack config options
var myProdConfig = Object.create(webpackConfig);
myProdConfig.output.filename = '[name].[hash].js';
myProdConfig.plugins = myProdConfig.plugins.concat(
// make the vendor.js file with cachebusting filename
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.[hash].js' }),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin()
);
// run webpack
webpack(myProdConfig, function(err, stats) {
if(err) { throw new gutil.PluginError('webpack:build', err); }
gutil.log('[webpack:build]', stats.toString({
colors: true
}));
if (done) { done(); }
});
}
function createDevCompiler() {
// show me some sourcemap love people
var myDevConfig = Object.create(webpackConfig);
myDevConfig.devtool = 'inline-source-map';
myDevConfig.debug = true;
myDevConfig.plugins = myDevConfig.plugins.concat(
// Make the vendor.js file
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }),
new WebpackNotifierPlugin({ title: 'Webpack build', excludeWarnings: true })
);
// create a single instance of the compiler to allow caching
return webpack(myDevConfig);
}
function buildDevelopment(done, devCompiler) {
// run webpack
devCompiler.run(function(err, stats) {
if(err) { throw new gutil.PluginError('webpack:build-dev', err); }
gutil.log('[webpack:build-dev]', stats.toString({
chunks: false, // dial down the output from webpack (it can be noisy)
colors: true
}));
if (done) { done(); }
});
}
function bundle(options) {
var devCompiler;
function build(done) {
if (options.shouldWatch) {
buildDevelopment(done, devCompiler);
} else {
buildProduction(done);
}
}
if (options.shouldWatch) {
devCompiler = createDevCompiler();
gulp.watch('src/**/*', function() { build(); });
}
return new Promise(function(resolve, reject) {
build(function (err) {
if (err) {
reject(err);
} else {
resolve('webpack built');
}
});
});
}
module.exports = {
build: function() { return bundle({ shouldWatch: false }); },
watch: function() { return bundle({ shouldWatch: true }); }
};

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 webpack.config.js:

/* eslint-disable no-var, strict, prefer-arrow-callback */
'use strict';
var path = require('path');
module.exports = {
cache: true,
entry: {
// The entry point of our application; the script that imports all other scripts in our SPA
main: './src/main.tsx',
// The packages that are to be included in vendor.js
vendor: [
'babel-polyfill',
'events',
'flux',
'react'
]
},
// Where the output of our compilation ends up
output: {
path: path.resolve(__dirname, './dist/scripts'),
filename: '[name].js',
chunkFilename: '[chunkhash].js'
},
module: {
loaders: [{
// The loader that handles ts and tsx files. These are compiled
// with the ts-loader and the output is then passed through to the
// babel-loader. The babel-loader uses the es2015 and react presets
// in order that jsx and es6 are processed.
test: /\.ts(x?)$/,
exclude: /node_modules/,
loader: 'babel-loader?presets[]=es2015&presets[]=react!ts-loader'
}, {
// The loader that handles any js files presented alone.
// It passes these to the babel-loader which (again) uses the es2015
// and react presets.
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}]
},
plugins: [
],
resolve: {
// Files with the following extensions are fair game for webpack to process
extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js']
},
};

Inject#

Your compiled output needs to be referenced from some kind of HTML page. So we've got this:

<!doctype html>
<html lang="en">
<head>
<meta charSet="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ES6 + Babel + React + Flux + Karma: The Secret Recipe</title>
<!-- inject:css -->
<!-- endinject -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
<body>
<div id="content"></div>
<!-- inject:js -->
<!-- endinject -->
</body>
</html>

Which is no more than a boilerplate HTML page with a couple of key features:

  • a single &lt;div /&gt; element in the &lt;body /&gt; which is where our React app is going to be rendered.
  • &lt;!-- inject:css --&gt; and &lt;!-- inject:js --&gt; placeholders where css and js is going to be injected by gulp-inject.
  • a single &lt;link /&gt; 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 inject.build() and inject.watch(). They take css and javascript and, using our shell template, create a new page which has the css and javascript dropped into their respective placeholders:

'use strict';
var gulp = require('gulp');
var inject = require('gulp-inject');
var glob = require('glob');
function injectIndex(options) {
var postInjectCb = options.postInjectCb;
var postInjectCbTriggerId = null;
function run() {
var target = gulp.src('./src/index.html');
var sources = gulp.src([
//'./dist/styles/main*.css',
'./dist/scripts/vendor*.js',
'./dist/scripts/main*.js'
], { read: false });
return target
.on('end', function() { // invoke postInjectCb after 1s
if (postInjectCbTriggerId || !postInjectCb) { return; }
postInjectCbTriggerId = setTimeout(function() {
postInjectCb();
postInjectCbTriggerId = null;
}, 1000);
})
.pipe(inject(sources, { ignorePath: '/dist/', addRootSlash: false, removeTags: true }))
.pipe(gulp.dest('./dist'));
}
var jsCssGlob = 'dist/**/*.{js,css}';
function checkForInitialFilesThenRun() {
glob(jsCssGlob, function (er, files) {
var filesWeNeed = ['dist/scripts/main', 'dist/scripts/vendor'/*, 'dist/styles/main'*/];
function fileIsPresent(fileWeNeed) {
return files.some(function(file) {
return file.indexOf(fileWeNeed) !== -1;
});
}
if (filesWeNeed.every(fileIsPresent)) {
run('initial build');
} else {
checkForInitialFilesThenRun();
}
});
}
checkForInitialFilesThenRun();
if (options.shouldWatch) {
gulp.watch(jsCssGlob, function(evt) {
if (evt.path && evt.type === 'changed') {
run(evt.path);
}
});
}
}
module.exports = {
build: function() { return injectIndex({ shouldWatch: false }); },
watch: function(postInjectCb) { return injectIndex({ shouldWatch: true, postInjectCb: postInjectCb }); }
};

This also triggers the server to serve up the new content.

Static Files#

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:

'use strict';
var gulp = require('gulp');
var cache = require('gulp-cached');
var targets = [
// In my own example I don't use any of the targets below, they
// are included to give you more of a feel of how you might use this
{ description: 'FONTS', src: './fonts/*', dest: './dist/fonts' },
{ description: 'STYLES', src: './styles/*', dest: './dist/styles' },
{ description: 'FAVICON', src: './favicon.ico', dest: './dist' },
{ description: 'IMAGES', src: './images/*', dest: './dist/images' }
];
function copy(options) {
// Copy files from their source to their destination
function run(target) {
gulp.src(target.src)
.pipe(cache(target.description))
.pipe(gulp.dest(target.dest));
}
function watch(target) {
gulp.watch(target.src, function() { run(target); });
}
targets.forEach(run);
if (options.shouldWatch) {
targets.forEach(watch);
}
}
module.exports = {
build: function() { return copy({ shouldWatch: false }); },
watch: function() { return copy({ shouldWatch: true }); }
};

Karma#

Finally, we're ready to get our tests set up to run continually with Karma. tests.watch() triggers the following task:

'use strict';
var Server = require('karma').Server;
var path = require('path');
var gutil = require('gulp-util');
module.exports = {
watch: function() {
// Documentation: https://karma-runner.github.io/0.13/dev/public-api.html
var karmaConfig = {
configFile: path.join(__dirname, '../karma.conf.js'),
singleRun: false,
plugins: ['karma-webpack', 'karma-jasmine', 'karma-mocha-reporter', 'karma-sourcemap-loader', 'karma-phantomjs-launcher', 'karma-phantomjs-shim'], // karma-phantomjs-shim only in place until PhantomJS hits 2.0 and has function.bind
reporters: ['mocha']
};
new Server(karmaConfig, karmaCompleted).start();
function karmaCompleted(exitCode) {
gutil.log('Karma has exited with:', exitCode);
process.exit(exitCode);
}
}
};

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:

/* eslint-disable no-var, strict */
'use strict';
var webpackConfig = require('./webpack.config.js');
module.exports = function(config) {
// Documentation: https://karma-runner.github.io/0.13/config/configuration-file.html
config.set({
browsers: [ 'PhantomJS' ],
files: [
'test/import-babel-polyfill.js', // This ensures we have the es6 shims in place from babel
'test/**/*.tests.ts',
'test/**/*.tests.tsx'
],
port: 9876,
frameworks: [ 'jasmine', 'phantomjs-shim' ],
logLevel: config.LOG_INFO, //config.LOG_DEBUG
preprocessors: {
'test/import-babel-polyfill.js': [ 'webpack', 'sourcemap' ],
'src/**/*.{ts,tsx}': [ 'webpack', 'sourcemap' ],
'test/**/*.tests.{ts,tsx}': [ 'webpack', 'sourcemap' ]
},
webpack: {
devtool: 'eval-source-map', //'inline-source-map', - inline-source-map doesn't work at present
debug: true,
module: webpackConfig.module,
resolve: webpackConfig.resolve
},
webpackMiddleware: {
quiet: true,
stats: {
colors: true
}
},
// reporter options
mochaReporter: {
colors: {
success: 'bgGreen',
info: 'cyan',
warning: 'bgBlue',
error: 'bgRed'
}
},
junitReporter: {
outputDir: 'test-results', // results will be saved as $outputDir/$browserName.xml
outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile
suite: ''
}
});
};

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.

Babel 5 -> Babel 6#

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.