precompiler.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /* eslint-disable no-console */
  2. import Async from 'neo-async';
  3. import fs from 'fs';
  4. import * as Handlebars from './handlebars';
  5. import {basename} from 'path';
  6. import {SourceMapConsumer, SourceNode} from 'source-map';
  7. module.exports.loadTemplates = function(opts, callback) {
  8. loadStrings(opts, function(err, strings) {
  9. if (err) {
  10. callback(err);
  11. } else {
  12. loadFiles(opts, function(err, files) {
  13. if (err) {
  14. callback(err);
  15. } else {
  16. opts.templates = strings.concat(files);
  17. callback(undefined, opts);
  18. }
  19. });
  20. }
  21. });
  22. };
  23. function loadStrings(opts, callback) {
  24. let strings = arrayCast(opts.string),
  25. names = arrayCast(opts.name);
  26. if (names.length !== strings.length
  27. && strings.length > 1) {
  28. return callback(new Handlebars.Exception('Number of names did not match the number of string inputs'));
  29. }
  30. Async.map(strings, function(string, callback) {
  31. if (string !== '-') {
  32. callback(undefined, string);
  33. } else {
  34. // Load from stdin
  35. let buffer = '';
  36. process.stdin.setEncoding('utf8');
  37. process.stdin.on('data', function(chunk) {
  38. buffer += chunk;
  39. });
  40. process.stdin.on('end', function() {
  41. callback(undefined, buffer);
  42. });
  43. }
  44. },
  45. function(err, strings) {
  46. strings = strings.map((string, index) => ({
  47. name: names[index],
  48. path: names[index],
  49. source: string
  50. }));
  51. callback(err, strings);
  52. });
  53. }
  54. function loadFiles(opts, callback) {
  55. // Build file extension pattern
  56. let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-[\]]/g, function(arg) { return '\\' + arg; });
  57. extension = new RegExp('\\.' + extension + '$');
  58. let ret = [],
  59. queue = (opts.files || []).map((template) => ({template, root: opts.root}));
  60. Async.whilst(() => queue.length, function(callback) {
  61. let {template: path, root} = queue.shift();
  62. fs.stat(path, function(err, stat) {
  63. if (err) {
  64. return callback(new Handlebars.Exception(`Unable to open template file "${path}"`));
  65. }
  66. if (stat.isDirectory()) {
  67. opts.hasDirectory = true;
  68. fs.readdir(path, function(err, children) {
  69. /* istanbul ignore next : Race condition that being too lazy to test */
  70. if (err) {
  71. return callback(err);
  72. }
  73. children.forEach(function(file) {
  74. let childPath = path + '/' + file;
  75. if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
  76. queue.push({template: childPath, root: root || path});
  77. }
  78. });
  79. callback();
  80. });
  81. } else {
  82. fs.readFile(path, 'utf8', function(err, data) {
  83. /* istanbul ignore next : Race condition that being too lazy to test */
  84. if (err) {
  85. return callback(err);
  86. }
  87. if (opts.bom && data.indexOf('\uFEFF') === 0) {
  88. data = data.substring(1);
  89. }
  90. // Clean the template name
  91. let name = path;
  92. if (!root) {
  93. name = basename(name);
  94. } else if (name.indexOf(root) === 0) {
  95. name = name.substring(root.length + 1);
  96. }
  97. name = name.replace(extension, '');
  98. ret.push({
  99. path: path,
  100. name: name,
  101. source: data
  102. });
  103. callback();
  104. });
  105. }
  106. });
  107. },
  108. function(err) {
  109. if (err) {
  110. callback(err);
  111. } else {
  112. callback(undefined, ret);
  113. }
  114. });
  115. }
  116. module.exports.cli = function(opts) {
  117. if (opts.version) {
  118. console.log(Handlebars.VERSION);
  119. return;
  120. }
  121. if (!opts.templates.length && !opts.hasDirectory) {
  122. throw new Handlebars.Exception('Must define at least one template or directory.');
  123. }
  124. if (opts.simple && opts.min) {
  125. throw new Handlebars.Exception('Unable to minimize simple output');
  126. }
  127. const multiple = opts.templates.length !== 1 || opts.hasDirectory;
  128. if (opts.simple && multiple) {
  129. throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
  130. }
  131. // Force simple mode if we have only one template and it's unnamed.
  132. if (!opts.amd && !opts.commonjs && opts.templates.length === 1
  133. && !opts.templates[0].name) {
  134. opts.simple = true;
  135. }
  136. // Convert the known list into a hash
  137. let known = {};
  138. if (opts.known && !Array.isArray(opts.known)) {
  139. opts.known = [opts.known];
  140. }
  141. if (opts.known) {
  142. for (let i = 0, len = opts.known.length; i < len; i++) {
  143. known[opts.known[i]] = true;
  144. }
  145. }
  146. const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
  147. let output = new SourceNode();
  148. if (!opts.simple) {
  149. if (opts.amd) {
  150. output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
  151. } else if (opts.commonjs) {
  152. output.add('var Handlebars = require("' + opts.commonjs + '");');
  153. } else {
  154. output.add('(function() {\n');
  155. }
  156. output.add(' var template = Handlebars.template, templates = ');
  157. if (opts.namespace) {
  158. output.add(opts.namespace);
  159. output.add(' = ');
  160. output.add(opts.namespace);
  161. output.add(' || ');
  162. }
  163. output.add('{};\n');
  164. }
  165. opts.templates.forEach(function(template) {
  166. let options = {
  167. knownHelpers: known,
  168. knownHelpersOnly: opts.o
  169. };
  170. if (opts.map) {
  171. options.srcName = template.path;
  172. }
  173. if (opts.data) {
  174. options.data = true;
  175. }
  176. let precompiled = Handlebars.precompile(template.source, options);
  177. // If we are generating a source map, we have to reconstruct the SourceNode object
  178. if (opts.map) {
  179. let consumer = new SourceMapConsumer(precompiled.map);
  180. precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
  181. }
  182. if (opts.simple) {
  183. output.add([precompiled, '\n']);
  184. } else {
  185. if (!template.name) {
  186. throw new Handlebars.Exception('Name missing for template');
  187. }
  188. if (opts.amd && !multiple) {
  189. output.add('return ');
  190. }
  191. output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']);
  192. }
  193. });
  194. // Output the content
  195. if (!opts.simple) {
  196. if (opts.amd) {
  197. if (multiple) {
  198. output.add(['return ', objectName, ';\n']);
  199. }
  200. output.add('});');
  201. } else if (!opts.commonjs) {
  202. output.add('})();');
  203. }
  204. }
  205. if (opts.map) {
  206. output.add('\n//# sourceMappingURL=' + opts.map + '\n');
  207. }
  208. output = output.toStringWithSourceMap();
  209. output.map = output.map + '';
  210. if (opts.min) {
  211. output = minify(output, opts.map);
  212. }
  213. if (opts.map) {
  214. fs.writeFileSync(opts.map, output.map, 'utf8');
  215. }
  216. output = output.code;
  217. if (opts.output) {
  218. fs.writeFileSync(opts.output, output, 'utf8');
  219. } else {
  220. console.log(output);
  221. }
  222. };
  223. function arrayCast(value) {
  224. value = value != null ? value : [];
  225. if (!Array.isArray(value)) {
  226. value = [value];
  227. }
  228. return value;
  229. }
  230. /**
  231. * Run uglify to minify the compiled template, if uglify exists in the dependencies.
  232. *
  233. * We are using `require` instead of `import` here, because es6-modules do not allow
  234. * dynamic imports and uglify-js is an optional dependency. Since we are inside NodeJS here, this
  235. * should not be a problem.
  236. *
  237. * @param {string} output the compiled template
  238. * @param {string} sourceMapFile the file to write the source map to.
  239. */
  240. function minify(output, sourceMapFile) {
  241. try {
  242. // Try to resolve uglify-js in order to see if it does exist
  243. require.resolve('uglify-js');
  244. } catch (e) {
  245. if (e.code !== 'MODULE_NOT_FOUND') {
  246. // Something else seems to be wrong
  247. throw e;
  248. }
  249. // it does not exist!
  250. console.error('Code minimization is disabled due to missing uglify-js dependency');
  251. return output;
  252. }
  253. return require('uglify-js').minify(output.code, {
  254. sourceMap: {
  255. content: output.map,
  256. url: sourceMapFile
  257. }
  258. });
  259. }