precompiler.js 8.3 KB

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