Cartero
In the year 2013, why do we still organize our web assets like it's 1990, grouping them together in big directories separated by their type? Instead, why don't we leverage directories more effectively to put files together that really belong together? For example, why don't we put JavaScript and stylesheet assets that are just used by one particular page in the same directory as that page's template? And what about closely related assets like personModel.js, personView.js, person.css, etc.? Why don't we keep those together in a single "person" directory, instead of spreading them out all over the place? It sure would be nice to be able to quickly switch between "person" files just by clicking on another file in the same directory! And it sure would be nice to "require" all the "person" assets just by referencing the "person" directory, instead of each individual asset!
One of the obstacles has been that asset management has a lot of moving parts. A complete general solution needs to address preprocessing (i.e. compiling .scss, .coffee, etc.) for arbitrary asset types, minification and concatenation in production mode, and dependency management.
Cartero works on top of Grunt.js and optionally together with Bower, addressing these issues so that we can more effectively organize assets, optimize their delivery, and scale up applications.
- Group your assets into "bundles" of related JavaScript files, stylesheets, templates, and even images. Then just specify the bundles that each page requires.
- Keep assets for a particular page with that page's template to automatically serve them with the page.
- All necessary
<script>
and <link>
tags are generated for you.
- Bundle dependencies (and inter-bundle dependencies) are resolved.
- In development mode, served assets are preprocessed, but not minified or concatenated.
- In production mode, served assets are preprocessed, minified and concatenated.
- Large asset bundles can optionally be kept separate for optimal loading and caching.
- Use your preferred JavaScript module system, e.g. RequireJS, Marionette Modules, or even CommonJS!
- Integrates with Bower, automatically resolving dependencies in
bower.json
files.
Cartero is JavaScript framework, stylesheet and templating language agnostic. It also almost works with any web framework – the very small "hook" of runtime logic is easy to port to any web framework, but is currently only available for Node.js / Express. Instructions for writing a Hook for another framework are below.
Overview
The Asset Library
Keep all your assets, regardless of type, in your application's Asset Library (except for assets that are just used by a particular page, which can be stored with that page's template - see below). Each subdirectory of your Asset Library defines a Bundle that may contain JavaScript files, stylesheets, templates, and images.
assetLibrary/
dialogs/
dialogManager.coffee
editPersonDialog/
bundle.json = { dependencies : [ "dialogs" ] }
editPersonDialog.coffee
editPersonDialog.scss
editPersonDialog.tmpl
Here, the editPersonDialog
bundle depends on the dialogs
bundle because of its bundle.json
file (contents inlined). Dependencies (and other bundle meta-data) can be specified either in bundle.json
files that live in bundle directories themselves, in an external bundle meta-data file, or implicitly through the directory structure (see the childrenDependOnParents
option).
Page specific assets
Keep assets that are just used by one particular page in the same directory as the page's template, and they will be automatically be included when it is rendered. No more messing with <script>
and <link>
tags! For example, say your page templates live in a directory named views
:
views/
login/
login.jade
login.coffee
login.scss
When the login.jade
template is rendered, the compiled login.coffee
and login.scss
assets will automatically be included. A page can also "extend" on the assets required by another page.
Using Cartero with Bower
The Bower components
directory can also be used as an Asset Library, for example:
app/
dialogs/
bundle.json = { dependencies : [ "components/jquery-ui" ] }
dialogManager.coffee
editPersonDialog/
bundle.json = { dependencies : [ "app/dialogs" ] }
editPersonDialog.coffee
editPersonDialog.scss
editPersonDialog.tmpl
components/
jquery-ui
bower.json = { "dependencies": { "jquery": "~> 1.10.1" }, ... }
...
jquery
...
Bower dependencies are automatically resolved, so when the app/editPersonDialog
bundle is required by a page, the components/jquery-ui
and components/jquery
bundles will also be included automatically. Note that since Bower packages generally contain extra files like unit tests, you also need to tell Cartero which assets from each Bower package should be used with the whitelistedFiles
option. Also, you'll want to set the allowNestedBundles
flag to false
for the components
directory, since the Bower namespace is flat.
Getting started
First, install Cartero via npm:
npm install cartero
Now configure the Cartero Grunt Task in your applcation's gruntfile. (If you haven't used Grunt before, read this.) Here is the minimal configuration that is required to run the Cartero Grunt Task (all options shown are required):
module.exports = function( grunt ) {
grunt.initConfig( {
cartero : {
options : {
projectDir : __dirname,
library : {
path : "assetLibrary/"
},
views : {
path : "views/",
viewFileExt : ".jade"
}
publicDir : "static/",
tmplExt : ".tmpl",
mode : "dev"
}
dev : {},
prod : {
options : {
mode : "prod"
}
}
}
} );
grunt.loadNpmTasks( "cartero" );
grunt.loadNpmTasks( "grunt-contrib-watch" );
};
The Cartero Grunt Task also takes options that allow you to call arbitrary preprocessing and minification tasks (to compile .scss, uglify JavaScript, etc.), and more. See the reference section for a complete list of options for the Cartero task.
Once you have configured the Cartero Grunt Task, you need to configure the Hook in your web framework. The Hook is a small piece of runtime logic that resides in your web application framework. It uses the output of the Cartero Grunt Task to provide your application with the raw HTML that loads the assets on each page. As of this writing there is only a Hook available for Node.js / Express, which is implemented as Express middleware. To install it, run:
npm install cartero-express-hook
Then use
it, passing the absolute path of your project directory (i.e. the projectDir
option from the gruntfile configuration).
var app = express();
var carteroMiddleware = require( "cartero-express-hook" );
app.configure( function() {
app.set( "port" , process.env.PORT || 3000 );
app.set( "views" , path.join( __dirname, "views" ) );
app.use( express.static( path.join( __dirname, "static" ) ) );
app.use( carteroMiddleware( __dirname ) );
} );
Now you are ready to go. To let Cartero know which asset bundles are required by which pages, you use Directives. The Cartero Grunt Task scans your page template files for these Directives, which have the form ##cartero_xyz
. The ##cartero_requires
Directive is used to declare dependencies:
// peopleList.jade
// ##cartero_requires "dialogs/editPersonDialog"
doctype 5
html(lang="en")
head
title login
| !{cartero_js}
| !{cartero_css}
body
| !{cartero_tmpl}
h1 People List
// ...
Notice the three variables that Cartero makes available to your template engine:
cartero_js
- the raw HTML of the <script>
elements that load all the required JavaScript files.
cartero_css
- the raw HTML of the <link>
elements that load all the required CSS files.
cartero_tmpl
- the raw, concatenated contents of all the required client side template files.
When you run either of the following commands from the directory of your gruntfile:
grunt cartero:dev --watch
grunt cartero:prod
The Cartero Grunt Task will fire up, process all of your assets, and generate the output used by the Hook. The dev
mode --watch
flag tells the Cartero Grunt Task to watch all of your assets for changes and reprocess them as needed. In prod
mode, the task will terminate after minifying and concatenating your assets. In either mode, when you load a page, the three variables cartero_js
, cartero_css
, and cartero_tmpl
with be available to the page's template, and will contain all the raw HTML necessary to load the assets for the page.
Reference
Cartero Grunt Task Options
options : {
"projectDir" : __dirname,
"library" : {
path : "assetLibrary/",
bundleProperties : grunt.file.readJSON( "bundleProperties.json" ),
namespace : "app"
allowNestedBundles : true,
directoriesToFlatten : /^_.*/,
childrenDependOnParents : true
},
"views" : {
path : "views/",
viewFileExt : ".jade",
filesToIgnore : /^__.*/,
directoriesToIgnore : /^__.*/,
directoriesToFlatten : /^_.*/,
namespace : "app"
}
"publicDir" : "static/",
"mode" : "dev",
"preprocessingTasks" : [ {
name : "coffee",
inExt : ".coffee",
outExt : ".js",
options : {
sourceMap : true
}
}, {
name : "sass",
inExt : ".scss",
outExt : ".css"
} ],
"minificationTasks" : [ {
name : "htmlmin",
inExt : ".tmpl",
options : {
removeComments : true
}
}, {
name : "uglify",
inExt : ".js",
options : {
mangle : false
}
} ],
browserify : true
}
Properties of bundle.json
Each of your bundles may contain a bundle.json
file that specifies meta-data about the bundle, such as dependencies. (Note: An actual bundle.json file, since it is simple JSON, can not contain JavaScript comments, as does the example.) By using the bundleProperties
grunt taks option, you can alternatively specify this meta-data for all bundles in a central location.
{
"dependencies" : [ "JQuery" ],
"whitelistedFiles" : [ "backbone.js" ],
"filePriority" : [ "backbone.js" ],
"directoriesToFlatten" : [ "mixins" ],
"prioritizeFlattenedDirectories" : false,
"keepSeparate" : true,
"devModeOnlyFiles" : [ "mixins/backbone.subviews-debug.js" ],
"prodModeOnlyFiles" : [ "mixins/backbone.subviews.js" ],
"dynamicallyLoadedFiles" : [ "ie-8.css" ],
"browserify_executeOnLoad" : [ "backbone.js" ]
}
Directives
##cartero_requires bundleName_1, [ bundleName_2, ... ]
This Directive is used in server side templates to specify which bundles they require. Bundles are referred to by their name, which is the full path of their folder, relative to the Asset Library directory in which they reside. If the Asset Library directory has a namespace
property, that namespace should be pre-pended to the bundle name. Generally you will want to enclose the Directive in a "comment" block of whatever template language you are using, as shown here (.erb syntax).
<%# ##cartero_requires "app/dialogs/editPersonDialog" %>
<%# All dependencies are automatically resolved and included %>
##cartero_extends parentView
This Directive is used in server side templates to specify that one template should "inherit" all of the assets of another. parentView must be a path relative to the view directory (pre-pended with the view directory's namespace
, if it has one).
<%# ##cartero_extends "layouts/site_layout.twig" %>
##cartero_dir
When your assets are processed, this Directive is replaced with the path of the directory in which it appears. It is similar in concept to the node.js global __dirname
, but the path it evaluates to is relative to your application's publicDir
.
var templateId = "##cartero_dir";
It can be used in any type of asset processed by Cartero, including client side template files.
<script type="text/template" id="##cartero_dir">
...
</script>
##cartero_browserify_executeOnLoad
When the browserify
option in the Cartero Grunk Task is enabled, this directive is used in JavaScript files to specify that they should be automatically executed when they are loaded. You will definitely want to include this directive in your "main" JavaScript files for each page, since otherwise they would never be executed!
FAQ
Q: Does Cartero work with Rails, PHP, etc., or just with Node.js / Express?
The heart of Cartero is an intelligent Grunt.js task, and can be used with any web framework. However, there is a small piece of logic called the Hook which must be called from your web framework, since it is used when each page is rendered. If you are interested in developing a Cartero Hook for your web framework of choice, keep reading - it's not hard.
From a high level perspective, the Hook is responsible for populating the cartero_js
, cartero_css
, and cartero_tmpl
variables and making them available to the template being rendered. The implementation details are somewhat dependent on your web framework, but the general idea will always be similar.
- When the Hook is configured or initialized, it should be passed the absolute path of your
projectDir
. - The Hook needs to be called before your web framework's "render" function to set the value of the three template variables for which it is responsible. It should be passed the template file being rendered.
- The Hook uses the
cartero.json
file that was generated by the Cartero Grunt Task, located in the projectDir
, to lookup the assets needed for the template being rendered. The cartero.json
file has the following format. All paths in the file are relative to projectDir
.
{
publicDir : "static",
parcels : {
"views/peopleList/peopleList.jade" : {
js : [
"static/library-assets/jquery/jquery.js",
"static/library-assets/jquery-ui/jquery-ui.js",
"static/view-assets/peopleList/peopleList.js"
],
css : [
"static/library-assets/jquery-ui/jquery-ui.css",
"static/view-assets/peopleList/peopleList.css"
],
tmpl : [
"static/library-assets/dialogs/editPersonDialog/editPersonDialog.tmpl"
]
},
}
}
- The Hook then generates the raw HTML that will include the assets in the page being rendered and puts it into the
cartero_js
, cartero_css
, and cartero_tmpl
template variables. For the case of js
and css
files, it just needs to transform the paths in the cartero.json
file to be relative to the publicDir
, and then wrap them in <script>
or <link>
tags. For tmpl
assets, the Hook needs to read the files, concatenate their contents, and then put the whole shebang into cartero_tmpl
.
Q: Does Cartero address the issue of cache busting?
Yes. The name of the concatenated asset files generated in prod
mode includes an MD5 digest of their contents. When the contents of one of the files changes, its name will be updated, which will cause browsers to request a new copy of the content. The Rails Asset Pipeline implements the same cache busting technique.
Q: The "watch" task terminates on JS/CSS errors. Can I force it to keep running?
Yes. Use the Grunt --force
flag.
grunt cartero:dev --force --watch
Q: I'm getting the error: EMFILE, too many open files
EMFILE means you've reached the OS limit of concurrently open files. There isn't much we can do about it, however you can increase the limit yourself.
Add ulimit -n [number of files]
to your .bashrc/.zshrc file to increase the soft limit.
If you reach the OS hard limit, you can follow this StackOverflow answer to increase it.
Q: Since Cartero combines files in prod
mode, won't image urls used in my stylesheets break?
Yes and No. They would break, but Cartero automatically scans your .css
files for url()
statements, and fixes their arguments so that they don't break.
Cartero Hook Directory
If you develop a Hook for your web framework, please let us know and we'll add it to the directory.
##Change Log
See the CHANGELOG.md file.
About
By Oleg Seletsky and David Beck.
Copyright (c) 2013 Rotunda Software, LLC.
Licensed under the MIT License.