Skip to content

Instantly share code, notes, and snippets.

@Tronix117
Last active December 16, 2015 13:38
Show Gist options
  • Save Tronix117/5442567 to your computer and use it in GitHub Desktop.
Save Tronix117/5442567 to your computer and use it in GitHub Desktop.
Cakefile script to easily build Jade, Stylus and CoffeeScript source files for little projects or static website. Also provide consistent website structure. Also watch Jade and Stylus dependencies (if you modify an import or an extends, every file which use it will be rebuilt). Include a basic support of I18n - Internationalization with support …
#I18n - format of a translation file, located in `app/locales/en.coffee`
# here you can put whatever coffee-script code you want, as well as defining some custom methods
# Don't remove the BEGIN and END comment, the Cakefile will automaticaly insert missing translations between
#=BEGIN
T = {}
#=END
# the code you want (or nothing :D)
module.exports = T
###
# Cakefile
#
# Build easily Stylus, CoffeeScript and Jade source files
# for little projects.
#
# First install requirements using `npm install`
#
# Then, modify the config in the `config.coffee` file
#
# Then:
#
# * to build: `cake build`
# * to watch files and build automaticaly once modified: `cake cake`
# * to clean generated files: `cake clean`
#
# @author Jeremy Trufier <[email protected]>
###
{spawn, exec} = require 'child_process'
fs = require 'fs'
path = require 'path'
jade = require 'jade'
monocle = require('jade/node_modules/monocle')
readdirp = require('jade/node_modules/monocle/node_modules/readdirp')
stylus = require 'stylus'
jade = require 'jade'
coffee = require 'coffee-script'
config = require './config'
config.paths ?= {}
task 'build', 'Build JS & CSS', ->
Clock.active = false
copy file, output for file, output of config.paths.assets
compileDirectories config.paths.source
task 'watch', 'Watch source files and build JS & CSS', ->
monocleAssetDir = monocle()
for dir, output of config.paths.assets
copy dir, output, true
monocleAssetDir.watchDirectory
root: p = path.resolve dir
complete: ((p, output)-> ()-> log 'watching', TERM.YELLOW, p, 'all')(p)
listener: ((output) -> (file) -> copy file.fullPath, output)(output)
monocleDir = monocle()
compileDirectories config.paths.source, true
for dir, output of config.paths.source
monocleDir.watchDirectory
root: p = path.resolve dir
fileFilter: ['*.jade', '*.coffee', '*.styl']
directoryFilter: path.resolve('*') # only files in root level of the directory = not recursive
complete: ((p)-> ()-> log 'watching', TERM.YELLOW, p, 'all')(p)
listener: ((output) -> (file) -> compile file.fullPath, output, true)(output)
task 'clean', 'Clean built JS & CSS files', ->
Clock.active = false
# ANSI Terminal Colors
TERM =
RESET: '\x1b[0m'
RED: '\x1b[0;31m'
GREEN: '\x1b[0;32m'
YELLOW: '\x1b[0;33m'
BLUE: '\x1b[0;34m'
MAGENTA: '\x1b[0;35m'
CYAN: '\x1b[0;36m'
CYAN_REVERSE: '\x1b[0;36;7m'
CLEAR_LINE: '\x1b[2K'
CLEAR_SCREEN: '\x1b[2J'
CURSOR_TO_ORIGIN: '\x1b[0;0f'
outputExtName =
'.coffee': '.js'
'.styl': '.css'
'.jade': '.html'
out = (s)->
process.stdout.write TERM.RESET + s
log = (message, modifier, explanation, interpretor) ->
Clock.hide()
out TERM.CYAN + Clock.getFormatedTime()
out ' - ' + TERM.MAGENTA + ((interpretor or 'none') + ' ').slice(0,6)
out ' - ' + (modifier or '') + (if message.length > 10 then message else (message + ' ').slice(0,10))
out ' ' + ((explanation or '').replace(path.resolve() + '/', '') or '')
Clock.show()
Clock =
active: true
_running: false
getFormatedTime: ->
('0' + (d = new Date()).getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2)
show: ->
process.stdout.write '\n'
Clock._running = true
Clock.render()
render: -> process.stdout.write '\r' + TERM.CLEAR_LINE + TERM.CYAN_REVERSE + Clock.getFormatedTime()
run: ->
Clock.render() if Clock._running
setTimeout (-> Clock.run()), 1000 if Clock.active
hide: ->
Clock._running = false
process.stdout.write '\r' + TERM.CLEAR_LINE
Clock.run()
resolveOutputFile = (file, output, sub)->
file = file.replace(i, o) for i, o of outputExtName
path.resolve config.paths.build, sub, output, path.basename file
i18nLocales = {}
compileDirectories = (dirs, watching)->
for locale in (config.i18n?.locales or [])
i18nLocales[locale] = new I18n(locale, path.resolve(config.i18n?.localesPath))
end = -> i18n.regenerateLocaleFile() for l, i18n of i18nLocales
count = 0
countEnd = ()-> end() if (count -= 1) <=0
for dir, output of config.paths.source
count += 1
compileDirectory dir, output, watching, countEnd
compileDirectory = (dir, output, watching, end)->
readdirp
root: path.resolve dir
fileFilter: ['*.jade', '*.coffee', '*.styl']
directoryFilter: path.resolve('*') # only files in root level of the directory = not recursive
.on('data', ((output, watching) -> (file) -> compile file.fullPath, output, watching)(output, watching))
.on('end', end)
compile = (file, output, watching)->
cb = (interpretor)-> (err, info)->
#.replace(/\r?\n|\r/g, TERM.MAGENTA + '\n\r ')
return log 'error', TERM.RED, err, interpretor if err
component = info.replace(/\x1b\[\d+m/gi,'').match /(\w+)\s(\S+)\s*$/
unless info is '\n' or component?[2]? and component[2].match /(\/nib\/lib\/)|(\/stylus\/lib\/)/gi
if component?[1]? and -1 < ['watching', 'compiled', 'rendered', 'removed'].indexOf(component[1])
infoModifier =
'watching': TERM.YELLOW
'rendered': TERM.BLUE
'removed': TERM.RED
'compiled': TERM.GREEN
'rebuilt': TERM.MAGENTA
details = (if component[2].indexOf('/') isnt 0 then path.resolve(component[2]) else component[2])
log component[1], infoModifier[component[1]], details, interpretor
else log 'info', TERM.BLUE, info.replace /\s+$/gi, '', interpretor
switch ext = path.extname file
when '.styl' then compileStylus file, output, watching, cb('stylus')
when '.coffee' then compileCoffee file, output, watching, cb('coffee')
when '.jade' then compileJade file, output, watching, cb('jade')
else return log 'don\'t know what to do with', TERM.RED, file
##
# Stylus magic
#
watchDependenciesFile = {} # file1: ['fileimport1','fileimport2']
watchDependenciesFileWatchers = {} # 'fileimport1': {'file1':{output: '', cb: (->)}}
watchDependenciesCallback = (dependency) -> ()->
cb() for file, cb of watchDependenciesFileWatchers[dependency]
return
watchDependenciesMonocle = monocle()
watchDependencies = (file, dependencies, cb)->
if file of watchDependenciesFile
for oldImport in watchDependenciesFile[file]
if -1 < i = dependencies.indexOf oldImport
dependencies.splice i, 1
else
delete watchDependenciesFileWatchers[file][oldImport]
watchDependenciesFile[file] = dependencies
for dependency in dependencies
unless dependency of watchDependenciesFileWatchers # then the watcher don't exist yet for this file
watchDependenciesMonocle.watchFiles
files: [dependency]
listener: watchDependenciesCallback(dependency)
complete: ()-> log 'watching', TERM.YELLOW, dependency, 'all'
watchDependenciesFileWatchers[dependency] = {}
watchDependenciesFileWatchers[dependency][file] = cb
useStylusPlugins = (style)->
for plugin, options of (config.stylus?.use or {})
unless plugin is 'url'
fn = require(if /^\.+\//.test(plugin) then path.resolve(plugin) else plugin)
throw new Error('plugin ' + plugin + ' does not export a function') if typeof fn isnt 'function'
style.use fn options
else
style.define('url', stylus.url options)
compileStylus = (file, output, watching, cb) ->
outputFile = resolveOutputFile file, output
fs.readFile file, 'utf8', (err, str)->
return cb err.message if err
options = config.stylus or {}
options.filename = file
options._imports = []
code = stylus(str, options)
useStylusPlugins(code)
code.render (err, outputCode)->
return cb err.message if err
if watching
dependencies = []
(dependencies.push imported.path if imported.path) for imported in options._imports
watchDependencies file, dependencies, =>
compileStylus.call @, file, output, watching, cb
writeFile outputFile, outputCode, (err)->
return cb err.message if err
cb null, 'compiled ' + outputFile
##
# Coffee magic
#
compileCoffee = (file, output, watching, cb) ->
outputFile = resolveOutputFile file, output
args = arguments
fs.readFile file, 'utf8', (err, str)->
return cb err.message if err
options = config.coffee or {}
try
outputCode = coffee.compile str, options
writeFile outputFile, outputCode, (err)->
return cb err.message if err
cb null, 'compiled ' + outputFile
catch err
cb err.message
##
# Jade magic
#
compileJade = (file, output, watching, cb) ->
args = arguments
fs.readFile file, 'utf8', (err, str)->
return cb err.message if err
options = config.jade or {}
options.filename = file
try
if config.i18n and config.i18n.locales?.length
for locale in config.i18n.locales
options.locale = locale
outputFile = resolveOutputFile file, output, locale
options.compiler = JadeI18nCompiler
outputJade = jade.compile str, options
writeFile outputFile, outputJade(options.locals or {}), ((outputFile)-> (err)-> # `options.locals` may be a duplicate with `options` above
return cb err.message if err
cb null, 'rendered ' + outputFile
)(outputFile)
else
outputFile = resolveOutputFile file, output
outputJade = jade.compile str, options
writeFile outputFile, outputJade(options.locals or {}), ((outputFile)-> (err)-> # `options.locals` may be a duplicate with `options` above
return cb err.message if err
cb null, 'rendered ' + outputFile
)(outputFile)
if watching
watchDependencies file, (dependency for dependency of options.dependencies), =>
compileJade.call @, file, output, watching, cb
catch err
cb err.message
copy = (input, output) ->
path.resolve (config.paths.build or ''), output
input = path.resolve(input)
output = path.resolve((config.paths.build or ''), output) + '/'
if fs.statSync(input).isDirectory() then input += '/*'
exec ['cp', '-R', input, output].join(' '), (err, stdout, stderr) ->
if err or stderr
log 'error ', TERM.RED, 'Unable to copy' + path.resolve input
return log err and stderr, TERM.RED
log 'copied ', TERM.GREEN, input + TERM.RED + ' to ' + TERM.RESET + output
clean = (file, output) ->
process.stdout.write TERM.CLEAR_SCREEN + TERM.CURSOR_TO_ORIGIN
mkdirp = (p, cb) ->
p = path.resolve p
fs.stat p, (err, stat)->
return cb null, p if not err and stat.isDirectory()
return cb err unless err.code is 'ENOENT'
fs.mkdir p, (err)->
return cb null, p unless err
return cb err unless err.code is 'ENOENT'
mkdirp path.dirname(p), (err) ->
return cb err if err
mkdirp p, cb
writeFile = (output, p, cb)->
mkdirp path.dirname(output), (err, dir)->
return log 'all', TERM.RED, err if err
fs.writeFile output, p, cb
jade.Parser.prototype._resolvePath = jade.Parser.prototype.resolvePath
jade.Parser.prototype.resolvePath = (path)->
this.options.dependencies ?= {}
this.options.dependencies[path = jade.Parser.prototype._resolvePath.apply this, arguments] = true
path
# Need to be done in a cleaner way
class JadeI18nCompiler
constructor: (node, options)->
@i18n = i18nLocales[options.locale]
@importsIncludes = options.importsIncludes = {}
jade.Compiler.call this, node, options
__proto__: jade.Compiler.prototype
visitText: (node)->
node.val = @i18n.tr(node.val)
jade.Compiler.prototype.visitText.call this, node
visit: (node)->
@importsIncludes[node.filename] = true if node.filename
jade.Compiler.prototype.visit.call this, node
###
# I18n class
# Allow you to add Internationalization to your CoffeeScript application
#
# Just call it with a `require 'util/I18n'` or `new I18n` if you don't have a `require` system
# More informations here: https://github.com/Tronix117/tradify
# Translation files should be saved in `locales/{langage code}.coffee`
#
# Then you can translate everything with `tr('{0} day', numberOfDay)`
#
# @author Jeremy Trufier <[email protected]>
###
class I18n
constructor: (code, localesPath)->
@locale = 'en'
@translations = null
@localeFile = null
code = @getLanguageCode(code)
pf = [
code.language + '-' + code.region
code.language + '-' + code.region.toUpperCase()
code.language + '_' + code.region
code.language + '_' + code.region.toUpperCase()
code.language
code.language.toUpperCase()
code.region
code.region.toUpperCase()
@locale
]
while pf.length and not @translations
try @translations = require(@localeFile = localesPath + '/' + (@locale = pf.shift())) catch e
@translations = {} unless @translations
@
getLanguageCode: (code)->
lang = (code || 'en').toLowerCase().match(/(\w\w)[-_]?(\w\w)?/)
{ language: lang[1], region: lang[2] || lang[1] }
# i18n
# How is it working ?
#
# tr(string, arg1, arg2, ...)
#
# Inside the first argument, to write some arguments the format is:
# {0} -> write arguments 1
# {1} -> write arguments 2
# ...
# {0s} -> translator help: type, can be: s(tring), i(nteger), f(loat), d(ate)
# {0#Date field} -> Comment for the translator
# {0i#Number} -> guess !
tr: (s)=>
return s if typeof s isnt 'string'
t = @translations[s] = [].concat(if @translations[s] is null then [] else @translations[s])
a = arguments
(s = if typeof ti is "function" then ti.apply(this, a) else ti or s) for ti in t
s.replace /{(\d+)\w?(#.*)?}/g, (n, c) ->
a[c] if a[c = parseInt(c) + 1]
escapeCoffee = (str)->
str = JSON.stringify(str or 'null').replace(/#/gi, '\\\#')
regenerateLocaleFile: ()->
@_localefile = path.resolve(@localeFile + '.coffee')
fs.readFile @_localefile, (err, data)=>
return log 'error', TERM.RED, err.message, 'i18n' if err and err.code isnt 'ENOENT'
buffer = "#=BEGIN\n\rT=\n\r"
for key, translation of @translations
if not translation or (typeof translation is 'object' and translation.length) is 0 then translation = 'null'
else if typeof translation is 'object' and translation.length is 1 then translation = escapeCoffee(translation[0])
else translation = escapeCoffee(translation)
buffer += " " + escapeCoffee(key) + ": " + translation + "\n\r"
buffer += '#=END'
buffer = data.toString().replace(/#=BEGIN[\s\S]*#=END/gim, buffer) if data
writeFile @_localefile, buffer, (err)=>
return log 'error', TERM.RED, err.message, 'i18n' if err and err.code isnt 'ENOENT'
log 'rebuilt', TERM.BLUE, @_localefile, 'i18n'
module.exports = new I18n
# Recommended working tree
# project
# | -- app
# | | -- assets
# | | | -- images
# | | | -- sounds
# | | | -- fonts
# | | + -- .htaccess
# | | -- locales
# | | | -- en.coffee
# | | | -- fr.coffee
# | | + -- fr-ca.coffee
# | | -- scripts
# | | + -- main.coffee
# | | -- styles
# | | + -- main.styl
# | | -- templates
# | | -- layouts
# | | + -- default.jade
# | + index.jade
# | -- build
# | | -- css
# | | + -- main.css
# | | -- js
# | | + -- main.js
# | + -- index.html
# + -- Cakefile # where the magic happens
# + -- README.md # because every good coder makes a README
# Source Files and ouput directory
# if directories are passed then all top file inside will be built
# ex: `'styl': 'css'` then `styl/smthg.styl` will be built as `css/smthg.css`
# but not `styl/dir/smthgelse.styl` (also watch will not work with new files)
module.exports =
paths:
build: 'build' # every output destination will be prefixed by this folder
source: # Please give only folders for the moment, results are not guarenteed with something else ^^
'app/templates': ''
'app/styles': 'css' # so, styles will be built in the `build/css` folder
'app/scripts': 'js'
assets: # Everything will be copied to destination, without build
'app/assets': ''
stylus: # will be passed as stylus options
use:
'nib': {}
'url':
paths: ['app/assets/images'] # needed to compute image size
compress: false # minify the css, not recommended for development
coffee: # will be passed as coffee-script options
bare: false # check coffee-script doc to know more about
jade:
locals: {} # variables that can be accessed directly from jade files
pretty: true # render a pretty html, recommended for development
i18n: # if you want to enable translation of jade file ; otherwise set this to `false`
locales: ['en', 'fr', 'es'] # langages you support, be sure to create a file per langage
localesPath: 'app/locales' # directory where you will store the locale files, they should have the format in the exemple file
{
"author": "Your name <your-mail@provider>",
"contributors": [
"Jeremy Trufier <[email protected]> (https://gist.github.com/Tronix117/5442567)"
],
"name": "your-project",
"description": "",
"version": "0.1.0beta",
"private": true,
"homepage": "http://www.your-project.com",
"bugs": "https://github.com/your-name/your-project/issues",
"repository": {
"type": "git",
"url": "[email protected]:your-name/your-project.git"
},
"engines": {
"node": ">= 0.9"
},
"scripts": {
"start": "cake watch",
"postinstall": "cake build",
"postupdate": "cake build"
},
"dependencies": {
"coffee-script":"~1.6.2",
"jade": "~0.30.0",
"stylus": "~0.32.1"
},
"optionalDependencies": {
"nib": "~0.9.1",
"marked": "~0.2.8"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment