/* eslint-disable no-console */ import Async from 'neo-async'; import fs from 'fs'; import * as Handlebars from './handlebars'; import { basename } from 'path'; import { SourceMapConsumer, SourceNode } from 'source-map'; module.exports.loadTemplates = function(opts, callback) { loadStrings(opts, function(err, strings) { if (err) { callback(err); } else { loadFiles(opts, function(err, files) { if (err) { callback(err); } else { opts.templates = strings.concat(files); callback(undefined, opts); } }); } }); }; function loadStrings(opts, callback) { let strings = arrayCast(opts.string), names = arrayCast(opts.name); if (names.length !== strings.length && strings.length > 1) { return callback( new Handlebars.Exception( 'Number of names did not match the number of string inputs' ) ); } Async.map( strings, function(string, callback) { if (string !== '-') { callback(undefined, string); } else { // Load from stdin let buffer = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', function(chunk) { buffer += chunk; }); process.stdin.on('end', function() { callback(undefined, buffer); }); } }, function(err, strings) { strings = strings.map((string, index) => ({ name: names[index], path: names[index], source: string })); callback(err, strings); } ); } function loadFiles(opts, callback) { // Build file extension pattern let extension = (opts.extension || 'handlebars').replace( /[\\^$*+?.():=!|{}\-[\]]/g, function(arg) { return '\\' + arg; } ); extension = new RegExp('\\.' + extension + '$'); let ret = [], queue = (opts.files || []).map(template => ({ template, root: opts.root })); Async.whilst( () => queue.length, function(callback) { let { template: path, root } = queue.shift(); fs.stat(path, function(err, stat) { if (err) { return callback( new Handlebars.Exception(`Unable to open template file "${path}"`) ); } if (stat.isDirectory()) { opts.hasDirectory = true; fs.readdir(path, function(err, children) { /* istanbul ignore next : Race condition that being too lazy to test */ if (err) { return callback(err); } children.forEach(function(file) { let childPath = path + '/' + file; if ( extension.test(childPath) || fs.statSync(childPath).isDirectory() ) { queue.push({ template: childPath, root: root || path }); } }); callback(); }); } else { fs.readFile(path, 'utf8', function(err, data) { /* istanbul ignore next : Race condition that being too lazy to test */ if (err) { return callback(err); } if (opts.bom && data.indexOf('\uFEFF') === 0) { data = data.substring(1); } // Clean the template name let name = path; if (!root) { name = basename(name); } else if (name.indexOf(root) === 0) { name = name.substring(root.length + 1); } name = name.replace(extension, ''); ret.push({ path: path, name: name, source: data }); callback(); }); } }); }, function(err) { if (err) { callback(err); } else { callback(undefined, ret); } } ); } module.exports.cli = function(opts) { if (opts.version) { console.log(Handlebars.VERSION); return; } if (!opts.templates.length && !opts.hasDirectory) { throw new Handlebars.Exception( 'Must define at least one template or directory.' ); } if (opts.simple && opts.min) { throw new Handlebars.Exception('Unable to minimize simple output'); } const multiple = opts.templates.length !== 1 || opts.hasDirectory; if (opts.simple && multiple) { throw new Handlebars.Exception( 'Unable to output multiple templates in simple mode' ); } // Force simple mode if we have only one template and it's unnamed. if ( !opts.amd && !opts.commonjs && opts.templates.length === 1 && !opts.templates[0].name ) { opts.simple = true; } // Convert the known list into a hash let known = {}; if (opts.known && !Array.isArray(opts.known)) { opts.known = [opts.known]; } if (opts.known) { for (let i = 0, len = opts.known.length; i < len; i++) { known[opts.known[i]] = true; } } const objectName = opts.partial ? 'Handlebars.partials' : 'templates'; let output = new SourceNode(); if (!opts.simple) { if (opts.amd) { output.add( "define(['" + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];' ); } else if (opts.commonjs) { output.add('var Handlebars = require("' + opts.commonjs + '");'); } else { output.add('(function() {\n'); } output.add(' var template = Handlebars.template, templates = '); if (opts.namespace) { output.add(opts.namespace); output.add(' = '); output.add(opts.namespace); output.add(' || '); } output.add('{};\n'); } opts.templates.forEach(function(template) { 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 (!template.name) { throw new Handlebars.Exception('Name missing for template'); } if (opts.amd && !multiple) { output.add('return '); } output.add([ objectName, "['", template.name, "'] = template(", precompiled, ');\n' ]); } }); // Output the content if (!opts.simple) { if (opts.amd) { if (multiple) { output.add(['return ', objectName, ';\n']); } output.add('});'); } else if (!opts.commonjs) { output.add('})();'); } } if (opts.map) { output.add('\n//# sourceMappingURL=' + opts.map + '\n'); } output = output.toStringWithSourceMap(); output.map = output.map + ''; if (opts.min) { output = minify(output, opts.map); } if (opts.map) { fs.writeFileSync(opts.map, output.map, 'utf8'); } output = output.code; if (opts.output) { fs.writeFileSync(opts.output, output, 'utf8'); } else { console.log(output); } }; function arrayCast(value) { value = value != null ? value : []; if (!Array.isArray(value)) { value = [value]; } return value; } /** * Run uglify to minify the compiled template, if uglify exists in the dependencies. * * We are using `require` instead of `import` here, because es6-modules do not allow * dynamic imports and uglify-js is an optional dependency. Since we are inside NodeJS here, this * should not be a problem. * * @param {string} output the compiled template * @param {string} sourceMapFile the file to write the source map to. */ function minify(output, sourceMapFile) { try { // Try to resolve uglify-js in order to see if it does exist require.resolve('uglify-js'); } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') { // Something else seems to be wrong throw e; } // it does not exist! console.error( 'Code minimization is disabled due to missing uglify-js dependency' ); return output; } return require('uglify-js').minify(output.code, { sourceMap: { content: output.map, url: sourceMapFile } }); }