About Our Build Process

Cantilever strives for simplicity in development workflows. We frequently opt for less powerful or more manual tools in order to streamline and clarify the process of working on one of our projects. Our build processes epitomize this. As of 2018 we have transitioned to using NPM scripts as our default build tool. Most projects will use the following components:

  • Vagrant (Homestead) for local environment
  • Browsersync for hot reloading and LAN sharing
  • ESLint and StyleLint support
  • LESS for CSS pre-processing (We’re phasing this out soon)

Overall Philosophy

  • Code should live at its intended, final location as often as possible. The build process should move as few files around as it can.
  • We want to work with code as it will be delivered to clients, to whatever extent possible. The build process should only modify code if completely necessary.
  • Optimization should be a part of the delivery strategy, not the coding strategy. The build process should optimize only things that cannot be optimized at the server/CDN level (no minification)
  • The build should be fast and almost unnoticible.

Our old build process abstracted out every "working" file in a project into a tree structure inside a "source" directory. We have moved away from this practice lately, towards a setup where we work mainly with the true working files wherever they happen to be within the project structure. The build process only has to compile/process files which cannot be edited "raw" for whatever reason.


Ex. Antipattern: A folder of JS libs that gets copied to the public directory on every run of the build process. Simply keep those libs in the public directory in the repo instead.

About Node Build Processes

Node.js comes with the ability to run node modules from the command line via the "scripts" key in package.json. This provides simple aliases for complex commands. The commands are basic bash but also have access to the installed node modules, making this a clean and easy way to define build scripts. Here is a basic dossier on how and why NPM works great as a build tool:

Here are some neat tricks you can use in these tools:

Why Not Grunt/Webpack?

Task runners complicate the act of running a simple node module. While the consistent API of a task runner is attractive, accessing that ability requires installing and maintaining not only Grunt/Webpack, but all of the Grunt/Webpack-specific wrappers for the actual Node packages we want to use.

Grunt frequently requires several lines of code to accomplish what one line of Bash can do. On a very apples-to-apples test, Grunt is faster than raw NPM, but even on hefty build processes, the NPM-only approach has held up well for us, while Grunt projects have ballooned into lengthy build durations. Using NPM allows us to be closer to the commands that are actually running, which is a Cantilever principle.

Why Node?

Frequently, our projects don’t use node aside from the build process. While we recognize that requiring node in a project just for the build is not ideal, there is no sensible alternative on our radar that is cross-platform, regularly maintained, and easy to use. If such a thing appears, we’ll be eager to switch.

Sample Build Process

"config": {
  // Other stuff goes here
	"scripts": {
    "setup": "npm i && composer install",
    "browser-sync": "browser-sync start --proxy https://philosophy.cantilever.test --files 'public/wp-content/themes/cl-philosophy/' --files 'public/assets/' --https=true --reloadDebounce=50",
    "styles": "lessc --glob --source-map-map-inline source/less/app.less | postcss -u autoprefixer -o public/assets/css/app.css",
    "lint-styles": "stylelint source/less/**/*.less --syntax less",
    "work:scripts": "esw --watch public/assets/js --ignore-pattern public/assets/js/lib",
    "work:styles": "onchange source/less/ -- npm run styles && npm run lint-styles",
    "work": "vagrant up && npm-run-all --parallel browser-sync work:*",
    "db:pull-prod": "export $(cat .env | xargs) && wp @dev migratedb pull https://philosophy.cantilever.co $MDP_PROD_KEY --find=//philosophy.cantilever.co,/home/forge/philosophy.cantilever.co/public --replace=//philosophy.cantilever.test,/home/vagrant/code/public --skip-replace-guids --url=philosophy.cantilever.test",
    "db:pull-prod-with-media": "export $(cat .env | xargs) && wp @dev migratedb pull https://philosophy.cantilever.co $MDP_PROD_KEY --find=//philosophy.cantilever.co,/home/forge/philosophy.cantilever.co/public --replace=//philosophy.cantilever.test,/home/vagrant/code/public --skip-replace-guids --url=philosophy.cantilever.test --media=compare-and-remove",
    "wp:update": "wp @dev core update && wp @dev plugin update --all && wp @dev theme delete twentynineteen twentytwenty",
    "sync": "git checkout master && git fetch upstream && git merge upstream/master"
  • This short snippet of code handles project setup, transpiling, linting, and hot reloading through BrowserSync. The Grunt equivalent was literally 10x as long.
  • Colons denote sub-tasks of a larger category. They have no functional effect aside from working nicely with npm-run-all (notes to follow)
  • Here, the `scripts` task handles watching script files for changes. The `scripts:watch` is watching for changes in source scripts to run our linter.
  • Most tasks run directly with npm run my-task. NPM allows you to run specially-named tasks like start without the run. So to start working, you run npm start.

Example Projects

Tips and Tricks

  • To pass the output of a task from one part to another, use a pipe character.
  • In an onchange task, if the file path is unquoted, the onchange task will find the target files when it is started, and will watch those files. If the file path is quoted, the onchange task will read the file structure anew each time it runs, so new files are caught.
  • You can run commands inside a vagrant box by doing vagrant ssh -- whatever. So vagrant ssh -- ls will show you the contents of the vagrant box’s home directory. Wild!