Grundler

The no-faff Ruby frontend bundler.

Now that all the evergreen browsers have support for JavaScript module loading, we can finally pretend like node.js never happened! Grundler is the sinecure for everyone who's sick to death of npm, yarn, babel, webpack, parcel, rollup, vite, fartspray, and snowpack. And I only made one of those up.

Installation

Add this line to your application's Gemfile:

gem "grundler"

And then execute:

$ bundle install

Or install it yourself as:

$ gem install grundler

But we have enough package managers, please stop

Sorry! But Grundler fills one of the most important niches in the Ruby ecosystem: MINE. Specifically, that would be the niche where I sit, precariously perched, cursing the bloat and complexity of node's module ecosystem, longing for the easy and carefree days of dropping a .js file into a "lib" folder and calling it a day. Seriously, we got ourselves into this hellscape just because we didn't think globals were enterprise-grade or whatever?! Like what were people even

Alright, settle down, explain how it works

You've totally used a package manager before. You'll need to use one to install this one, in fact. So you probably know most of the stuff you need to know. Grundler, as a design goal, tries to 1) leverage the new browser-supported module format and 2) do as little as possible. You won't find any script sections or dev dependencies in its configuration. You can't run commands through it, shrinkwrap, or add prepublish steps. It'll only grab packages for you off npm's repository so you can use them and you should be damn well content with that.

Please tell me you didn't invent a new configuration file though

I'm not below that sort of thing but no, it reads your package.json kind of like you'd expect. Grundler only bothers with the important part, the { "dependencies" } section. If you expect it to also run your dev server or test suite or whatever, you will be disappointed. You might be disappointed anyway but I'd honestly prefer you at least be disappointed with the right things?

So what actually happens when I add a package then

Grundler goes to everyone's favorite JavaScript repository, npm, to look for the package you wanted to install. If it's found, it'll look up the most recent version, download its tarball, and analyze the JSON metadata to see if it uses the new ES module format. If it does, bingo, we can untar the module file and only the module file to our dependency folder (currently called nodules because come on, it was right there but the node people were too buttoned-up I guess). This is cool because it's reminiscent of the old "go to someone's site and download a JS file" but structured and reproducible.

If the package you're trying to fetch isn't using ES module format though, you're in trouble.

Trouble

Trouble. Grundler has a thing I've aptly named CrapMode that gets engaged whenever a package is using one of the old bothersome and browser-incompatible module formats (which, unfortunately, is pretty often). It will wrap the library in a simple little shim of sorts, to fool the module into believing it can hook into module.exports but AHA that's a honeypot we put there and then we can export. It's bound to fail in mysterious ways but it turns out there are a lot of packages that work just fine with it.

So you may get lucky. Or you may not. People have really been contorting themselves over the years doing weird shit to get their packages loading across the various formats the Internet has coughed up, and module.exports might not be a thing they use. Either way, if the package is one you really want, the responsible and adult thing to do is to go to their repository and help them out with a PR to convert to the new module format. Node has had support for quite a while, now browsers have it too — we're finally on track towards the compilation-free past/future again. Contribute and be part of it!

And how do I use my freshly downloaded package again

You import it, like you would any file you wrote yourself. Look:

$ bundle exec grundle add three
Installing three 0.124.0

That should result in a file called nodules/three.js. That's your package, ready for pickup! Now, in order to import things in the browser, you need to opt in to the browser's module system by using type="module" in your script tag:

<script src="main.js" type="module"></script>

And inside main.js you can do:

import * as THREE from "./nodules/three.js";
console.log(THREE);

And if that thing is an ES module, or the CrapMode shim worked, you're good to go!

That import statement though, slashes and stuff, ew

I hear you. It's not as pretty as just from "three". People are working on this problem, with something called import maps. But it's not THAT ugly and look: the code is there. It can be loaded, just the way you'd expect if a teammate had written the code. You don't have npm's hundreds of megabytes of dep-tree lurking beneath the depths, ready to kneecap your CI system and bog down your builds. Let's hope we can all get back to the fairytale land of "it's just another file in your project". I'd even check the nodules folder in to your project if I were you — after all, in the end you're responsible for everything you ship, so why do we keep pretending our dependencies are this pristine thing, never to be touched?

But I'm using TypeScript, if you just download the compiled and minified JS distribution I won't have my d.ts files and

Go away! Contemplate your life choices. I will have no truck with your transpiled abomination. The entire point of Grundler is to dramatically shorten the build chain, get back to just writing JS files, doing away with build servers and transpilation once and for all. Plus everyone knows TypeScript is just .NET wearing JavaScript's skin, ready to murder you in your sleep with a rictus grin on its rotting, lying face.

Ok but what about deployment, won't this ruin my artisanal never-expiring cached CDN

Yes. I'm throwing my hands up here and again saying look, I'm sorry, but browsers don't do the import maps thing yet. And adding version numbers to downloaded libs would be murder on your actual source files: going through every file in your project that has import * from thing-0.10.1.js and replacing it with thing-0.10.2.js every time you bumped versions would absolutely SUCK.

You realistically have three options at this time:

1) Weep and use Webpack 2) Ditch your far-future cache and trust people's browsers to do the right thing 3) Use an import-aware fingerprint tool when building for production

But... there IS no import-aware fingerprinting tool

Ha! Pop quiz, hotshot: what has two thumbs and has just written that kind of fingerprinting tool? It trawls a directory full of js files, copies them to md5-stamped files, and rewrites all import statements to use the stamped files. I'll put it up once it's been documented and more thoroughly tested. And once I've made sure it's actually a good idea.

Yes, yes, it's a build step, which makes me a little sad, but fingers crossed we can get rid of it when browsers have robust support for import maps. I bet the Babel crew have said something similar but then it kind of never happened and the tools got entrenched and suddenly everyone's stuck in the labyrinth with no way out and an angry minotaur belching somewhere behind them. But you can trust me! I'm a very honest-looking guy and I totally mean it: ship import maps and this won't be a problem anymore.

Cool, how do I use Grundler then

Grundler tries its best to not do very much. No spiderweb of metadata or configuration, no weird executable sandbox things, and only four commands:

Adding packages

Run grundle add [list of packages]. If you wanted to install, say, three.js and ky and have installed Grundler from your Gemfile it'd look like this:

bundle exec grundle add three ky

Grundler will go and find the latest version of those packages, install them, and add them to the package.json file.

Installing from package.json

Once you've added a few packages, you can distribute the package.json and install using that, and Grundler will fetch the version specified. To install from package.json, run:

bundle exec grundle install

Updating packages

Sometimes a package has been updated! To grab all the newest versions, run:

bundle exec grundle update

All your packages will be updated and written to package.json. You currently can't use the update command to update single packages, but Grundler is dumb enough that adding the packages again by running add package1 package2 will do that for you.

Removing packages

Tired of a package? No module export and CrapMode didn't work? Run:

bundle exec grundle remove packagename

It'll be removed from your nodules folder and your package.json file. Grundler will also get rid of any empty directories left after a removal.

Configuration

There's only one configuration directive at this point and it's called nodulePath. Say you have all your JS source files in a directory called src. Then you might want your dependencies to live in src/nodules. Add the nodulePath directive to your package.json like so:

{
  "dependencies": {},
  "nodulePath": "./src/nodules"
}

That'll make all of Grundler's operations work on that directory instead.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Testing is done with Minitest and mocking of npm is done with webmock. I initially wanted to use VCR for the JSON stubs but there seemed to be a lot of configuration and nothing worked and so I just put them there by hand and this is all TMI and never mind, there's like 200 lines of tests in a single file, you'll get the hang of it quickly.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/johanhalse/grundler.

License

The gem is available as open source under the terms of the MIT License.