buddy
Helping you get sh*t done since 2010
buddy is a fast and simple build tool for web projects. It can compile source code from higher order JS/CSS/HTML languages, resolves dependencies, and bundle (and optionally compress) all sources for more efficient delivery to the browser.
Features
- Resolves and manages JS/CSS/HTML dependencies, efficiently packaging resources into bundled files
- Transforms other languages to JS/CSS/HTML
- Built around Babel and PostCSS: installs and configures plugins automatically based on target language version
- For development:
- Watches source files for changes
- Runs a static file server (or a custom application server)
- Refreshes connected browsers
- For production:
- Outputs unique filenames
- Compresses sources, including images
Installation
Install buddy as a devDependency
in your project directory:
$ npm install --save-dev buddy
If you want a global buddy command, install the buddy-cli with $ npm install --global buddy-cli
Usage
Usage: buddy [options] <command> [configpath]
Commands:
build [configpath] build js, css, html, and image sources
watch [configpath] watch js, css, html, and image source files and build changes
deploy [configpath] build compressed js, css, html, and image sources
Options:
-h, --help output usage information
-V, --version output the version number
-c, --compress compress output for production deployment
-g, --grep <pattern> only run build targets matching <pattern>
-i, --invert inverts grep matches
--input input file/directory for simple config-free build
--output output file/directory for simple config-free build
-r, --reload reload all connected live-reload clients on file change during watch
-s, --serve create (or launch) a webserver to serve files during watch
-S, --script run script on build completion
-v, --verbose print all messages for debugging
Configuration
buddy is configurable via js
or json
formatted configuration files. By default, buddy looks for the nearest buddy.js
, buddy.json
, or package.json
(with a buddy
entry). Alternatively, you can specify the path to your configuration file while running the buddy
command.
Note that, whichever way you configure it, buddy will treat the directory that contains the configuration file as the project root.
Please refer to the annotated configuration guide to see all the different options.
Plugins
buddy's ability to transform and manipulate different source files is made possible by a flexible plugin system. In fact, all of the core language features are implemented as plugins internally, so there should be very few features that cannot be implemented this way.
One of the most common use cases for extending buddy is to enable working with higher-order JS/CSS/HTML languages. The following plugins can be installed ($ npm install --save-dev {plugin}
) if you prefer not to write vanilla JS/CSS/HTML:
Follow the plugins guide to learn about writing your own.
How do I...
Manage JS dependencies?
JS dependencies are declared by use of require()
expressions, and closely follow the module semantics as used in Node.js. This makes it possible to write modules for the browser the same way as you would for Node.js server environments. Although buddy preserves similar author-time semantics, run-time behaviour does differ. In Node.js modules, each file is wrapped in a function closure to provide an isolated scope for module-level variable/function/class declarations, ensuring that there are no conflicts between modules. In the browser, however, wrapping each module in a closure can impose significant start-up cost and overhead. As a result, for performance reasons, buddy flattens all modules into a shared scope, renames all declarations, and inlines all calls to require()
:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www"
}
]
}
}
const foo = require('./foo');
console.log(foo());
module.exports = function foo () {
return 'foo';
};
Resulting in:
if ('undefined' === typeof self) var self = this;
if ('undefined' === typeof global) var global = self;
if ('undefined' === typeof process) var process = { env: {} };
var $m = self.$m = self.$m || {};
var require = self.require || function require (id) {
if ($m[id]) {
if ('function' == typeof $m[id]) $m[id]();
return $m[id].exports;
}
if ('test' == 'development') {
console.warn('module ' + id + ' not found');
}
};
(function () {
$m['src/foo.js'] = { exports: {} };
$m['src/foo.js'].exports = function foo () {
return 'foo';
};
$m['src/index.js'] = { exports: {} };
const _srcindexjs_foo = $m['src/foo.js'].exports;
console.log(_srcindexjs_foo());
})()
Although these optimizations are possible to apply in most cases, there are two scenarios where buddy needs to de-optimize by wrapping module contents and/or preserving calls to require()
:
- referencing modules in another bundle:
require('module-from-another-bundle')
will be preserved as it cannot be safely inlined (read more about working with multiple bundles) - circular dependencies: modules that
require
each other (including several orders removed) will be wrapped in a closure function and lazily evaluated when eventually called with a non-inlined require()
Manage CSS dependencies?
CSS dependencies are declared by use of the @import
statement. buddy replaces these statements with the referenced file contents, inlining a file's dependencies rather than concatenating them:
{
"buddy": {
"build": [
{
"input": "src/index.css",
"output": "www"
}
]
}
}
@import 'foo.css';
body {
color: red;
}
@import './utils/bar.css';
@import 'normalize.css';
p {
color: blue;
}
Resulting in:
body {
color: red;
}
p {
color: blue;
}
Note that, while a JS dependency tree can be optimized to avoid duplicates, the cascading nature of CSS requires that dependency order be strictly observed, and as a result, duplicate @import
statements will result in duplicate file content.
Manage HTML dependencies?
Although HTML dependencies are numerous and varied, buddy only manages a specific subset of dependencies that are flagged for inlining. Specifying an inline
attribute on certain tags results in the file contents being copied into the HTML:
{
"buddy": {
"build": [
{
"input": "src/index.html",
"output": "www"
}
]
}
}
<!DOCTYPE html>
<html>
<head>
<link inline rel="stylesheet" href="src/index.css">
<script inline src="src/index.js"></script>
</head>
<body>
<img inline src="src/image.svg">
</body>
</html>
Resulting in:
<!DOCTYPE html>
<html>
<head>
<style>
body {
color: red;
}
</style>
<script>
console.log('foo');
</script>
</head>
<body>
<svg>
<circle cx="50" cy="50" r="25"/>
</svg>
</body>
</html>
Specify target JS versions?
Since buddy uses Babel to transform JS sources, it is easy to target a specific version of JavaScript you want to output to. Specifying one or more output versions simply loads the appropriate Babel plugins required to generate the correct syntax. If one or more of the plugins have not yet been installed, buddy will automatically install them to your dev-dependencies
:
{
"buddy": {
"build": [
{
"input": "src/browser.js",
"output": "www",
"version": "es5"
},
{
"input": "src",
"output": "dist",
"bundle": false,
"version": "node6"
}
]
}
}
The following JS version targets are valid:
- es5
- es2015 (alias es6)
- es2016 (alias es7)
- node4
- node6
In addition to generic language/environment versions, buddy also supports browser version targets, and Autoprefixer-style browser list configuration:
{
"buddy": {
"build": [
{
"input": "src/chrome.js",
"output": "www",
"version": {
"chrome": 50
}
},
{
"input": "src/browsers.js",
"output": "www",
"version": ["last 2 versions", "iOS >= 7"]
}
]
}
}
Specify target CSS versions?
Since buddy uses PostCSS and Autoprefixer to transform CSS sources, it is easy to target specific browser versions (via vendor prefixes) you want to output to:
{
"buddy": {
"build": [
{
"input": "src/index.css",
"output": "www",
"version": ["last 2 versions", "iOS >= 7"]
}
]
}
}
Break-up JS bundles into smaller files?
Large JS bundles can be broken up into a collection of smaller bundles by nesting builds. Each build can have one or more child builds, and any parent modules that are referenced in child builds will not be duplicated:
{
"buddy": {
"build": [
{
"input": "src/libs.js",
"output": "www",
"build": [
{
"input": "src/index.js",
"output": "www"
},
{
"input": "src/extras.js",
"output": "www"
}
]
}
]
}
}
const lodash = require('lodash');
const react = require('react');
const lodash = require('lodash');
const react = require('react');
const lodash = require('lodash');
Lazily evaluate a JS bundle?
By default, js modules in a bundle are evaluated in reverse dependency order as soon as the file is loaded, with the input
module evaluated and executed last. Sometimes, however, it is useful to delay evaluation and execution until a later time (so-called lazy evaluation). For example, when loading several bundles in parallel, it may be important to have more control over the order of evaluation:
{
"buddy": {
"build": [
{
"input": "src/libs.js",
"output": "www",
"build": [
{
"input": "src/index.js",
"output": "www",
"bootstrap": false,
"build": [
{
"input": "src/extras.js",
"output": "www",
"bootstrap": false
}
]
}
]
}
]
}
}
require('src/index.js');
require('src/extras.js');
Inline environment variables?
All references to process.env.*
variables are automatically inlined in JS source files. In addition to all the system variables set before build, the following special variables are set during build:
RUNTIME
: current runtime for browser code (value browser
or server
)BUDDY_{LABEL or INDEX}_INPUT
: input filepath(s) for target identified with LABEL
or INDEX
(value filepath
or filepath,filepath,...
if multiple inputs)BUDDY_{LABEL or INDEX}_INPUT_HASH
: hash(es) of input file(s) for target identified with LABEL
or INDEX
(value xxxxxx
or xxxxxx,xxxxxx,...
if multiple inputs)BUDDY_{LABEL or INDEX}_INPUT_DATE
: timestamp(s) of input file(s) for target identified with LABEL
or INDEX
(value 000000
or 000000,000000,...
if multiple inputs)BUDDY_{LABEL or INDEX}_OUTPUT
: output filepath(s) for target identified with LABEL
or INDEX
(value filepath
or filepath,filepath,...
if multiple outputs)BUDDY_{LABEL or INDEX}_OUTPUT_HASH
: hash(es) of output file(s) for target identified with LABEL
or INDEX
(value xxxxxx
or xxxxxx,xxxxxx,...
if multiple outputs)BUDDY_{LABEL or INDEX}_OUTPUT_DATE
: timestamp(s) of output file(s) for target identified with LABEL
or INDEX
(value 000000
or 000000,000000,...
if multiple outputs)
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www/index-%hash%.js",
"label": "js"
},
{
"input": "src/index.css",
"output": "www/index-%hash%.css",
"label": "css"
},
{
"input": "src/service-worker.js",
"output": "www",
"label": "sw"
}
]
}
}
The last target (labelled sw
) will have access to the unique outputs of the previous targets:
const VERSION = process.env.BUDDY_SW_INPUT_HASH;
const ASSET_JS = process.env.BUDDY_JS_OUTPUT;
const ASSET_CSS = process.env.BUDDY_CSS_OUTPUT;
...which converts to:
const VERSION = 'c71a077b25a6ee790a4ce328fc4a0807';
const ASSET_JS = 'www/index-03d534db2f963c0829b5115cef08fcce.js';
const ASSET_CSS = 'www/index-cf4e0949af42961334452b1e11fe1cfd.css';
Avoid writing relative dependency paths?
Since buddy implements the same dependency resolution semantics as Node.js, it is possible to end up with unwieldy relative paths when referencing files from deeply nested project directories: require('../../../../some-module')
. And as for Node.js, you have a choice between the following two workarounds:
- nest your project source files in a
node_modules
directory:
project/
node_modules/ (installed with npm)
src/
node_modules/ (manually created)
app/
libs/
- add your project source directory to the
$NODE_PATH
environment variable:
$ NODE_PATH=./src buddy watch
Allowing you to require('libs/some-module')
from anywhere in your project directory structure.
Alias a dependency?
When writing universal modules for use in both server and browser environments, it is sometimes desirable to specify an alternative entry point for inclusion in the browser. The alternative to the main
package.json parameter is browser
:
{
"name": "myModule",
"version": "1.0.0",
"main": "lib/server.js",
"browser": "lib/browser.js"
}
buddy correctly handles this remapping when resolving node_modules dependencies that use the browser
parameter. In addition, it is possible to employ more advanced uses to alias files and modules directly in your project:
{
"browser": {
"someModule": "node_modules/someModule/dist/someModule-with-addons.js"
}
}
...or even disable a module completely when bundling for the browser:
{
"browser": {
"someModule": false
}
}
Read more about the possible uses of browser
.
Build React (.jsx) source?
A React language plugin is provided by default. Just specify react
as a build target version to compile .jsx
files:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"version": ["es5", "react"]
}
]
}
}
Write JS with Flow types?
A Flow plugin is provided by default. Just specify flow
as a build target version to strip Flow types from .js
files:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"version": ["es5", "flow"]
}
]
}
}
Configure Babel?
Babel is configured via the options.babel
build configuration parameter:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"options": {
"babel": {
"plugins": [["babel-plugin-transform-es2015-classes", { "loose": false }]],
"presets": ["my-cool-babel-preset"]
}
}
}
]
}
}
Configure PostCSS?
PostCSS is configured via the options.postcss
build configuration parameter:
{
"buddy": {
"build": [
{
"input": "src/index.css",
"output": "www",
"options": {
"postcss": {
"plugins": ["postcss-color-function"]
}
}
}
]
}
}
Configure a plugin?
Plugins are configured via the options.{plugin}
build configuration parameter:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"options": {
"uglify": {
"compressor": {
"drop_debugger": true
}
}
}
}
]
}
}
Generate unique filenames?
Unique filenames can be automatically generated by including one of two types of token in the output filename:
- %date%: inserts the current time stamp at the time of build
- %hash%: inserts a hash of the file's content
{
"buddy": {
"build": [
{
"input": "somefile.js",
"output": "somefile-%hash%.js"
},
{
"input": "somefile.css",
"output": "somefile-%date%.css"
}
]
}
}
Unique filenames are generally recommended as a cache optimisation for production deploys, so it's often a good idea to only specify a unique name when compressing:
{
"buddy": {
"build": [
{
"input": "somefile.js",
"output": "www",
"output_compressed": "www/somefile-%hash%.js"
}
]
}
}
Skip a build?
Individual builds can be skipped by using the --grep
and --invert
command flags. The --grep
command flag will isolate builds with input
or label
that match the provided pattern, and the --invert
pattern negates the match:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"label": "js"
},
{
"input": "src/index.css",
"output": "www",
"label": "css"
},
{
"input": "src/images",
"output": "www/images",
"label": "images"
}
]
}
}
$ buddy build --invert --grep images
Serve files while developing?
When executing the watch
command with the --serve
flag, buddy will rely on the buddy-server plugin to launch a local development server. If the plugin is not already installed, buddy will automatically install it to your dev-dependencies
.
buddy-server has two primary modes:
A default static file server that serves files from a local directory
:
"buddy": {
"server": {
"port": 8000,
"directory": "www"
}
}
Or a custom application server:
"buddy": {
"server": {
"port": 8000,
"file": "./index.js"
}
}
When working with a custom server, you can pass along application environment variables and flags to the Node.js runtime:
"buddy": {
"server": {
"port": 8000,
"file": "./index.js",
"env": {
"DEBUG": "*"
},
"flags": ["--inspect"]
}
}
Reload files while developing?
When executing the watch
command with the --serve
and --reload
flags, buddy will rely on the buddy-server plugin to launch a local development server, reloading any connected clients after re-builds. If the plugin is not already installed, buddy will automatically install it to your dev-dependencies
.