Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80657 views
1
/**
2
* Copyright 2013 Facebook, Inc.
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
var path = require('path');
18
var inherits = require('util').inherits;
19
var EventEmitter = require('events').EventEmitter;
20
21
var ProjectConfigurationLoader =
22
require('./loader/ProjectConfigurationLoader');
23
var ConfigurationTrie = require('./ConfigurationTrie');
24
var MessageList = require('./MessageList');
25
var AnalyzeChangedTask = require('./AnalyzeChangedTask');
26
27
/**
28
* A task represention a map rebuild operation
29
* @class
30
* @extends {EventEmitter}
31
*
32
* @param {Array} files
33
* @param {Array.<ResourceLoader>} loaders
34
* @param {ResourceMap} map
35
*/
36
function MapUpdateTask(files, loaders, map, options) {
37
EventEmitter.call(this);
38
39
this.files = files.map(function(file){
40
file[0] = path.normalize(file[0]);
41
return file;
42
});
43
this.map = map;
44
this.loaders = loaders;
45
this.configurationLoader = null;
46
this.maxOpenFiles = options && options.maxOpenFiles || 200;
47
this.maxProcesses = options && options.maxProcesses || 1;
48
49
this.messages = MessageList.create();
50
this.changed = [];
51
this.changedPaths = {};
52
this.newConfigurations = {};
53
this.skipped = [];
54
55
// setup ProjectConfigurationLoader, so that MapUpdateTask can resolve
56
// configurations
57
this.loaders.forEach(function(loader) {
58
if (loader.isConfiguration) {
59
this.configurationLoader = loader;
60
}
61
}, this);
62
if (!this.configurationLoader) {
63
this.configurationLoader = new ProjectConfigurationLoader();
64
}
65
}
66
inherits(MapUpdateTask, EventEmitter);
67
68
/**
69
* Runs the task
70
* @public
71
*/
72
MapUpdateTask.prototype.run = function() {
73
this.markChangedFiles(function() {
74
this.emit('changed-files', this.changed);
75
this.processChangedConfigurations(function() {
76
this.emit('changed', this.changed);
77
this.analyzeChanged(function() {
78
this.emit('analyzed', this.changed);
79
this.updateMap(function() {
80
this.emit('mapUpdated', this.changed);
81
this.postProcess(function() {
82
this.emit('postProcessed', this.map);
83
this.emit('complete', this.map);
84
});
85
});
86
});
87
});
88
});
89
return this;
90
};
91
92
/**
93
* @protected
94
* @param {String} newPath
95
* @param {Resource} oldResource
96
*/
97
MapUpdateTask.prototype.markAsChanged = function(mtime, newPath, oldResource) {
98
var filePath = newPath || oldResource.path;
99
if (!this.changedPaths[filePath]) {
100
this.changed.push(this.changedPaths[filePath] = {
101
mtime: mtime,
102
newPath: newPath,
103
oldResource: oldResource,
104
path: filePath
105
});
106
}
107
};
108
109
/**
110
* Go through found files and existing map and mark files as changed
111
* @param {Function} callback
112
*/
113
MapUpdateTask.prototype.markChangedFiles = function(callback) {
114
var visited = {};
115
var path = require('path');
116
117
this.files.forEach(function(pair) {
118
var filePath = pair[0];
119
var mtime = pair[1];
120
visited[filePath] = true;
121
var resource = this.map.getResourceByPath(filePath);
122
if (!resource) {
123
this.markAsChanged(mtime, filePath, null);
124
} else if (resource.mtime < mtime) {
125
this.markAsChanged(mtime, filePath, resource);
126
}
127
}, this);
128
129
this.map.getAllResources().forEach(function(resource) {
130
if (!visited[resource.path]) {
131
this.markAsChanged(resource.mtime, null, resource);
132
}
133
}, this);
134
callback.call(this);
135
};
136
137
/**
138
* Mark all files touched by changes in configuration
139
* @param {Function} callback
140
*/
141
MapUpdateTask.prototype.processChangedConfigurations = function(callback) {
142
var toLoad = [];
143
var affected = [];
144
145
var changedConfigurations = this.changed.filter(function(record) {
146
return this.configurationLoader.matchPath(record.path);
147
}, this);
148
changedConfigurations.forEach(function(record) {
149
if (record.newPath) {
150
toLoad.push(record);
151
}
152
if (record.oldResource) {
153
affected.push(record.oldResource);
154
}
155
});
156
157
var next = function() {
158
var affectedDirectories = [];
159
affected.forEach(function(resource) {
160
affectedDirectories.push
161
.apply(affectedDirectories, resource.getHasteRoots());
162
}, this);
163
if (affectedDirectories.length) {
164
var regex = new RegExp('^' + '(' + affectedDirectories.join('|').replace('\\','\\\\') + ')');
165
this.files.forEach(function(pair) {
166
if (regex.test(pair[0])) {
167
this.markAsChanged(
168
pair[1],
169
pair[0],
170
this.map.getResourceByPath(pair[0]));
171
}
172
}, this);
173
}
174
callback.call(this);
175
}.bind(this);
176
177
if (toLoad.length) {
178
var waiting = toLoad.length;
179
var me = this;
180
toLoad.forEach(function(record) {
181
this.configurationLoader
182
.loadFromPath(record.newPath, null, function(messages, resource) {
183
resource.mtime = record.mtime;
184
record.newResource = resource;
185
me.newConfigurations[resource.path] = resource;
186
me.messages.mergeAndRecycle(messages);
187
affected.push(resource);
188
if (--waiting === 0) {
189
next();
190
}
191
});
192
}, this);
193
} else {
194
next();
195
}
196
};
197
198
/**
199
* Parse and analyze changed files
200
* @protected
201
* @param {Function} callback
202
*/
203
MapUpdateTask.prototype.analyzeChanged = function(callback) {
204
if (!this.changed.length) {
205
callback.call(this);
206
return;
207
}
208
209
var configurations = this.files.filter(function(pair) {
210
return this.configurationLoader.matchPath(pair[0]);
211
}, this).map(function(pair) {
212
return this.newConfigurations[pair[0]] ||
213
this.map.getResourceByPath(pair[0]);
214
}, this);
215
216
var trie = new ConfigurationTrie(configurations);
217
218
// if resource was preloaded earlier just skip
219
var paths = this.changed.filter(function(record) {
220
return !record.newResource && record.newPath;
221
}).map(function(r) {
222
return r.path;
223
});
224
225
var task = new AnalyzeChangedTask(
226
this.loaders,
227
trie,
228
{
229
maxOpenFiles: this.maxOpenFiles,
230
maxProcesses: this.maxProcesses
231
});
232
233
task.runOptimaly(paths, function(messages, resources, skipped) {
234
this.messages.mergeAndRecycle(messages);
235
resources = resources.filter(function(r) { return !!r; });
236
resources.forEach(function(resource) {
237
var record = this.changedPaths[resource.path];
238
if (record) {
239
resource.mtime = record.mtime;
240
record.newResource = resource;
241
}
242
}, this);
243
244
this.skipped = skipped;
245
callback.call(this);
246
}.bind(this));
247
};
248
249
MapUpdateTask.prototype.postProcess = function(callback) {
250
var waiting = 0;
251
var me = this;
252
var toPostProcess = this.loaders.map(function() {
253
return [];
254
});
255
var loaders = this.loaders;
256
257
this.changed.forEach(function(record) {
258
if (record.newResource) {
259
for (var i = 0; i < loaders.length; i++) {
260
if (loaders[i].matchPath(record.path)) {
261
toPostProcess[i].push(record.newResource);
262
break;
263
}
264
}
265
}
266
});
267
268
function finished(messages) {
269
me.messages.mergeAndRecycle(messages);
270
if (--waiting === 0) {
271
callback.call(me);
272
}
273
}
274
waiting = toPostProcess.length;
275
276
toPostProcess.forEach(function(resources, index) {
277
loaders[index].postProcess(this.map, resources, finished);
278
}, this);
279
280
if (waiting === 0) {
281
callback.call(this);
282
}
283
};
284
285
/**
286
* Update existing map with the changes
287
* @param {Function} callback
288
*/
289
MapUpdateTask.prototype.updateMap = function(callback) {
290
this.changed.forEach(function(record) {
291
if (!record.newPath) {
292
this.map.removeResource(record.oldResource);
293
} else if (record.newResource && record.oldResource) {
294
this.map.updateResource(record.oldResource, record.newResource);
295
} else if (record.newResource) {
296
this.map.addResource(record.newResource);
297
}
298
}, this);
299
callback.call(this);
300
};
301
302
module.exports = MapUpdateTask;
303
304