123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. var path_1 = require("path");
  4. var chokidar = require("chokidar");
  5. var buildTask = require("./build");
  6. var copy_1 = require("./copy");
  7. var logger_1 = require("./logger/logger");
  8. var transpile_1 = require("./transpile");
  9. var config_1 = require("./util/config");
  10. var Constants = require("./util/constants");
  11. var errors_1 = require("./util/errors");
  12. var helpers_1 = require("./util/helpers");
  13. var interfaces_1 = require("./util/interfaces");
  14. // https://github.com/paulmillr/chokidar
  15. function watch(context, configFile) {
  16. configFile = config_1.getUserConfigFile(context, taskInfo, configFile);
  17. // Override all build options if watch is ran.
  18. context.isProd = false;
  19. context.optimizeJs = false;
  20. context.runMinifyJs = false;
  21. context.runMinifyCss = false;
  22. context.runAot = false;
  23. // Ensure that watch is true in context
  24. context.isWatch = true;
  25. context.sassState = interfaces_1.BuildState.RequiresBuild;
  26. context.transpileState = interfaces_1.BuildState.RequiresBuild;
  27. context.bundleState = interfaces_1.BuildState.RequiresBuild;
  28. context.deepLinkState = interfaces_1.BuildState.RequiresBuild;
  29. var logger = new logger_1.Logger('watch');
  30. function buildDone() {
  31. return startWatchers(context, configFile).then(function () {
  32. logger.ready();
  33. });
  34. }
  35. return buildTask.build(context)
  36. .then(buildDone, function (err) {
  37. if (err && err.isFatal) {
  38. throw err;
  39. }
  40. else {
  41. buildDone();
  42. }
  43. })
  44. .catch(function (err) {
  45. throw logger.fail(err);
  46. });
  47. }
  48. exports.watch = watch;
  49. function startWatchers(context, configFile) {
  50. var watchConfig = config_1.fillConfigDefaults(configFile, taskInfo.defaultConfigFile);
  51. var promises = [];
  52. Object.keys(watchConfig).forEach(function (key) {
  53. promises.push(startWatcher(key, watchConfig[key], context));
  54. });
  55. return Promise.all(promises);
  56. }
  57. function startWatcher(name, watcher, context) {
  58. return new Promise(function (resolve, reject) {
  59. // If a file isn't found (probably other scenarios too),
  60. // Chokidar watches don't always trigger the ready or error events
  61. // so set a timeout, and clear it if they do fire
  62. // otherwise, just reject the promise and log an error
  63. var timeoutId = setTimeout(function () {
  64. var filesWatchedString = null;
  65. if (typeof watcher.paths === 'string') {
  66. filesWatchedString = watcher.paths;
  67. }
  68. else if (Array.isArray(watcher.paths)) {
  69. filesWatchedString = watcher.paths.join(', ');
  70. }
  71. reject(new errors_1.BuildError("A watch configured to watch the following paths failed to start. It likely that a file referenced does not exist: " + filesWatchedString));
  72. }, helpers_1.getIntPropertyValue(Constants.ENV_START_WATCH_TIMEOUT));
  73. prepareWatcher(context, watcher);
  74. if (!watcher.paths) {
  75. logger_1.Logger.error("watcher config, entry " + name + ": missing \"paths\"");
  76. resolve();
  77. return;
  78. }
  79. if (!watcher.callback) {
  80. logger_1.Logger.error("watcher config, entry " + name + ": missing \"callback\"");
  81. resolve();
  82. return;
  83. }
  84. var chokidarWatcher = chokidar.watch(watcher.paths, watcher.options);
  85. var eventName = 'all';
  86. if (watcher.eventName) {
  87. eventName = watcher.eventName;
  88. }
  89. chokidarWatcher.on(eventName, function (event, filePath) {
  90. // if you're listening for a specific event vs 'all',
  91. // the event is not included and the first param is the filePath
  92. // go ahead and adjust it if filePath is null so it's uniform
  93. if (!filePath) {
  94. filePath = event;
  95. event = watcher.eventName;
  96. }
  97. filePath = path_1.normalize(path_1.resolve(path_1.join(context.rootDir, filePath)));
  98. logger_1.Logger.debug("watch callback start, id: " + watchCount + ", isProd: " + context.isProd + ", event: " + event + ", path: " + filePath);
  99. var callbackToExecute = function (event, filePath, context, watcher) {
  100. return watcher.callback(event, filePath, context);
  101. };
  102. callbackToExecute(event, filePath, context, watcher)
  103. .then(function () {
  104. logger_1.Logger.debug("watch callback complete, id: " + watchCount + ", isProd: " + context.isProd + ", event: " + event + ", path: " + filePath);
  105. watchCount++;
  106. })
  107. .catch(function (err) {
  108. logger_1.Logger.debug("watch callback error, id: " + watchCount + ", isProd: " + context.isProd + ", event: " + event + ", path: " + filePath);
  109. logger_1.Logger.debug("" + err);
  110. watchCount++;
  111. });
  112. });
  113. chokidarWatcher.on('ready', function () {
  114. clearTimeout(timeoutId);
  115. logger_1.Logger.debug("watcher ready: " + watcher.options.cwd + watcher.paths);
  116. resolve();
  117. });
  118. chokidarWatcher.on('error', function (err) {
  119. clearTimeout(timeoutId);
  120. reject(new errors_1.BuildError("watcher error: " + watcher.options.cwd + watcher.paths + ": " + err));
  121. });
  122. });
  123. }
  124. function prepareWatcher(context, watcher) {
  125. watcher.options = watcher.options || {};
  126. if (!watcher.options.cwd) {
  127. watcher.options.cwd = context.rootDir;
  128. }
  129. if (typeof watcher.options.ignoreInitial !== 'boolean') {
  130. watcher.options.ignoreInitial = true;
  131. }
  132. if (watcher.options.ignored) {
  133. if (Array.isArray(watcher.options.ignored)) {
  134. watcher.options.ignored = watcher.options.ignored.map(function (p) { return path_1.normalize(config_1.replacePathVars(context, p)); });
  135. }
  136. else if (typeof watcher.options.ignored === 'string') {
  137. // it's a string, so just do it once and leave it
  138. watcher.options.ignored = path_1.normalize(config_1.replacePathVars(context, watcher.options.ignored));
  139. }
  140. }
  141. if (watcher.paths) {
  142. if (Array.isArray(watcher.paths)) {
  143. watcher.paths = watcher.paths.map(function (p) { return path_1.normalize(config_1.replacePathVars(context, p)); });
  144. }
  145. else {
  146. watcher.paths = path_1.normalize(config_1.replacePathVars(context, watcher.paths));
  147. }
  148. }
  149. }
  150. exports.prepareWatcher = prepareWatcher;
  151. var queuedWatchEventsMap = new Map();
  152. var queuedWatchEventsTimerId;
  153. function buildUpdate(event, filePath, context) {
  154. return queueWatchUpdatesForBuild(event, filePath, context);
  155. }
  156. exports.buildUpdate = buildUpdate;
  157. function queueWatchUpdatesForBuild(event, filePath, context) {
  158. var changedFile = {
  159. event: event,
  160. filePath: filePath,
  161. ext: path_1.extname(filePath).toLowerCase()
  162. };
  163. queuedWatchEventsMap.set(filePath, changedFile);
  164. // debounce our build update incase there are multiple files
  165. clearTimeout(queuedWatchEventsTimerId);
  166. // run this code in a few milliseconds if another hasn't come in behind it
  167. queuedWatchEventsTimerId = setTimeout(function () {
  168. // figure out what actually needs to be rebuilt
  169. var queuedChangeFileList = [];
  170. queuedWatchEventsMap.forEach(function (changedFile) { return queuedChangeFileList.push(changedFile); });
  171. var changedFiles = runBuildUpdate(context, queuedChangeFileList);
  172. // clear out all the files that are queued up for the build update
  173. queuedWatchEventsMap.clear();
  174. if (changedFiles && changedFiles.length) {
  175. // cool, we've got some build updating to do ;)
  176. queueOrRunBuildUpdate(changedFiles, context);
  177. }
  178. }, BUILD_UPDATE_DEBOUNCE_MS);
  179. return Promise.resolve();
  180. }
  181. exports.queueWatchUpdatesForBuild = queueWatchUpdatesForBuild;
  182. // exported just for use in unit testing
  183. exports.buildUpdatePromise = null;
  184. exports.queuedChangedFileMap = new Map();
  185. function queueOrRunBuildUpdate(changedFiles, context) {
  186. if (exports.buildUpdatePromise) {
  187. // there is an active build going on, so queue our changes and run
  188. // another build when this one finishes
  189. // in the event this is called multiple times while queued, we are following a "last event wins" pattern
  190. // so if someone makes an edit, and then deletes a file, the last "ChangedFile" is the one we act upon
  191. changedFiles.forEach(function (changedFile) {
  192. exports.queuedChangedFileMap.set(changedFile.filePath, changedFile);
  193. });
  194. return exports.buildUpdatePromise;
  195. }
  196. else {
  197. // there is not an active build going going on
  198. // clear out any queued file changes, and run the build
  199. exports.queuedChangedFileMap.clear();
  200. var buildUpdateCompleteCallback_1 = function () {
  201. // the update is complete, so check if there are pending updates that need to be run
  202. exports.buildUpdatePromise = null;
  203. if (exports.queuedChangedFileMap.size > 0) {
  204. var queuedChangeFileList_1 = [];
  205. exports.queuedChangedFileMap.forEach(function (changedFile) {
  206. queuedChangeFileList_1.push(changedFile);
  207. });
  208. return queueOrRunBuildUpdate(queuedChangeFileList_1, context);
  209. }
  210. return Promise.resolve();
  211. };
  212. exports.buildUpdatePromise = buildTask.buildUpdate(changedFiles, context);
  213. return exports.buildUpdatePromise.then(buildUpdateCompleteCallback_1).catch(function (err) {
  214. return buildUpdateCompleteCallback_1();
  215. });
  216. }
  217. }
  218. exports.queueOrRunBuildUpdate = queueOrRunBuildUpdate;
  219. var queuedCopyChanges = [];
  220. var queuedCopyTimerId;
  221. function copyUpdate(event, filePath, context) {
  222. var changedFile = {
  223. event: event,
  224. filePath: filePath,
  225. ext: path_1.extname(filePath).toLowerCase()
  226. };
  227. // do not allow duplicates
  228. if (!queuedCopyChanges.some(function (f) { return f.filePath === filePath; })) {
  229. queuedCopyChanges.push(changedFile);
  230. // debounce our build update incase there are multiple files
  231. clearTimeout(queuedCopyTimerId);
  232. // run this code in a few milliseconds if another hasn't come in behind it
  233. queuedCopyTimerId = setTimeout(function () {
  234. var changedFiles = queuedCopyChanges.concat([]);
  235. // clear out all the files that are queued up for the build update
  236. queuedCopyChanges.length = 0;
  237. if (changedFiles && changedFiles.length) {
  238. // cool, we've got some build updating to do ;)
  239. copy_1.copyUpdate(changedFiles, context);
  240. }
  241. }, BUILD_UPDATE_DEBOUNCE_MS);
  242. }
  243. return Promise.resolve();
  244. }
  245. exports.copyUpdate = copyUpdate;
  246. function runBuildUpdate(context, changedFiles) {
  247. if (!changedFiles || !changedFiles.length) {
  248. return null;
  249. }
  250. var jsFiles = changedFiles.filter(function (f) { return f.ext === '.js'; });
  251. if (jsFiles.length) {
  252. // this is mainly for linked modules
  253. // if a linked library has changed (which would have a js extention)
  254. // we should do a full transpile build because of this
  255. context.bundleState = interfaces_1.BuildState.RequiresUpdate;
  256. }
  257. var tsFiles = changedFiles.filter(function (f) { return f.ext === '.ts'; });
  258. if (tsFiles.length) {
  259. var requiresFullBuild = false;
  260. for (var _i = 0, tsFiles_1 = tsFiles; _i < tsFiles_1.length; _i++) {
  261. var tsFile = tsFiles_1[_i];
  262. if (!transpile_1.canRunTranspileUpdate(tsFile.event, tsFiles[0].filePath, context)) {
  263. requiresFullBuild = true;
  264. break;
  265. }
  266. }
  267. if (requiresFullBuild) {
  268. // .ts file was added or deleted, we need a full rebuild
  269. context.transpileState = interfaces_1.BuildState.RequiresBuild;
  270. context.deepLinkState = interfaces_1.BuildState.RequiresBuild;
  271. }
  272. else {
  273. // .ts files have changed, so we can get away with doing an update
  274. context.transpileState = interfaces_1.BuildState.RequiresUpdate;
  275. context.deepLinkState = interfaces_1.BuildState.RequiresUpdate;
  276. }
  277. }
  278. var sassFiles = changedFiles.filter(function (f) { return /^\.s(c|a)ss$/.test(f.ext); });
  279. if (sassFiles.length) {
  280. // .scss or .sass file was changed/added/deleted, lets do a sass update
  281. context.sassState = interfaces_1.BuildState.RequiresUpdate;
  282. }
  283. var sassFilesNotChanges = changedFiles.filter(function (f) { return f.ext === '.ts' && f.event !== 'change'; });
  284. if (sassFilesNotChanges.length) {
  285. // .ts file was either added or deleted, so we'll have to
  286. // run sass again to add/remove that .ts file's potential .scss file
  287. context.sassState = interfaces_1.BuildState.RequiresUpdate;
  288. }
  289. var htmlFiles = changedFiles.filter(function (f) { return f.ext === '.html'; });
  290. if (htmlFiles.length) {
  291. if (context.bundleState === interfaces_1.BuildState.SuccessfulBuild && htmlFiles.every(function (f) { return f.event === 'change'; })) {
  292. // .html file was changed
  293. // just doing a template update is fine
  294. context.templateState = interfaces_1.BuildState.RequiresUpdate;
  295. }
  296. else {
  297. // .html file was added/deleted
  298. // we should do a full transpile build because of this
  299. context.transpileState = interfaces_1.BuildState.RequiresBuild;
  300. context.deepLinkState = interfaces_1.BuildState.RequiresBuild;
  301. }
  302. }
  303. if (context.transpileState === interfaces_1.BuildState.RequiresUpdate || context.transpileState === interfaces_1.BuildState.RequiresBuild) {
  304. if (context.bundleState === interfaces_1.BuildState.SuccessfulBuild || context.bundleState === interfaces_1.BuildState.RequiresUpdate) {
  305. // transpiling needs to happen
  306. // and there has already been a successful bundle before
  307. // so let's just do a bundle update
  308. context.bundleState = interfaces_1.BuildState.RequiresUpdate;
  309. }
  310. else {
  311. // transpiling needs to happen
  312. // but we've never successfully bundled before
  313. // so let's do a full bundle build
  314. context.bundleState = interfaces_1.BuildState.RequiresBuild;
  315. }
  316. }
  317. return changedFiles.concat();
  318. }
  319. exports.runBuildUpdate = runBuildUpdate;
  320. var taskInfo = {
  321. fullArg: '--watch',
  322. shortArg: null,
  323. envVar: 'IONIC_WATCH',
  324. packageConfig: 'ionic_watch',
  325. defaultConfigFile: 'watch.config'
  326. };
  327. var watchCount = 0;
  328. var BUILD_UPDATE_DEBOUNCE_MS = 20;