PageRenderTime 42ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/commands/compile.js

https://github.com/KINGSABRI/nodefront
JavaScript | 468 lines | 255 code | 56 blank | 157 comment | 39 complexity | 83ba41cc509e36952f25335ae34b62f4 MD5 | raw file
Possible License(s): MIT
  1. var fs = require('fs');
  2. var q = require('q');
  3. var jade = require('jade');
  4. var stylus = require('stylus');
  5. var pathLib = require('path');
  6. var utils = require('../lib/utils');
  7. // files with these extensions need to be compiled
  8. var compiledExtensions = {
  9. 'jade': undefined,
  10. 'styl': undefined,
  11. 'stylus': undefined
  12. };
  13. // compile functions for each file; file name => function map
  14. var compileFns = {};
  15. // what files are dependent on a given file; file => dependents map
  16. var dependents = {};
  17. // what are a given file's dependencies; file => dependencies map; inverse of
  18. // above map
  19. var dependencies = {};
  20. // regular expressions for finding dependencies
  21. var rJadeInclude = /^[ \t]*include[ \t]+([^\n]+)/gm;
  22. var rJadeExtends = /^[ \t]*extends?[ \t]+([^\n]+)/gm;
  23. var rStylusInclude = /^[ \t]*@import[ \t]+([^\n]+)/gm;
  24. /**
  25. * Node Module Export
  26. * ------------------
  27. * Exports the compile command for nodefront. This finds all *.jade and *.styl
  28. * target files in the current directory and compiles them to corresponding
  29. * *.html and *.css.
  30. *
  31. * @param env - the command-line environment
  32. * If the -r/--recursive flag is specified, the current directory is
  33. * recursively searched for target files.
  34. *
  35. * If the -w/--watch flag is specified, target files are recompiled upon
  36. * modification. Dependencies are also evaluated so that if, for example,
  37. * layout.jade includes index.jade, when index.jade is modified, both
  38. * index.jade and layout.jade are recompiled. This is referred to as
  39. * "dependency-intelligent" recompilation
  40. *
  41. * If the -s/--serve <port> flag is specified, the files in the current
  42. * directory are served on localhost at the given port number, which
  43. * defaults to 3000.
  44. *
  45. * If the -l/--live <port> flag is specified, -w/--watch and -s/--serve <port>
  46. * are implied. Not only are files recompiled upon modification, but the
  47. * browser is also automatically updated when corresponding HTML/CSS/JS/Jade/
  48. * Stylus files are altered. In the case of CSS/Stylus, link tags on the page
  49. * are simply removed and re-added. For HTML/JS/Jade, the browser is refreshed
  50. * entirely.
  51. *
  52. * @param shouldPromise - if true, returns a promise that yields completion
  53. */
  54. module.exports = exports = function(env, shouldPromise) {
  55. var server;
  56. var io;
  57. if (env.serve || env.live) {
  58. // serve the files on localhost
  59. if (typeof env.serve == 'number') {
  60. server = serveFilesLocally(env.serve, env.live);
  61. } else {
  62. server = serveFilesLocally(3000, env.live);
  63. }
  64. }
  65. if (env.live) {
  66. // initiate the socket connection
  67. io = require('socket.io').listen(server);
  68. io.sockets.on('connection', function(socket) {
  69. socket.on('resolvePaths', function(paths) {
  70. var resolvedPaths = [];
  71. var absPath;
  72. // use path library to resolve paths
  73. resolvedPaths[0] = pathLib.resolve('.' + paths[0]);
  74. resolvedPaths[1] = {};
  75. resolvedPaths[2] = {};
  76. // resolve each path in the latter two arrays, keeping track of the new
  77. // path and original in a map of new => original
  78. for (var i = 1; i <= 2; i++) {
  79. for (var j = 0; j < paths[i].length; j++) {
  80. absPath = pathLib.resolve('.' + paths[i][j]);
  81. resolvedPaths[i][absPath] = paths[i][j];
  82. }
  83. }
  84. // let the client know
  85. socket.emit('pathsResolved', resolvedPaths);
  86. });
  87. });
  88. }
  89. var promise = findFilesToCompile(env.recursive)
  90. .then(function(compileData) {
  91. var promises = [];
  92. // process each file to compile
  93. compileData.forEach(function(compileDatum) {
  94. // compileDatum is in the form [file_name_without_extension, extension,
  95. // contents]; extract this information
  96. var fileNameSansExtension = compileDatum[0];
  97. var extension = compileDatum[1];
  98. var contents = compileDatum[2];
  99. var fileName = fileNameSansExtension + '.' + extension;
  100. // compile the current file and record this function
  101. var compileFn = generateCompileFn(fileNameSansExtension, extension,
  102. contents, env.live);
  103. compileFns[fileName] = compileFn;
  104. promises.push(compileFn());
  105. if (env.watch || env.live) {
  106. // record dependencies for dependency-intelligent recompilation
  107. recordDependencies(fileName, extension, contents);
  108. recompileUponModification(fileName, extension, io);
  109. }
  110. });
  111. return q.all(promises);
  112. });
  113. if (shouldPromise) {
  114. return promise;
  115. } else {
  116. promise.end();
  117. }
  118. if (env.live) {
  119. // communicate to client whenever file is modified
  120. trackModificationsLive(io, env.recursive);
  121. }
  122. };
  123. /**
  124. * Function: trackModificationsLive
  125. * --------------------------------
  126. * Watch all HTML, CSS, and JS files for modifications. Emit a fileModified
  127. * event to the client upon modification to allow for live refreshes.
  128. *
  129. * @param io - socket.io connection if this is live mode
  130. * @param recursive - true to watch all files in subdirectories as well
  131. */
  132. function trackModificationsLive(io, recursive) {
  133. utils.readDirWithFilter('.', true, /\.(html|css|js)$/, true)
  134. .then(function(files) {
  135. files.forEach(function(fileName) {
  136. // use a small interval for quick live refreshes
  137. utils.watchFileForModification(fileName, 200, function() {
  138. io.sockets.emit('fileModified', fileName);
  139. });
  140. });
  141. });
  142. }
  143. /**
  144. * Function: serveFilesLocally
  145. * ---------------------------
  146. * Serves the files in the current directory on localhost at the given port
  147. * number.
  148. *
  149. * @param port - the port number to serve the files on
  150. * @param live - true if this is live mode
  151. *
  152. * @return the node HTTP server
  153. */
  154. function serveFilesLocally(port, live) {
  155. var http = require('http');
  156. var mime = require('mime');
  157. if (live) {
  158. // scripts to dynamically insert into html pages
  159. var scripts = '<script src="/socket.io/socket.io.js"></script>' +
  160. '<script src="/nodefront/live.js"></script>';
  161. }
  162. var server = http.createServer(function(request, response) {
  163. if (request.method == 'GET') {
  164. // file path in current directory
  165. var path = '.' + request.url.split('?')[0];
  166. var mimeType = mime.lookup(path, 'text/plain');
  167. var charset = mime.charsets.lookup(mimeType, '');
  168. var binary = charset !== 'UTF-8';
  169. // redirect /nodefront/live.js request to nodefront's live.js
  170. if (live && path === './nodefront/live.js') {
  171. path = pathLib.resolve(__dirname + '/../live.js');
  172. }
  173. // if file exists, serve it; otherwise, return a 404
  174. utils.readFile(path, binary)
  175. .then(function(contents) {
  176. // find this file's mime type or default to text/plain
  177. response.writeHead(200, {'Content-Type': mimeType});
  178. if (live && mimeType === 'text/html') {
  179. // add scripts before end body tag
  180. contents = contents.replace('</body>', scripts + '</body>');
  181. // if no end body tag is present, just append scripts
  182. if (contents.indexOf(scripts) === -1) {
  183. contents = contents + scripts;
  184. }
  185. }
  186. if (binary) {
  187. response.end(contents, 'binary');
  188. } else {
  189. response.end(contents);
  190. }
  191. }, function(err) {
  192. response.writeHead(404, {'Content-Type': 'text/plain'});
  193. response.end('File not found.');
  194. })
  195. .end();
  196. } else {
  197. // bad request error code
  198. response.writeHead(400, {'Content-Type': 'text/plain'});
  199. response.end('Unsupported request type.');
  200. }
  201. }).listen(port, '127.0.0.1');
  202. console.log('Serving your files at http://127.0.0.1:' + port + '/.');
  203. return server;
  204. }
  205. /**
  206. * Function: recompileUponModification
  207. * -----------------------------------
  208. * Recompiles the given file upon modification. This also recompiles any files
  209. * that depend on this file.
  210. *
  211. * @param fileName - the name of the file
  212. * @param extension - the extension of the file
  213. */
  214. function recompileUponModification(fileName, extension, io) {
  215. utils.watchFileForModification(fileName, 1000, function() {
  216. q.ncall(fs.readFile, fs, fileName, 'utf8')
  217. .then(function(contents) {
  218. // this may be a static file that doesn't have a compile function, but
  219. // is a dependency for some other compiled file
  220. if (compileFns[fileName]) {
  221. compileFns[fileName]()
  222. .end();
  223. // reset dependencies
  224. clearDependencies(fileName);
  225. recordDependencies(fileName, extension, contents);
  226. }
  227. // compile all files that depend on this one
  228. for (var dependentFile in dependents[fileName]) {
  229. console.log('Compiling dependent:', dependentFile);
  230. if (compileFns[dependentFile]) {
  231. compileFns[dependentFile]()
  232. .end();
  233. }
  234. }
  235. })
  236. .end();
  237. });
  238. }
  239. /**
  240. * Function: recordDependencies
  241. * ----------------------------
  242. * Given a file's name, extension and contents, records its compilation
  243. * dependencies for use while watching it for changes.
  244. *
  245. * @param fileName - the name of the file
  246. * @param extension - the extension of the file
  247. * @param contents - the contents of the file
  248. */
  249. function recordDependencies(fileName, extension, contents) {
  250. var dirName = pathLib.dirname(fileName);
  251. var matches;
  252. var dependencyFile;
  253. // find dependencies
  254. switch (extension) {
  255. case 'jade':
  256. while ((matches = rJadeInclude.exec(contents)) ||
  257. (matches = rJadeExtends.exec(contents))) {
  258. dependencyFile = matches[1];
  259. // if no extension is provided, use .jade
  260. if (dependencyFile.indexOf('.') === -1) {
  261. dependencyFile += '.jade';
  262. }
  263. dependencyFile = pathLib.resolve(dirName, dependencyFile);
  264. // this file is dependent upon dependencyFile
  265. // TODO: this currently only works with .jade dependency
  266. // files; add support for static files later on
  267. addDependency(dependencyFile, fileName);
  268. }
  269. break;
  270. case 'styl':
  271. case 'stylus':
  272. while ((matches = rStylusInclude.exec(contents))) {
  273. dependencyFile = matches[1];
  274. // if no extension is provided, use .styl
  275. if (dependencyFile.indexOf('.') === -1) {
  276. dependencyFile += '.styl';
  277. dependencyFile = pathLib.resolve(dirName, dependencyFile);
  278. // this may be an index include; resolve it
  279. try {
  280. fs.statSync(dependencyFile);
  281. } catch (e) {
  282. // actually including /index.styl
  283. dependencyFile = matches[1] + '/index.styl';
  284. }
  285. }
  286. addDependency(dependencyFile, fileName);
  287. }
  288. break;
  289. }
  290. }
  291. /**
  292. * Function: generateCompileFn
  293. * ---------------------------
  294. * Given a file name and its extension, returns a function that compiles this
  295. * file based off of its type (jade, stylus, etc.).
  296. *
  297. * @param fileNameSansExtension - file name without extension
  298. * @param extension - the extension of the file name
  299. * @param live - true if this is live mode
  300. *
  301. * @return function to compile this file that takes no parameters
  302. */
  303. function generateCompileFn(fileNameSansExtension, extension, live) {
  304. return function() {
  305. var fileName = fileNameSansExtension + '.' + extension;
  306. var contents = fs.readFileSync(fileName, 'utf8');
  307. switch (extension) {
  308. case 'jade':
  309. // run jade's render
  310. return q.ncall(jade.render, jade, contents, { filename: fileName })
  311. .then(function(outputHTML) {
  312. var compiledFileName = fileNameSansExtension + '.html';
  313. return utils.writeFile(compiledFileName, outputHTML)
  314. .then(function() {
  315. console.log('Compiled ' + compiledFileName + '.');
  316. });
  317. });
  318. case 'styl':
  319. case 'stylus':
  320. // run stylus' render
  321. return q.ncall(stylus.render, stylus, contents, {
  322. filename: fileName,
  323. compress: true
  324. })
  325. .then(function(outputCSS) {
  326. var compiledFileName = fileNameSansExtension + '.css';
  327. return utils.writeFile(compiledFileName, outputCSS)
  328. .then(function() {
  329. console.log('Compiled ' + compiledFileName + '.');
  330. });
  331. });
  332. }
  333. };
  334. }
  335. /**
  336. * Function: findFilesToCompile
  337. * ----------------------------
  338. * Reads the current directory and promises a list of files along with their
  339. * contents.
  340. *
  341. * @param recursive - true to read the current directory recursively
  342. * @return promise that yields an array of arrays in the form
  343. * [ [file_name_1_without_extension, file_name_1_extension, contents_1],
  344. * [file_name_2_without_extension, file_name_2_extension, contents_2], ... ].
  345. */
  346. function findFilesToCompile(recursive) {
  347. var rsFilter = '\\.(';
  348. for(var extension in compiledExtensions) {
  349. rsFilter += extension + '|';
  350. }
  351. rsFilter = rsFilter.substr(0, rsFilter.length - 1) + ')$';
  352. return utils.readDirWithFilter('.', recursive, new RegExp(rsFilter), true)
  353. .then(function(files) {
  354. var deferred = q.defer();
  355. // contains arrays of [file name sans extension, extension, contents]
  356. // i.e. for index.jade, array would be ['index', 'jade', contents of
  357. // index.jade]
  358. var compileData = [];
  359. var numFiles = files.length;
  360. files.forEach(function(file, index) {
  361. // extract extension and contents of current file
  362. var extensionLoc = file.lastIndexOf('.');
  363. var extension = file.substr(extensionLoc + 1);
  364. q.ncall(fs.readFile, fs, file, 'utf8')
  365. .then(function(contents) {
  366. compileData.push([file.substr(0, extensionLoc),
  367. extension, contents]);
  368. // done? if so, resolve with compileData
  369. if (index == numFiles - 1) {
  370. deferred.resolve(compileData);
  371. }
  372. })
  373. .end();
  374. });
  375. return deferred.promise;
  376. });
  377. }
  378. /**
  379. * Function: addDependency
  380. * -----------------------
  381. * Records a compilation dependency.
  382. *
  383. * @param fileName - the file name that dependent depends upon
  384. * @param dependent - the file name that is dependent on fileName
  385. */
  386. function addDependency(fileName, dependent) {
  387. // add to dependents map
  388. if (!dependents[fileName]) {
  389. dependents[fileName] = {};
  390. }
  391. dependents[fileName][dependent] = true;
  392. // add to dependencies map
  393. if (!dependencies[dependent]) {
  394. dependencies[dependent] = {};
  395. }
  396. dependencies[dependent][fileName] = true;
  397. }
  398. /**
  399. * Function: clearDependencies
  400. * ---------------------------
  401. * Clear the dependencies of the given file.
  402. *
  403. * @param fileName - the name of the file
  404. */
  405. function clearDependencies(fileName) {
  406. // clear dependents map
  407. for (var dependency in dependencies[fileName]) {
  408. delete dependents[dependency][fileName];
  409. }
  410. // clear dependency map
  411. delete dependencies[fileName];
  412. }