Last active
December 16, 2015 13:38
-
-
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 …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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