i hate build tools.

Usage no npm install needed!

<script type="module">
  import nrserUgh from 'https://cdn.skypack.dev/@nrser/ugh';



Don't use this.

I wrote it because I was dealing with way too many cross-dependent co-developed node packages at one time and I couldn't get gulp to work in a way I considered consistent or reliable and I couldn't find anything that seemed like it would.

But you can. You're better than me. I believe in you. Please. The last thing the world needs is another shitty over-engineered, under-understood, widely-misused build tool.

Everything past here is just assorted dribble I half-wrote at various times during "development".

How It (Doesn't) Work

I haven't been here in a bit and I forgot how it (doesn't) work, so I'm going to now write down what I can quickly figure out.


Ugh can and does build Ugh, but it maybe can't if it's not built already. Luckily there seems to be some Yarn/NPM "scripts" (really just shell one-liners) that can do it.


yarn run build

If there are dependency problems, blow away //node_modules and re-install 'em:

rm -rf ./node_modules && yarn

Seeing What's Up

You can see the dependency tree with

ugh list

What you want to do is run stuff, probably "watch" tasks, which will then react to input events like files changing and shit.

Running Shit

Find out more about the run sub-command:

$ ugh run -h

Usage: run|r [options] [tasks...]

Run tasks.


-h, --help                    output usage information
-p, --package <package-name>  Filter by task package name(s).
-y, --type <task-type-name>   Filter by task type name.
-t, --tag <task-tag>          Filter by task tag.
-i, --id <task-id>            Filter by task id.
--all                         Run ALL tasks.
--no-deps                     Don't run task dependencies first

You can build by running tasks with the build tag:

ugh run -t build

If that works what you probably want to watch stuff using the watch tag:

ugh run --all

What I Want

or, "motivation"...

Incremental-First Approach

most all the building everyone does is incremental. full builds are stupid easy. i care about optimizing for reactive, correct, fast incremental builds during development.


things should do what they need to do in response to changes to the source files. this is called watching in most build systems, and seems to be implemented as a bit of an add-on or afterthought, where to me it's the central concern.


  1. don't leave destination files in place if the source file was deleted. this can cause major confusion if there are old import lines accidentally left behind (which there often are) as the old compiled file gets used.

    ugh should automatically delete destination files when the source file is removed. the "watch" functionality in both gulp and fly seems like an afterthought that exhibits this behavior.

  2. don't leave the build in an inconsistent state.

    • clean out dependent files when their upstream sources change. this includes cleaning bundles when source files change so that developers aren't seeing an old bundle in the client when compilation fails. i think it's better to clearly on the client that the bundle is not there then load the old one.

    • don't write some compiled files if other fail.


  1. don't waste time with unnecessary spin-up and IO.

    just like it's super easy to do a full build every time, it's also super easy to spin up a new process, read in all the source files, and write all the destination files each time you need to execute a task - but it's slow as shit. don't do this.

    this means that:

    1. tasks should keep their libraries in memory if possible (not spawn).

      this can be tricky since most of these build tools were written under the presumption they would execute in their own process. mocha for instance seems to often break unless it starts clean every time. luckily, it spins up fast enough that it's not a huge problem.

    2. file contents should be kept in memory if possible.

      writing to the disk just for another task to read it in is stupid and slow. we can pass the buffer directly and write to the disk asynchronously.

      i would say we want to use streams to process as available, but i don't think nearly any of the build tools we use are capable of processing streams.

  2. don't thrash.

    for what i'm going to call pipe tasks that map in files to out files 1:1 the task can be executed whenever files change (though you might want a cooldown, what grunt seems to call debounce delay).

    for what i'm going to call funnel tasks that map in file to out files n:m where n > m (usually m = 1), i don't want the task run several times

Sub-Package Inclusion

i want to be able to include sub-packages that live under the project tree (most often as git sub-modules during co-development).



Dropped Gulp tasks as a CLI front-end.


API changes to do with upgrading from Webpack 1 to 2 and having Webpack directly read JavaScript source files instead of using the Babel transformed ES3 versions.


Changed how Mocha tasks are configured:



This was started to work on the task dependency graph stuff that never really materialized.


The current version, which has the web UI.


ugh before the web UI was added. i don't indent to keep developing on that branch, but it's there in case it's needed for some reason. v0.2 is almost totally compatible, so i don't see any reason for packages to stay depended on v0.1 versions.