buddy is a fast and simple build tool for the web. It compiles source code from higher order JS/CSS/HTML languages, resolves dependencies, and bundles all sources for more efficient delivery to the browser.
- 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
- Outputs minified and gzipped file sizes
- Downloads and configures Babel plugins by simply declaring target language version (
version: es5
) - Flattens scope for bundled dependencies to improve browser parse time
- Manually splits bundles with nested builds, removing duplicate dependencies across bundles
- Automatically splits bundles with dynamic
import('foo')
and lazy loaded bundles - Inlines environment variables with
process.env.FOO
- Downloads and configures PostCSS plugins by simply declaring target browser versions (
["last 2 versions", "iOS >= 7"]
)
- Inlines script/style/image assets when tags are marked up with an
inline
attribute - Inlines environment variables with
{FOO}
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: 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
-m, --maps generate js/css source maps
-v, --verbose print all messages for debugging
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.
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:
- buddy-plugin-coffeescript: transform
.coffee
source files to.js
- buddy-plugin-typescript: transform
.ts
and.tsx
source files to.js
- buddy-plugin-dust: transform
.dust
html template source files to.html
- buddy-plugin-handlebars: transform
.handlebars
html template source files to.html
- buddy-plugin-nunjucks: transform
.nunjucks
html template source files to.html
- buddy-plugin-less: transform
.less
source files to.css
- buddy-plugin-stylus: transform
.styl
source files to.css
Follow the plugins guide to learn about writing your own.
- Manage JS dependencies?
- Manage CSS dependencies?
- Manage HTML dependencies?
- Specify target JS versions?
- Specify target CSS versions?
- Break-up JS bundles into smaller files?
- Automatically generate a JS bundle based on the content of child bundles?
- Use source maps?
- Lazily evaluate a JS bundle?
- Inline environment variables?
- Avoid writing relative dependency paths?
- Alias a dependency?
- Build React (.jsx) source?
- Write JS with Flow types?
- Configure Babel?
- Configure PostCSS?
- Configure a plugin?
- Generate unique filenames?
- Skip a build?
- Specify different config for different environments?
- Serve files while developing?
- Reload files while developing?
- Display output file size?
JS dependencies are declared by use of require()
expressions (or import
statement for es6 modules), 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"
}
]
}
}
// src/index.js
const foo = require('./foo');
console.log(foo());
// src/foo.js
module.exports = function foo () {
return 'foo';
};
Resulting in:
/** BUDDY BUILT **/
// ...boilerplate
(function () {
/*== src/foo.js ==*/
$m['src/foo.js'] = { exports: {} };
$m['src/foo.js'].exports = function foo () {
return 'foo';
};
/*≠≠src/foo.js ≠≠*/
/*== src/index.js ==*/
$m['src/index.js'] = { exports: {} };
const _srcindexjs_foo = $m['src/foo.js'].exports;
console.log(_srcindexjs_foo());
/*≠≠src/index.js ≠≠*/
})()
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-inlinedrequire()
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"
}
]
}
}
/* src/index.css */
@import 'foo.css';
body {
color: red;
}
@import './utils/bar.css';
/* src/foo.css */
/* Import from module installed in node_modules */
@import 'normalize.css';
/* src/utils/bar.css*/
p {
color: blue;
}
Resulting in:
/*
normalize.css content here
*/
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.
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>
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 supported:
- es5
- es2015 (alias es6)
- es2016 (alias es7)
- es2017
- 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"]
}
]
}
}
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"]
}
]
}
}
Large JS bundles can be broken up into a collection of smaller bundles by nesting builds. Each build can have one or more children, and any parent modules that are referenced in child builds will not be duplicated:
{
"buddy": {
"build": [
{
"input": "src/libs.js",
"output": "www",
"children": [
{
"input": "src/index.js",
"output": "www"
},
{
"input": "src/extras.js",
"output": "www"
}
]
}
]
}
}
// src/libs.js
const lodash = require('lodash');
// src/index.js
const react = require('react');
// The 'lodash' module will not be included because index.js is a child of libs.js
const lodash = require('lodash');
// src/extras.js
// The 'react' module will be included because extras.js is not a child of index.js
const react = require('react');
// The 'lodash' module will not be included because index.js is a child of libs.js
const lodash = require('lodash');
The same result may also be achieved with dynamic child builds using import()
:
{
"buddy": {
"build": [
{
"input": "src/libs.js",
"output": "www/assets"
}
],
"server": {
"webroot": "www"
}
}
}
// src/libs.js
import('./index.js')
.then((index) => {
console.log('index module loaded');
});
...compiles to:
// www/assets/libs.js
buddyImport('/assets/index-b621480767a88ba492db23fdc85df175.js', 'src/index')
.then((index) => {
console.log('index module loaded');
});
Child builds will be automatically generated and loaded asynchronously at runtime. Note that some environments may require a Promise
polyfill, and that the id's passed to import()
must be statically resolvable strings. It may also be necessary to configure the child bundle url by declaring a webroot
property in buddy.server
config.
A build may be automatically generated based on the content of it's children. For example, to generate a parent bundle based on the of shared dependencies between children, specify a parent build with an input
of 'children:common'
or 'children:shared'
. All dependencies shared between child builds will be moved to the parent bundle:
{
"buddy": {
"build": [
{
"input": "children:shared",
"output": "www/shared.js",
"children": [
{
"input": "src/index.js",
"output": "www"
},
{
"input": "src/extras.js",
"output": "www"
}
]
}
]
}
}
// src/index.js
// The 'lodash' module will be moved to shared.js because it is also used in extras.js
const lodash = require('lodash');
// src/extras.js
// The 'react' module will not be moved to shared.js
const react = require('react');
// The 'lodash' module will be moved to shared.js because it is also used in index.js
const lodash = require('lodash');
Another possiblility is to gather all used node_modules
files into a parent bundle:
{
"buddy": {
"build": [
{
"input": "children:**/node_modules/**/*.js",
"output": "www/shared.js",
"children": [
{
"input": "src/index.js",
"output": "www"
},
{
"input": "src/extras.js",
"output": "www"
}
]
}
]
}
}
// src/index.js
// The 'lodash' module will be moved to shared.js because it is in node_modules
const lodash = require('lodash');
// src/extras.js
// The 'foo' module will not be moved to shared.js
const foo = require('./foo');
// The 'lodash' module will be moved to shared.js because it is in node_modules
const lodash = require('lodash');
Matching patterns follow the rules for glob matching.
Source maps for JS and CSS sources are automatically generated when bundling with the --maps
command flag. Source map files are generated alongside output files with an appended *.map
extension (ex: www/foo.js.map
). You may configure a server.sourceroot
base url to load source map files from, but the source map files must then be manually uploaded/moved.
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",
"children": [
{
"input": "src/index.js",
"output": "www",
"bootstrap": false,
"children": [
{
"input": "src/extras.js",
"output": "www",
"bootstrap": false
}
]
}
]
}
]
}
}
// After loading libs.js, index.js, and extras.js in parallel...
// ...guarantee that index.js is evaluated before extras.js
require('src/index');
require('src/extras');
All references to process.env.FOO
variables are automatically inlined in JS source files, and all references to {FOO}
variables are automatically inlined in HTML 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 (valuebrowser
orserver
)BUDDY_{LABEL or INDEX}_INPUT
: input filepath(s) for target identified withLABEL
orINDEX
(valuefilepath
orfilepath,filepath,...
if multiple inputs)BUDDY_{LABEL or INDEX}_INPUT_HASH
: hash(es) of input file(s) for target identified withLABEL
orINDEX
(valuexxxxxx
orxxxxxx,xxxxxx,...
if multiple inputs)BUDDY_{LABEL or INDEX}_INPUT_DATE
: timestamp(s) of input file(s) for target identified withLABEL
orINDEX
(value000000
or000000,000000,...
if multiple inputs)BUDDY_{LABEL or INDEX}_OUTPUT
: output filepath(s) for target identified withLABEL
orINDEX
(valuefilepath
orfilepath,filepath,...
if multiple outputs)BUDDY_{LABEL or INDEX}_OUTPUT_HASH
: hash(es) of output file(s) for target identified withLABEL
orINDEX
(valuexxxxxx
orxxxxxx,xxxxxx,...
if multiple outputs)BUDDY_{LABEL or INDEX}_OUTPUT_DATE
: timestamp(s) of output file(s) for target identified withLABEL
orINDEX
(value000000
or000000,000000,...
if multiple outputs)BUDDY_{LABEL or INDEX}_OUTPUT_URL
: url(s) of output file(s) for target identified withLABEL
orINDEX
(value/xxx/xxxx
or/xxx/xxxx,/xxx/xxxx,...
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:
// src/service-worker.js
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:
// service-worker.js
const VERSION = 'c71a077b25a6ee790a4ce328fc4a0807';
const ASSET_JS = 'www/index-03d534db2f963c0829b5115cef08fcce.js';
const ASSET_CSS = 'www/index-cf4e0949af42961334452b1e11fe1cfd.css';
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.
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
package.json field. 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
}
}
In addition to the standard behaviour of remapping a module to a file path, buddy extends this concept to allow renaming a project module's id reference:
{
"browser": {
"extra-libs": "./src/extra/libs.js"
}
}
// src/index.js
const bar = require('extra-libs'); // Instead of './src/extra/libs'
This can be especially useful when using child bundles.
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"]
}
]
}
}
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"]
}
]
}
}
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"]
}
}
}
]
}
}
PostCSS is configured via the options.postcss
build configuration parameter:
{
"buddy": {
"build": [
{
"input": "src/index.css",
"output": "www",
"options": {
"postcss": {
"plugins": ["postcss-color-function"]
}
}
}
]
}
}
Plugins are configured via the options.{plugin}
build configuration parameter:
{
"buddy": {
"build": [
{
"input": "src/index.js",
"output": "www",
"options": {
"uglify": {
"compressor": {
"drop_debugger": true
}
}
}
},
{
"input": "src/index.css",
"output": "www",
"options": {
"cssnano": {
"normalizeUrl": false
}
}
}
]
}
}
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"
}
]
}
}
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"
}
]
}
}
# Build everything except 'images'
$ buddy build --invert --grep images
Buddy accepts config files that contain named nested sets, and if one of those sets match the value of process.env.NODE_ENV
, the configuration data used in that set will then be used:
{
"buddy": {
"development": {
"build": {
"input": "src/dev.js",
"output": "www"
}
},
"production": {
"build": {
"input": "src/prod.js",
"output": "www"
}
},
"foo": {
"build": {
"input": "src/foo.js",
"output": "www"
}
}
}
}
# Build 'development' set
$ NODE_ENV=development & buddy build
Custom names may also be used and passed when executing:
$ buddy build foo
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"]
}
}
When working with the default static file server, you can pass along custom headers:
"buddy": {
"server": {
"port": 8000,
"directory": "www",
"headers": {
"x-foo": "foo"
}
}
}
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
.
When running the deploy
command, the minified and gzipped file sizes will be automatically output to the terminal. If the minified size exceeds 250 kB, a warning will also be output:
building lib/react-browser.js to lib/react.js
[processed 169 files in 2.52s]
built and compressed src/lib/react.js
compressed size: 332 kB
gzipped size: 60.5 kB
warning the output file exceeds the recommended 250 kB size
Consider splitting into smaller bundles to help improve browser startup execution time