Skip to content
This repository has been archived by the owner on Nov 8, 2022. It is now read-only.

popeindustries/buddy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NPM Version Build Status Downloads

buddy

Helping you get sh*t done since 2010

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.

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
    • Outputs minified and gzipped file sizes
JS Specific Features
  • 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
CSS Specific Features
  • Downloads and configures PostCSS plugins by simply declaring target browser versions (["last 2 versions", "iOS >= 7"])
HTML Specific Features
  • Inlines script/style/image assets when tags are marked up with an inline attribute
  • Inlines environment variables with {FOO}

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
    -m, --maps            generate js/css source maps
    -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 (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-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"
      }
    ]
  }
}
/* 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.

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 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"]
      }
    ]
  }
}

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 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.

Automatically generate a JS bundle based on the content of child bundles?

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.

Use source maps?

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.

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",
        "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');

Inline environment variables?

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 (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_{LABEL or INDEX}_OUTPUT_URL: url(s) of output file(s) for target identified with LABEL or INDEX (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';

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 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.

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
            }
          }
        }
      },
      {
        "input": "src/index.css",
        "output": "www",
        "options": {
          "cssnano": {
            "normalizeUrl": false
          }
        }
      }
    ]
  }
}

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"
      }
    ]
  }
}
# Build everything except 'images'
$ buddy build --invert --grep images

Specify different config for different environments?

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

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"]
  }
}

When working with the default static file server, you can pass along custom headers:

"buddy": {
  "server": {
    "port": 8000,
    "directory": "www",
    "headers": {
      "x-foo": "foo"
    }
  }
}

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.

Display output file size?

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

About

A simple build tool for html/js/css projects.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages