A tree-based build system.
Juni is a simple build system that acts on file system trees. Plugins are then applied to the tree, and manipulate it in-place.
Juni does not come with a CLI tool, or require you to have a special *file.js
in your project folder. Just write your own build script and run it how you
like.
var juni = require("juni");
new juni.Tree("./source")
.pipe(juni.populate())
// Call more plugins ...
.pipe(juni.output("./build"));
Juni is based around the Tree
class. A Tree
instance represents either a
file or folder on the file system. Typically a plugin will take a tree and
modify, remove, or add children to it.
Get the path to the tree. This is typically the value that was set in the constructor, though plugins can change it.
Set the path to the tree on the file system.
Returns true if the tree is a directory.
Returns true if the tree is a file.
Returns true if the tree has a parent tree. Will be false for the root tree.
Returns the parent tree, or null
if it has none.
Returns true if the tree has child trees. Will be false for files and empty directories.
Returns an array of child trees.
Returns a tree's file contents as a buffer. Only makes sense for file trees.
Set a tree's file contents. contents
should be a buffer or a string. Strings
will be automatically converted to a buffer.
Removes a tree from its parent tree.
Adds a child tree to a tree.
Traverses the tree using a walker. See "Walking" below.
Returns true if the tree matches the matcher. See "Matching" below.
Pass the tree through a transform function. Transform functions are typically returned by plugins. See "Plugins" below.
In a tree-based build system, being able to traverse trees easily and in custom
ways is super important. The Tree.walk
method traverses a tree using walkers
.
Walkers are just functions. They accept a tree and a function, and call the function with each tree node it visits. Which nodes it visits, and in what order are up the the walker.
Walkers can also return a value, making them a powerful way to extract nodes from the tree or compute values based on the tree.
If that sounds complicated, don't worry. You really only need to use walkers when building plugins, and even then you can probably just use the walkers bundled along with Juni.
Visit each item in the tree with a pre-order traversal.
This traversal ensures that all parent nodes will be visited before their children. Useful for copying the tree, for example to the file system.
Example usage:
var juni = require("juni");
var tree = new juni.tree("/my/path")
.pipe(populate());
tree.walk(juni.walk.pre(function(item)){
console.log(item.path());
});
A folder stucture like:
/my/path/scripts
/my/path/scripts/app.coffee
/my/path/scripts/main.js
/my/path/scripts/vendor
/my/path/scripts/vendor/jquery.js
will output:
/my/path/scripts
/my/path/scripts/app.coffee
/my/path/scripts/main.js
/my/path/scripts/vendor
/my/path/scripts/vendor/jquery.js
Visit each item in the tree with a post-order traversal.
This traversal ensures that all children nodes will be visited before their parents. Useful when removing nodes from the tree.
Example usage:
var juni = require("juni");
var tree = new juni.tree("/my/path")
.pipe(populate());
tree.walk(juni.walk.post(function(item)){
console.log(item.path());
});
A folder stucture like:
/my/path/scripts
/my/path/scripts/app.coffee
/my/path/scripts/main.js
/my/path/scripts/vendor
/my/path/scripts/vendor/jquery.js
will output:
/my/path/scripts/vendor/jquery.js
/my/path/scripts/vendor
/my/path/scripts/main.js
/my/path/scripts/app.coffee
/my/path/scripts
Matchers can be used to see if a tree node matches some specific queries. For example you can check if the filename contains "foo", or see if the modified date is over a day ago.
The Tree.match
method uses matchers
to decide if it returns true or false.
Similar to walkers, matchers are just functions. They accept a tree and return
true or false.
The matchers that come bundled with Juni all accept a query
, which is a
string, regex, function, or an array of those. Strings and regular expressions
are used to match against the tree filename.
If you want to match against something else, you can pass in your own function. A tree node is passed in and you can return true or false based on that.
Returns true if any query matches the tree node.
var match = tree.match(juni.match.any(["foo", /^_/]));
// tree.path() == "foolhardy" => true
// tree.path() == "_sadpanda" => true
// tree.path() == "banana stand" => false
Returns true if all queries match the tree node.
var match = tree.match(juni.match.all(["foo", /^_/]));
// tree.path() == "foolhardy" => false
// tree.path() == "_sadpanda" => false
// tree.path() == "banana stand" => false
// tree.path() == "_eatfood" => true
If just matching against the filename isn't enough, you can pass a function to the bundled matchers:
var inFooPathQuery = function(tree) {
return tree.path().indexOf("foo/") !== -1;
};
tree.match(juni.match.all(inFooPathQuery));
Juni plugins are used with the Tree.pipe
method.
Recursively builds a tree containing file system items in the current tree's path. You basically always want to call this after creating a new tree.
var tree = new juni.Tree("/my/path");
tree.hasChildren() // => false
tree.pipe(juni.populate());
tree.hasChildren() // => true
Prints a tree to the console. Useful for debugging.
Writes the tree to the file system at the dest
path.
// Copy all files from source to build
new juni.Tree("./source")
.pipe(populate())
.pipe(output("./build"));
The Tree.pipe
method expects a function that accepts a Tree
object. The
return value doesn't matter. We call this a transform
function.
Typically a plugin might want to accept options or other input though, so by convention, a Juni plugin is a function that accepts any arguments and returns a transform function. An example makes this clearer:
// Here is a simple transform function that will append some text to all files
// in a juni tree.
var appender = function(tree) {
tree.walk(juni.walk.pre(function(item){
if (item.isFile()) {
var contents = item.contents().toString();
item.setContents(contents + "Appender was here!");
}
}));
};
tree.pipe(appender);
// This would be much more useful if we could specifiy the text to append.
// Let's make a simple plugin
var appender = function(appendString) {
// Return a transform function
return function(tree) {
tree.walk(juni.walk.pre(function(item){
if (item.isFile()) {
var contents = item.contents().toString();
item.setContents(contents + appendString);
}
}));
};
};
// Our appender plugin is now a plugin--it has to be called before it is passed
// to `Tree.pipe`.
var dateAppender = appender(new Date());
tree.pipe(dateAppender);
// or simply
tree.pipe(appender(new Date()));