Refactor precompiler API into two phase

Load templates and then parse them in a distinct operation. This will allow us to use other input sources such as stdin and strings.
This commit is contained in:
kpdecker
2015-08-04 09:26:19 -05:00
parent ba31ef8ae4
commit a62cbad95a
3 changed files with 155 additions and 123 deletions
+8 -6
View File
@@ -26,7 +26,7 @@ var optimist = require('optimist')
'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath',
'default': ''
'default': ''
},
'k': {
'type': 'string',
@@ -59,12 +59,12 @@ var optimist = require('optimist')
'description': 'Template root. Base value that will be stripped from template names.',
'alias': 'root'
},
'p' : {
'p': {
'type': 'boolean',
'description': 'Compiling a partial template',
'alias': 'partial'
},
'd' : {
'd': {
'type': 'boolean',
'description': 'Include data when compiling',
'alias': 'data'
@@ -103,9 +103,11 @@ var argv = optimist.argv;
argv.templates = argv._;
delete argv._;
var Precompiler = require('../dist/cjs/precompiler');
Precompiler.loadTemplates(argv);
if (argv.help || (!argv.templates.length && !argv.version)) {
optimist.showHelp();
return;
} else {
Precompiler.cli(argv);
}
return require('../dist/cjs/precompiler').cli(argv);
+89 -77
View File
@@ -5,28 +5,73 @@ import {basename} from 'path';
import {SourceMapConsumer, SourceNode} from 'source-map';
import uglify from 'uglify-js';
module.exports.loadTemplates = function(opts) {
// Build file extension pattern
let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
let ret = [];
function processTemplate(template, root) {
let path = template,
stat;
try {
stat = fs.statSync(template);
} catch (err) {
throw new Handlebars.Exception(`Unable to open template file "${template}"`);
}
if (stat.isDirectory()) {
opts.hasDirectory = true;
fs.readdirSync(template).map(function(file) {
let childPath = template + '/' + file;
if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
processTemplate(childPath, root || template);
}
});
} else {
let data = fs.readFileSync(path, 'utf8');
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length + 1);
}
template = template.replace(extension, '');
ret.push({
path: path,
name: template,
source: data
});
}
}
opts.templates.forEach(function(template) {
processTemplate(template, opts.root);
});
opts.templates = ret;
};
module.exports.cli = function(opts) {
if (opts.version) {
console.log(Handlebars.VERSION);
return;
}
if (!opts.templates.length) {
if (!opts.templates.length && !opts.hasDirectory) {
throw new Handlebars.Exception('Must define at least one template or directory.');
}
opts.templates.forEach(function(template) {
try {
fs.statSync(template);
} catch (err) {
throw new Handlebars.Exception(`Unable to open template file "${template}"`);
}
});
if (opts.simple && opts.min) {
throw new Handlebars.Exception('Unable to minimize simple output');
}
if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) {
if (opts.simple && (opts.templates.length !== 1 || opts.hasDirectory)) {
throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
}
@@ -41,10 +86,6 @@ module.exports.cli = function(opts) {
}
}
// Build file extension pattern
let extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
@@ -63,76 +104,47 @@ module.exports.cli = function(opts) {
}
output.add('{};\n');
}
function processTemplate(template, root) {
let path = template,
stat = fs.statSync(path);
if (stat.isDirectory()) {
fs.readdirSync(template).map(function(file) {
let childPath = template + '/' + file;
if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
processTemplate(childPath, root || template);
}
});
} else {
let data = fs.readFileSync(path, 'utf8');
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = path;
}
if (opts.data) {
options.data = true;
}
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length + 1);
}
template = template.replace(extension, '');
let precompiled = Handlebars.precompile(data, options);
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
}
if (opts.simple) {
output.add([precompiled, '\n']);
} else if (opts.partial) {
if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']);
} else {
if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']);
}
}
}
opts.templates.forEach(function(template) {
processTemplate(template, opts.root);
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = template.path;
}
if (opts.data) {
options.data = true;
}
let precompiled = Handlebars.precompile(template.source, options);
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
}
if (opts.simple) {
output.add([precompiled, '\n']);
} else if (opts.partial) {
if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) {
output.add('return ');
}
output.add(['Handlebars.partials[\'', template.name, '\'] = template(', precompiled, ');\n']);
} else {
if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) {
output.add('return ');
}
output.add(['templates[\'', template.name, '\'] = template(', precompiled, ');\n']);
}
});
// Output the content
if (!opts.simple) {
if (opts.amd) {
if (opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) {
if (opts.templates.length > 1 || (opts.templates.length == 1 && opts.hasDirectory)) {
if (opts.partial) {
output.add('return Handlebars.partials;\n');
} else {
+58 -40
View File
@@ -16,6 +16,12 @@ describe('precompiler', function() {
precompile,
minify,
emptyTemplate = {
path: __dirname + '/artifacts/empty.handlebars',
name: 'empty',
source: ''
},
file,
content,
writeFileSync;
@@ -51,10 +57,9 @@ describe('precompiler', function() {
Precompiler.cli({templates: []});
}, Handlebars.Exception, 'Must define at least one template or directory.');
});
it('should throw on missing template', function() {
shouldThrow(function() {
Precompiler.cli({templates: ['foo']});
}, Handlebars.Exception, 'Unable to open template file "foo"');
it('should handle empty/filtered directories', function() {
Precompiler.cli({hasDirectory: true, templates: []});
// Success is not throwing
});
it('should throw when combining simple and minimized', function() {
shouldThrow(function() {
@@ -68,105 +73,118 @@ describe('precompiler', function() {
});
it('should throw when combining simple and directories', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname], simple: true});
Precompiler.cli({hasDirectory: true, templates: [1], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
it('should enumerate directories by extension', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'});
equal(/'example_2'/.test(log), true);
log = '';
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'});
equal(/'empty'/.test(log), true);
equal(/'example_1'/.test(log), true);
});
it('should protect from regexp patterns', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'});
// Success is not throwing
});
it('should output simple templates', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], simple: true});
equal(log, 'simple\n');
});
it('should output amd templates', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], amd: true});
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'});
Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, namespace: 'foo'});
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
});
it('should output commonjs templates', function() {
Handlebars.precompile = function() { return 'commonjs'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], commonjs: true});
equal(/template\(commonjs\)/.test(log), true);
});
it('should set data flag', function() {
Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true});
Precompiler.cli({templates: [emptyTemplate], simple: true, data: true});
equal(log, 'simple\n');
});
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'});
equal(log, 'simple\n');
});
it('should handle different root', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', root: 'foo/'});
Precompiler.cli({templates: [emptyTemplate], simple: true, known: 'foo'});
equal(log, 'simple\n');
});
it('should output to file system', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'});
Precompiler.cli({templates: [emptyTemplate], simple: true, output: 'file!'});
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
});
it('should handle BOM', function() {
Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; };
Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true});
equal(log, 'simple\n');
});
it('should output minimized templates', function() {
Handlebars.precompile = function() { return 'amd'; };
uglify.minify = function() { return {code: 'min'}; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], min: true});
equal(log, 'min');
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], min: true, map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
describe('#loadTemplates', function() {
it('should throw on missing template', function() {
shouldThrow(function() {
Precompiler.loadTemplates({templates: ['foo']});
}, Handlebars.Exception, 'Unable to open template file "foo"');
});
it('should enumerate directories by extension', function() {
var opts = {templates: [__dirname + '/artifacts'], extension: 'hbs'};
Precompiler.loadTemplates(opts);
equal(opts.templates.length, 1);
equal(opts.templates[0].name, 'example_2');
opts = {templates: [__dirname + '/artifacts'], extension: 'handlebars'};
Precompiler.loadTemplates(opts);
equal(opts.templates.length, 3);
equal(opts.templates[0].name, 'bom');
equal(opts.templates[1].name, 'empty');
equal(opts.templates[2].name, 'example_1');
});
it('should handle regular expression characters in extensions', function() {
Precompiler.loadTemplates({templates: [__dirname + '/artifacts'], extension: 'hb(s'});
// Success is not throwing
});
it('should handle BOM', function() {
var opts = {templates: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true};
Precompiler.loadTemplates(opts);
equal(opts.templates[0].source, 'a');
});
it('should handle different root', function() {
var opts = {templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'};
Precompiler.loadTemplates(opts);
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
});
});
});