precompiler.js 8.3 KB

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