mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-12 18:52:50 +00:00
538 lines
15 KiB
JavaScript
538 lines
15 KiB
JavaScript
var pathUtil = require('path')
|
|
, fs = require('fs')
|
|
, dirname = pathUtil.dirname
|
|
, basename = pathUtil.basename
|
|
, join = pathUtil.join
|
|
, exists = fs.exists || pathUtil.exists
|
|
, relative = pathUtil.relative
|
|
, resolve = pathUtil.resolve
|
|
, crypto = require('crypto')
|
|
, chokidar = require('chokidar')
|
|
, stylus = require('stylus')
|
|
, nib = require('nib')
|
|
, less = require('less')
|
|
, racer = require('racer')
|
|
, Promise = racer.util.Promise
|
|
, hasKeys = racer.util.hasKeys
|
|
, finishAfter = racer.util.async.finishAfter
|
|
, asyncForEach = racer.util.async.forEach
|
|
, htmlUtil = require('html-util')
|
|
, parseHtml = htmlUtil.parse
|
|
, minifyHtml = htmlUtil.minify
|
|
, styleCompilers = {
|
|
stylus: stylusCompiler
|
|
, less: lessCompiler
|
|
}
|
|
, derby = require('./derby')
|
|
, isWindows = process.platform === 'win32'
|
|
|
|
exports.css = css;
|
|
exports.templates = templates;
|
|
exports.js = js;
|
|
exports.library = library;
|
|
exports.parseName = parseName;
|
|
exports.hashFile = hashFile;
|
|
exports.genInfo = genInfo;
|
|
exports.writeGen = writeGen;
|
|
exports.writeJs = writeJs;
|
|
exports.watch = watch;
|
|
|
|
function css(root, clientName, compress, callback) {
|
|
// TODO: Set default configuration options in a single place
|
|
var styles = derby.get('styles') || ['less', 'stylus']
|
|
, compiled = []
|
|
, finish;
|
|
|
|
root += '/styles';
|
|
|
|
if (!Array.isArray(styles)) styles = [styles];
|
|
|
|
finish = finishAfter(styles.length, function(err) {
|
|
callback(err, compiled.join(''));
|
|
});
|
|
|
|
styles.forEach(function(style, i) {
|
|
var compiler = styleCompilers[style];
|
|
if (!compiler) finish(new Error('Unable to find compiler for: ' + style));
|
|
|
|
compiler(root, clientName, compress, function(err, value) {
|
|
compiled[i] = value || '';
|
|
finish(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function stylusCompiler(root, clientName, compress, callback) {
|
|
findPath(root, clientName, '.styl', function(path) {
|
|
if (!path) return callback('');
|
|
fs.readFile(path, 'utf8', function(err, styl) {
|
|
if (err) return callback(err);
|
|
stylus(styl)
|
|
.use(nib())
|
|
.set('filename', path)
|
|
.set('compress', compress)
|
|
.set('include css', true)
|
|
.render(callback);
|
|
});
|
|
});
|
|
}
|
|
|
|
function lessCompiler(root, clientName, compress, callback) {
|
|
var dir = clientName.charAt(0) === '/' ? dirname(clientName) : root;
|
|
findPath(root, clientName, '.less', function(path) {
|
|
if (!path) return callback('');
|
|
|
|
fs.readFile(path, 'utf8', function(err, lessFile) {
|
|
if (err) return callback(err);
|
|
var parser = new less.Parser({
|
|
paths: [dirname(path)]
|
|
, filename: path
|
|
});
|
|
parser.parse(lessFile, function(err, tree) {
|
|
var compiled;
|
|
if (err) return callback(err);
|
|
try {
|
|
compiled = tree.toCSS({compress: compress});
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
callback(null, compiled);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function templates(root, clientName, callback) {
|
|
loadTemplates(root + '/views', clientName, callback);
|
|
}
|
|
|
|
function js(parentFilename, options, callback) {
|
|
var finish, inline, inlineFile, js;
|
|
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
|
|
// Needed for tests
|
|
if (!parentFilename) return callback();
|
|
|
|
// TODO: Move this to Tracks:
|
|
// Express will try to include mime or connect, which won't work in the
|
|
// browser. It doesn't actually need this for routing, so just ignore it
|
|
options.ignore || (options.ignore = ['connect', 'mime']);
|
|
options.entry || (options.entry = parentFilename);
|
|
|
|
inlineFile = join(dirname(parentFilename), 'inline.js');
|
|
finish = finishAfter(2, function(err) {
|
|
callback(err, js, inline);
|
|
});
|
|
racer.js(options, function(err, value) {
|
|
js = value;
|
|
finish(err);
|
|
});
|
|
fs.readFile(inlineFile, 'utf8', function(err, value) {
|
|
inline = value;
|
|
// Ignore file not found error
|
|
if (err && err.code === 'ENOENT') err = null;
|
|
finish(err);
|
|
});
|
|
}
|
|
|
|
function library(root, callback) {
|
|
var components = {};
|
|
|
|
fs.readdir(root, function(err, files) {
|
|
if (err) return callback(err);
|
|
asyncForEach(files, libraryFile, function(err) {
|
|
if (err) return callback(err);
|
|
callback(null, components);
|
|
});
|
|
});
|
|
|
|
function libraryFile(file, callback) {
|
|
var path = root + '/' + file
|
|
fs.stat(path, function(err, stats) {
|
|
if (err) return callback(err);
|
|
|
|
if (stats.isDirectory()) {
|
|
return addComponent(root, file, callback);
|
|
}
|
|
if (extensions['html'].test(file)) {
|
|
file = file.replace(extensions['html'], '');
|
|
return addComponent(root, file, callback);
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function addComponent(root, name, callback) {
|
|
loadTemplates(root, name, function(err, templates, instances) {
|
|
components[name] = {
|
|
templates: templates
|
|
, instances: instances
|
|
};
|
|
callback(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function unixRelative(from, to) {
|
|
var path = relative(from, to);
|
|
return isWindows ? path.replace(/\\/g, '/') : path;
|
|
}
|
|
|
|
function parseName(parentFilename) {
|
|
var parentDir = dirname(parentFilename)
|
|
, root = parentDir
|
|
, base = basename(parentFilename).replace(/\.(?:js|coffee)$/, '');
|
|
if (base === 'index') {
|
|
base = basename(parentDir);
|
|
root = dirname(dirname(parentDir));
|
|
} else if (basename(parentDir) === 'lib') {
|
|
root = dirname(parentDir);
|
|
}
|
|
return {
|
|
root: root
|
|
, clientName: base
|
|
};
|
|
}
|
|
|
|
function hashFile(file) {
|
|
var hash = crypto.createHash('md5').update(file).digest('base64');
|
|
// Base64 uses characters reserved in URLs and adds extra padding charcters.
|
|
// Replace "/" and "+" with the unreserved "-" and "_" and remove "=" padding
|
|
return hash.replace(/[\/\+=]/g, function(match) {
|
|
switch (match) {
|
|
case '/': return '-';
|
|
case '+': return '_';
|
|
case '=': return '';
|
|
}
|
|
});
|
|
}
|
|
|
|
function genInfo(root, filename, isPackage) {
|
|
var staticRoot = derby.get('staticRoot') || join(root, 'public')
|
|
, staticMount = derby.get('staticMount') || ''
|
|
, staticDir = isPackage
|
|
? derby.get('staticPackageDir') || '/genpack'
|
|
: derby.get('staticDir') || '/gen'
|
|
, staticPath = join(staticRoot, staticDir)
|
|
, filePath = join(staticPath, filename)
|
|
, relativePath = join(staticMount, staticDir, filename)
|
|
return {
|
|
staticPath: staticPath
|
|
, staticRoot: staticRoot
|
|
, filePath: filePath
|
|
, relativePath: relativePath
|
|
}
|
|
}
|
|
|
|
function writeGen(root, filename, file, isPackage, callback) {
|
|
var info = genInfo(root, filename, isPackage)
|
|
function finish(err) {
|
|
if (err) return callback(err);
|
|
fs.writeFile(info.filePath, file, function(err) {
|
|
callback(err, info.relativePath);
|
|
});
|
|
}
|
|
exists(info.staticPath, function(value) {
|
|
if (value) return finish();
|
|
|
|
fs.mkdir(info.staticRoot, '0777', function(err) {
|
|
// Not a problem if the directory already exists
|
|
if (err && err.code !== 'EEXIST') return finish(err);
|
|
fs.mkdir(info.staticPath, '0777', function(err) {
|
|
// Not a problem if the directory already exists
|
|
if (err && err.code !== 'EEXIST') return finish(err);
|
|
finish();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function writeJs(root, file, isPackage, callback) {
|
|
var hash = hashFile(file)
|
|
, filename = hash + '.js'
|
|
writeGen(root, filename, file, isPackage, function(err, relativePath) {
|
|
callback(err, relativePath, hash);
|
|
});
|
|
}
|
|
|
|
function watch(dir, type, onChange) {
|
|
var extension = extensions[type]
|
|
, hashes = {}
|
|
, watcher = chokidar.watch([])
|
|
|
|
watcher
|
|
.on('add', checkModified)
|
|
.on('change', checkModified)
|
|
.on('unlink', checkModified)
|
|
.on('error', function(err) {
|
|
console.error('Watch error\n', err);
|
|
})
|
|
|
|
files(dir, extension).forEach(function(path) {
|
|
fs.readFile(path, 'utf8', function(err, file) {
|
|
if (err) return console.error('Watch error\n', err);
|
|
hashes[path] = hashFile(file);
|
|
watcher.add(path);
|
|
});
|
|
});
|
|
|
|
function checkModified(path) {
|
|
fs.readFile(path, 'utf8', function(err, file) {
|
|
if (err) return console.error('Watch error\n', err);
|
|
var hash = hashFile(file);
|
|
if (hash === hashes[path]) return;
|
|
hashes[path] = hash;
|
|
onChange(path);
|
|
})
|
|
}
|
|
}
|
|
|
|
function absolutePath(path) {
|
|
return path === resolve('/', path);
|
|
}
|
|
|
|
function findPath(root, name, extension, callback) {
|
|
if (!absolutePath(name)) {
|
|
name = join(root, name);
|
|
}
|
|
var path = name + extension;
|
|
exists(path, function(value) {
|
|
if (value) return callback(path);
|
|
path = join(name, 'index' + extension);
|
|
exists(path, function(value) {
|
|
callback(value ? path : null);
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadTemplates(root, fileName, callback) {
|
|
var count = 0
|
|
, calls = {incr: incr, finish: finish};
|
|
function incr() {
|
|
count++;
|
|
}
|
|
function finish(err, templates, instances) {
|
|
if (err) {
|
|
calls.finish = function() {};
|
|
return callback(err);
|
|
}
|
|
if (--count) return;
|
|
if (hasKeys(instances)) {
|
|
callback(null, templates, instances);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
forTemplate(root, fileName, 'import', calls);
|
|
}
|
|
|
|
function forTemplate(root, fileName, get, calls, files, templates, instances, alias, currentNs) {
|
|
if (currentNs == null) currentNs = '';
|
|
calls.incr();
|
|
findPath(root, fileName, '.html', function(path) {
|
|
var getCount, got, matchesGet, promise;
|
|
if (path === null) {
|
|
if (!files) {
|
|
// Return without doing anything if the path isn't found, and this is the
|
|
// initial automatic lookup based on the clientName
|
|
return calls.finish(null, {}, {});
|
|
} else {
|
|
return calls.finish(new Error(
|
|
"Can't find file " + fileName
|
|
));
|
|
}
|
|
}
|
|
files || (files = {});
|
|
templates || (templates = {});
|
|
instances || (instances = {});
|
|
|
|
got = false;
|
|
if (get === 'import') {
|
|
matchesGet = function() {
|
|
return got = true;
|
|
}
|
|
} else if (Array.isArray(get)) {
|
|
getCount = get.length;
|
|
matchesGet = function(name) {
|
|
--getCount || (got = true);
|
|
return ~get.indexOf(name);
|
|
}
|
|
} else {
|
|
matchesGet = function(name) {
|
|
got = true;
|
|
return get === name;
|
|
}
|
|
}
|
|
|
|
promise = files[path];
|
|
if (!promise) {
|
|
promise = files[path] = new Promise;
|
|
fs.readFile(path, 'utf8', function(err, file) {
|
|
promise.resolve(err, file);
|
|
});
|
|
}
|
|
promise.on(function(err, file) {
|
|
if (err) return calls.finish(err);
|
|
try {
|
|
parseTemplateFile(root, dirname(path), path, calls, files, templates, instances, alias, currentNs, matchesGet, file);
|
|
} catch (err) {
|
|
if (err.message) {
|
|
err.message = 'In file ' + path + '\n\n' + err.message;
|
|
}
|
|
return calls.finish(err);
|
|
}
|
|
if (!got && get !== 'import') {
|
|
return calls.finish(new Error(
|
|
"Can't find template '" + get + "' in " + path
|
|
));
|
|
}
|
|
calls.finish(null, templates, instances);
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseTemplateFile(root, dir, path, calls, files, templates, instances, alias, currentNs, matchesGet, file) {
|
|
var relativePath = unixRelative(root, path);
|
|
|
|
parseHtml(file + '\n', {
|
|
// Force template tags to be treated as raw tags,
|
|
// meaning their contents are not parsed as HTML
|
|
rawTags: /^(?:[^\s=\/!>]+:|style|script)$/i
|
|
, matchEnd: matchEnd
|
|
, start: onStart
|
|
, text: onText
|
|
});
|
|
|
|
function matchEnd(tagName) {
|
|
if (tagName.slice(-1) === ':') {
|
|
return /<\/?[^\s=\/!>]+:[\s>]/i;
|
|
}
|
|
return new RegExp('</' + tagName, 'i');
|
|
}
|
|
|
|
// These variables pass state from attributes in the start tag to the
|
|
// following template text
|
|
var name, attrs;
|
|
|
|
function onStart(tag, tagName, _attrs) {
|
|
var i = tagName.length - 1
|
|
name = (tagName.charAt(i) === ':' ? tagName.slice(0, i) : '').toLowerCase();
|
|
attrs = _attrs;
|
|
for (var key in attrs) {
|
|
if (attrs[key] === null) attrs[key] = true;
|
|
}
|
|
|
|
if (name === 'import') {
|
|
parseImport(root, dir, path, calls, files, templates, instances, currentNs, attrs)
|
|
}
|
|
}
|
|
|
|
function onText(text, isRawText) {
|
|
if (!matchesGet(name)) return;
|
|
if (name === 'import') {
|
|
if (onlyWhitespace(text)) return;
|
|
return calls.finish(new Error(
|
|
"Content not allowed after <import:> in" + path + " : " + text
|
|
));
|
|
}
|
|
var templateName = relativePath + ':' + name
|
|
, instanceName = alias || name
|
|
if (currentNs) {
|
|
instanceName = currentNs + ':' + instanceName;
|
|
}
|
|
instances[instanceName] = [templateName, attrs];
|
|
if (templates[templateName]) return;
|
|
if (!(name && isRawText)) {
|
|
if (onlyWhitespace(text)) return;
|
|
return calls.finish(new Error(
|
|
"Can't read template in " + path + " near the text: " + text
|
|
));
|
|
}
|
|
templates[templateName] = attrs.literal ? text : minifyHtml(text);
|
|
}
|
|
}
|
|
|
|
function parseImport(root, dir, path, calls, files, templates, instances, currentNs, attrs) {
|
|
var src = attrs.src
|
|
, ns = attrs.ns
|
|
, as = attrs.as
|
|
, template = attrs.template
|
|
, toGet = 'import'
|
|
, srcNs
|
|
if (!src) {
|
|
return calls.finish(new Error(
|
|
"Template import in " + path + " must have a 'src' attribute"
|
|
));
|
|
}
|
|
if (template) {
|
|
toGet = template.toLowerCase().split(' ');
|
|
if (toGet.length > 1 && (as != null)) {
|
|
return calls.finish(new Error(
|
|
"Template import of '" + src + "' in " + path +
|
|
" can't specify multiple 'template' values with 'as'"
|
|
));
|
|
}
|
|
}
|
|
if ('ns' in attrs) {
|
|
if (as) {
|
|
return calls.finish(new Error(
|
|
"Template import of '" + src + "' in " + path +
|
|
" can't specifiy both 'ns' and 'as' attributes"
|
|
));
|
|
}
|
|
// Import into the namespace specified via 'ns' underneath
|
|
// the current namespace
|
|
ns = ns
|
|
? currentNs ? currentNs + ':' + ns : ns
|
|
: currentNs;
|
|
} else if (as) {
|
|
// If 'as' is specified, import into the current namespace
|
|
ns = currentNs;
|
|
} else {
|
|
// If no namespace is specified, use the src file name
|
|
// as the default namespace
|
|
i = src.lastIndexOf('/');
|
|
srcNs = i ? src.slice(i + 1) : src;
|
|
ns = currentNs ? currentNs + ':' + srcNs : srcNs;
|
|
}
|
|
ns = ns.toLowerCase();
|
|
|
|
forTemplate(root, join(dir, src), toGet, calls, files, templates, instances, as, ns);
|
|
}
|
|
|
|
function onlyWhitespace(text) {
|
|
// Minify removes HTML comments & linebreaks
|
|
return /^\s*$/.test(minifyHtml(text))
|
|
}
|
|
|
|
// TODO: These should be set as configuration options
|
|
var extensions = {
|
|
html: /\.html$/i
|
|
, css: /\.styl$|\.css|\.less$/i
|
|
, js: /\.js$/i
|
|
};
|
|
|
|
var ignoreDirectories = ['node_modules', '.git', 'gen'];
|
|
|
|
function ignored(path) {
|
|
return ignoreDirectories.indexOf(path) === -1;
|
|
}
|
|
|
|
function files(dir, extension, out) {
|
|
if (out == null) out = [];
|
|
fs.readdirSync(dir).filter(ignored).forEach(function(p) {
|
|
p = join(dir, p);
|
|
if (fs.statSync(p).isDirectory()) {
|
|
files(p, extension, out);
|
|
} else if (extension.test(p)) {
|
|
out.push(p);
|
|
}
|
|
});
|
|
return out;
|
|
}
|