UNPKG

14.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3var path_1 = require("path");
4var chokidar = require("chokidar");
5var buildTask = require("./build");
6var copy_1 = require("./copy");
7var logger_1 = require("./logger/logger");
8var transpile_1 = require("./transpile");
9var config_1 = require("./util/config");
10var Constants = require("./util/constants");
11var errors_1 = require("./util/errors");
12var helpers_1 = require("./util/helpers");
13var interfaces_1 = require("./util/interfaces");
14// https://github.com/paulmillr/chokidar
15function 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}
48exports.watch = watch;
49function 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}
57function 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}
124function 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}
150exports.prepareWatcher = prepareWatcher;
151var queuedWatchEventsMap = new Map();
152var queuedWatchEventsTimerId;
153function buildUpdate(event, filePath, context) {
154 return queueWatchUpdatesForBuild(event, filePath, context);
155}
156exports.buildUpdate = buildUpdate;
157function 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}
181exports.queueWatchUpdatesForBuild = queueWatchUpdatesForBuild;
182// exported just for use in unit testing
183exports.buildUpdatePromise = null;
184exports.queuedChangedFileMap = new Map();
185function 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}
218exports.queueOrRunBuildUpdate = queueOrRunBuildUpdate;
219var queuedCopyChanges = [];
220var queuedCopyTimerId;
221function 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}
245exports.copyUpdate = copyUpdate;
246function 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}
319exports.runBuildUpdate = runBuildUpdate;
320var taskInfo = {
321 fullArg: '--watch',
322 shortArg: null,
323 envVar: 'IONIC_WATCH',
324 packageConfig: 'ionic_watch',
325 defaultConfigFile: 'watch.config'
326};
327var watchCount = 0;
328var BUILD_UPDATE_DEBOUNCE_MS = 20;