If you've landed here, I can only assume you're like me and see packages as the highest form of sophistication in software development. In that same vein, I bet at some point in the past you've wished you could start applying DRY principles to your client-side efforts. I know for myself, I don't enjoy writing the same application bootstrap code constantly and so recently, I was motivated to codify it.
This body of understanding has taken me quite some while to figure out, hopefully what I share here is helpful enough to get you up to speed. No post is complete without some kind of example, so throughout I'm going to reference a package I've just finished putting together called protoculture.
Briefly described, protoculture encapsulates all the common bootstrap and conventions I've been using while developing TypeScript apps that use React and Redux. Honestly, I've already gotten a lot of benefit out of putting this package together, but nothing about this post will be coupled to its objective.
It might be piling on a bit too much too early if you're really starting from scratch learning all of this. But I do suggest circling back afterwards and containerizing as a stretch goal.
This body of understanding has taken me quite some while to figure out, hopefully what I share here is helpful enough to get you up to speed. No post is complete without some kind of example, so throughout I'm going to reference a package I've just finished putting together called protoculture.
Briefly described, protoculture encapsulates all the common bootstrap and conventions I've been using while developing TypeScript apps that use React and Redux. Honestly, I've already gotten a lot of benefit out of putting this package together, but nothing about this post will be coupled to its objective.
TypeScript
Based on the title, you shouldn't be surprised. The first thing I'm going to tell anyone looking to tame their front end code is to stop writing it in plain ES. Although with that said, much of this post focuses on the generic npm ecosystem, so if you are still committed to ES, I'm sure there are still plenty of useful tidbits here.
Getting into things, make sure you understand TypeScript itself and have an appreciation for authoring code in it. This should start with really getting a handle on tsconfig.json which even took me a bit to appreciate fully.
Once you're in the swing of things, try to look for opportunities to make use of things like enums and generics. Explore TypeScript and find ways make your code more expressive and easier to consume.
With regards to IDE support, they're in a bit of a weird space currently and can sometimes get in the way. Especially Visual Studio which still trips on its own solution and project models.
Before you go too far writing any TypeScript code, throw a tsconfig.json in your projects early and play by the rules.
If you really need a suggestion for an editor, look at Atom, Visual Studio Code or WebStorm, they are all quite strong.
Before you go too far writing any TypeScript code, throw a tsconfig.json in your projects early and play by the rules.
If you really need a suggestion for an editor, look at Atom, Visual Studio Code or WebStorm, they are all quite strong.
Building TypeScript
Building your final outputs in the JavaScript space has got to be one of the most challenging things to gain a good mental model for. It's going to have to happen if we want to produce a package, so you'll have to commit to at least appreciating all the objectives.
The community has produced great tools like JSPM and WebPack which take care of module loading and bundling. Briefly explained:
The community has produced great tools like JSPM and WebPack which take care of module loading and bundling. Briefly explained:
- Module loading is the convention where anything processing your code will go through a series of steps and specific locations to resolve your imports to actual JS code.
- Bundling is the process you go through to produce your final output. This usually ends up being one or several .js files that you reference from script tags.
To accomplish this, what I've resorted to doing when producing packages backed by TypeScript code is I get tsc to produce its output as UMD modules. This ensures that anyone looking to use your package can do so, regardless of what module system they're using inside of their project.
If someone using your package is authoring their code in plain JS, they'll be on their way. Indeed one of the nice things about this process is that while your code will be written in TypeScript, consumers of your package won't be obligated to do the same.
If anyone intends on consuming your package from another TypeScript project however, this is only going to be half the story. I'll address the other half once a few other things have been established.
Docker
This won't come as a surprise to people who know me, but I will containerize anything that can be. The benefits I've observed in doing this cannot be understated and if for whatever reason you've missed it up until now, it has become eye-rollingly easy to get started using Docker for Windows and/or Docker for Mac.
My usage of docker in this scenario is simple. If I can orchestrate my tooling and processes through a container, then anyone can run them. Not only will we all then be using the same tools and have the same experience, but the process will run anywhere. Including my build server!
It might be piling on a bit too much too early if you're really starting from scratch learning all of this. But I do suggest circling back afterwards and containerizing as a stretch goal.
npm
Goes without saying, you need to be authoring a package.json to contain all your dependencies and metadata.
This one is straightforward and most of your existing understandings of this file as a consumer are going to carry you. The main things to shore up will be to do with making sure your package is ready for others:
- The version string as it is committed in source control will never change and should stay permanently fixed at something like 0.0.0-development.
Bear in mind that publishing your package will require that this version number be different to any prior version of your package that has been published. But I'll explain how that process works shortly. - Be a good FOSS citizen and take the time to choose and appreciate a proper SPDX license identifier for your package. All the cool kids are doing Apache 2 lately.
- Identify the entrypoints of your package. There are two types of entrypoints to know about for our purposes, I'll go into detail about what I've chosen shortly.
Another file you're going to want to know about is .npmignore which is used to mask off files from being included during packaging. You'll want this so that you don't include more than what is necessary when pushing your package to npm.
Yarn
Yarn is a recently-released although long in development drop-in replacement for npm. If you use server-side tooling like NuGet, composer or bundler, then you can basically think of this as bringing the quality of dependency management up to community expectations in one strike.
It's designed to be fully compatible with npm, all you need to do is start using it and remember to commit your lockfile to source control.
TypeScript and npm Revisited
While going over npm, I mentioned two entrypoint properties in package.json. The files they point to are clear when you read them and the respective documentation for the properties does a good enough job explaining things.
What wasn't immediately apparent when I was putting all of this together was how to produce these two files and what exactly they would correspond to. Especially when producing a library.
Libraries are usually just inert functionality, waiting to be integrated. There is no entrypoint and nothing to invoke at a top level.
Nevertheless, consumers of your package are going to want some notion of the touchpoints you are exposing. With that in mind, your index actually ends up looking kind of like a header file! When typescript goes to build your UMD modules, index.js will just be the JS equivalent of those re-exports and index.d.ts will export the corresponding type information. Spend a good amount of time understanding this as I think once understood, the motivations behind everything else gets clearer.
Something else recently improved in this space is the introduction of @types packages for TypeScript. These packages are produced either by package maintainers or the community to allow people authoring TypeScript to get type hinting for packages not authored in TypeScript.
Before @types, there was an external tool called Typings that would install these dependencies, much like npm would install its own packages. I spent a bit of time using this prior and have to say @types is a massive improvement. Especially when introduced alongside Yarn. Everything blends in nicely with the node_modules directory and at long last the ecosystem seems to be stabilizing around some standard expectations.
What wasn't immediately apparent when I was putting all of this together was how to produce these two files and what exactly they would correspond to. Especially when producing a library.
Libraries are usually just inert functionality, waiting to be integrated. There is no entrypoint and nothing to invoke at a top level.
Nevertheless, consumers of your package are going to want some notion of the touchpoints you are exposing. With that in mind, your index actually ends up looking kind of like a header file! When typescript goes to build your UMD modules, index.js will just be the JS equivalent of those re-exports and index.d.ts will export the corresponding type information. Spend a good amount of time understanding this as I think once understood, the motivations behind everything else gets clearer.
Something else recently improved in this space is the introduction of @types packages for TypeScript. These packages are produced either by package maintainers or the community to allow people authoring TypeScript to get type hinting for packages not authored in TypeScript.
Before @types, there was an external tool called Typings that would install these dependencies, much like npm would install its own packages. I spent a bit of time using this prior and have to say @types is a massive improvement. Especially when introduced alongside Yarn. Everything blends in nicely with the node_modules directory and at long last the ecosystem seems to be stabilizing around some standard expectations.
Semantic Release
There's a lot of cool going on here, but this is a really nice one and my hat goes off to the maintainers of this project.
I mentioned earlier that you shouldn't put any real version numbers inside of your package.json file. This is for good reason, as I'm sure it's obvious that managing that by hand would become a burden. It also makes continuous delivery quite difficult.
Semantic release is an npm-based dev-time tool that shoulders the burden of coming up with a proper version number and publishing npm packages with them. It's extremely easy to get started using it, especially if you're using any CI platforms that are popular with the community.
Continuous Integration and Delivery
One skill that I think people don't always spend enough time advancing is build and deployment. There are a number of very valid reasons for this, ranging from comfort and confidence to not having the infrastructure available to tinker with.
If you're looking for a quick way to upgrade your skills, I can think of no better a teacher than producing open source packages. The purpose of the package is immaterial, you can seriously take the most menial of things that you deal with and turn them into packages.
What I'd like to first say is that Travis CI is free for open source projects. Just go make yourself an account there and poke around the interface. It integrates fully with github and they have done such a good job coming up with a simple and intuitive experience that it can guide you into learning exactly what kind of objectives to seek out when configuring build flows.
Second, based on the publicly visible build information for protoculture you can see what goes on just by examining the logs. Configuring builds in Travis CI is done using a simple .yml file. No surprises or undocumented gotchas.
In fact, if this is something you are thinking about taking on, you're now probably itching to read more about what I've done with Docker!
Summary
I've talked about a lot of different moving parts in this post and I think that's really what motivated me to author it. Creating packages for npm is not trivial - and automating it can be daunting. Adding a transpiled language like TypeScript doesn't help matters as you are removing yourself by one degree from the flow in the hopes of obtaining other structural advantages.
Up until recently I only had a rough blueprint of what the best practices were in the JavaScript space. There is a lot of cynicism and humour out there targeting node, npm and browser development. Much of it is a caricature of legitimate gripes, but most of the time I see through it as a way to avoid some very rewarding effort.
Being able to reuse code on the client side has given me a level of confidence similar to what I enjoy during server side development. Going forward, I know my abstractions will be much better suited for deployment and reuse because of the great tools made by the community.
I hope this post has helped you and as always if I think of any improvements or if anyone has a correction for me, I'll be sure to make edits.
Comments
Post a Comment