/
index.js
260 lines (215 loc) · 6.94 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
'use strict';
var fs = require('fs');
var RSVP = require('rsvp');
var rimraf = RSVP.denodeify(require('rimraf'));
var helpers = require('broccoli-kitchen-sink-helpers');
var Plugin = require('broccoli-plugin');
var debugGenerator = require('debug');
var Key = require('./key');
var canUseInputFiles = require('./can-use-input-files');
var walkSync = require('walk-sync');
CachingWriter.prototype = Object.create(Plugin.prototype);
CachingWriter.prototype.constructor = CachingWriter;
function CachingWriter (inputNodes, options) {
options = options || {};
Plugin.call(this, inputNodes, {
name: options.name,
annotation: options.annotation,
persistentOutput: true
});
this._cachingWriterPersistentOutput = !!options.persistentOutput;
this._lastKeys = null;
this._shouldBeIgnoredCache = Object.create(null);
this._resetStats();
this._cacheInclude = options.cacheInclude || [];
this._cacheExclude = options.cacheExclude || [];
this._inputFiles = options.inputFiles || {};
if (!Array.isArray(this._cacheInclude)) {
throw new Error('Invalid cacheInclude option, it must be an array or undefined.');
}
if (!Array.isArray(this._cacheExclude)) {
throw new Error('Invalid cacheExclude option, it must be an array or undefined.');
}
}
Object.defineProperty(CachingWriter.prototype, 'debug', {
get: function() {
return this._debug || (this._debug = debugGenerator(
'broccoli-caching-writer:' +
this._name +
(this._annotation ? (' > [' + this._annotation + ']') : '')));
}
});
CachingWriter.prototype._resetStats = function() {
this._stats = {
stats: 0,
files: 0
};
};
CachingWriter.prototype.getCallbackObject = function() {
return {
build: this._conditionalBuild.bind(this)
};
};
CachingWriter.prototype._conditionalBuild = function () {
var writer = this;
var start = new Date();
var invalidateCache = false;
var dir;
var lastKeys = [];
if (!writer._lastKeys) {
writer._lastKeys = [];
// Force initial build even if inputNodes is []
invalidateCache = true;
}
function shouldNotBeIgnored(relativePath) {
/*jshint validthis:true */
return !this.shouldBeIgnored(relativePath);
}
function keyForFile(relativePath) {
var fullPath = dir + '/' + relativePath;
var stats = fs.statSync(fullPath);
/*jshint validthis:true */
return new Key({
relativePath: relativePath,
fullPath: dir + '/' + relativePath,
basePath: dir,
mode: stats.mode,
size: stats.size,
mtime: stats.mtime.getTime(),
isDirectory: function() {
return false;
}
}, undefined, this.debug);
}
for (var i = 0, l = writer.inputPaths.length; i < l; i++) {
dir = writer.inputPaths[i];
var files;
if (canUseInputFiles(this._inputFiles)) {
this.debug('using inputFiles directly');
files = this._inputFiles.filter(shouldNotBeIgnored, this).map(keyForFile, this);
} else {
this.debug('walking %o', this._inputFiles);
files = walkSync.entries(dir, this._inputFiles).filter(entriesShouldNotBeIgnored, this).map(keyForEntry, this);
}
this._stats.files += files.length;
var lastKey = writer._lastKeys[i];
var key = keyForDir(dir, files, this.debug);
lastKeys.push(key);
if (!invalidateCache /* short circuit */ && !key.isEqual(lastKey)) {
invalidateCache = true;
}
}
this._stats.inputPaths = writer.inputPaths;
this.debug('%o', this._stats);
this.debug('derive cacheKey in %dms', new Date() - start);
this._resetStats();
if (invalidateCache) {
start = new Date();
writer._lastKeys = lastKeys;
var promise = RSVP.Promise.resolve();
if (!this._cachingWriterPersistentOutput) {
promise = promise.then(function() {
return rimraf(writer.outputPath);
}).then(function() {
fs.mkdirSync(writer.outputPath);
}).finally(function() {
this.debug('purge output in %dms', new Date() - start);
start = new Date();
}.bind(this));
}
return promise.then(function() {
return writer.build();
}).finally( function() {
this.debug('rebuilding cache in %dms', new Date() - start);
}.bind(this));
}
};
// Takes in a path and { include, exclude }. Tests the path using regular expressions and
// returns true if the path does not match any exclude patterns AND matches atleast
// one include pattern.
CachingWriter.prototype.shouldBeIgnored = function (fullPath) {
if (this._shouldBeIgnoredCache[fullPath] !== undefined) {
return this._shouldBeIgnoredCache[fullPath];
}
var excludePatterns = this._cacheExclude;
var includePatterns = this._cacheInclude;
var i = null;
// Check exclude patterns
for (i = 0; i < excludePatterns.length; i++) {
// An exclude pattern that returns true should be ignored
if (excludePatterns[i].test(fullPath) === true) {
return (this._shouldBeIgnoredCache[fullPath] = true);
}
}
// Check include patterns
if (includePatterns !== undefined && includePatterns.length > 0) {
for (i = 0; i < includePatterns.length; i++) {
// An include pattern that returns true (and wasn't excluded at all)
// should _not_ be ignored
if (includePatterns[i].test(fullPath) === true) {
return (this._shouldBeIgnoredCache[fullPath] = false);
}
}
// If no include patterns were matched, ignore this file.
return (this._shouldBeIgnoredCache[fullPath] = true);
}
// Otherwise, don't ignore this file
return (this._shouldBeIgnoredCache[fullPath] = false);
};
// Returns a list of matched files
CachingWriter.prototype.listFiles = function() {
return this.listEntries().map(function(entry) {
return entry.fullPath;
});
};
// Returns a list of matched files
CachingWriter.prototype.listEntries = function() {
function listEntries(keys, files) {
for (var i=0; i< keys.length; i++) {
var key = keys[i];
if (key.isDirectory()) {
var children = key.children;
if(children && children.length > 0) {
listEntries(children, files);
}
} else {
files.push(key.entry);
}
}
return files;
}
return listEntries(this._lastKeys, []).sort(function (a, b) {
var pathA = a.relativePath;
var pathB = b.relativePath;
if (pathA === pathB) {
return 0;
} else if (pathA < pathB) {
return -1;
} else {
return 1;
}
}
);
};
module.exports = CachingWriter;
function keyForDir(dir, children, debug) {
// no need to stat dir, we don't care about anything other then that they are
// a dir
return new Key({
relativePath: '/',
basePath: dir,
fullPath: dir,
mode: 16877,
size: 0,
mtime: 0,
isDirectory: function() { return true; }
}, children, debug);
}
function entriesShouldNotBeIgnored(entry) {
/*jshint validthis:true */
return !this.shouldBeIgnored(entry.relativePath);
}
function keyForEntry(entry) {
/*jshint validthis:true */
return new Key(entry, undefined, this.debug);
}