Skip to content

Commit cd03848

Browse files
author
benholloway
committed
finally fixed incremental compile
1 parent e933c5d commit cd03848

File tree

1 file changed

+183
-124
lines changed

1 file changed

+183
-124
lines changed

lib/build/browserify.js

Lines changed: 183 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ var through = require('through2'),
1919
*/
2020
function init() {
2121

22-
// share a common cache by default
22+
// share a common cache
2323
var caches = {
2424
cache : {},
2525
packageCache: {}
2626
};
27+
var internalCache = {};
2728

2829
// complete
2930
return {
@@ -37,15 +38,14 @@ function init() {
3738
*/
3839
function create(opt) {
3940

40-
// ensure options
41-
// browserify must be in debug mode
41+
// user options override defaults but we always use debug mode and our own cache
4242
var options = merge({
4343
anonymous : false,
4444
bowerRelative : false,
4545
projectRelative: false,
4646
transforms : [],
4747
sourceMapBase : null
48-
}, caches, opt, {
48+
}, opt, caches, {
4949
debug: true
5050
});
5151

@@ -127,158 +127,217 @@ function init() {
127127
}
128128
}
129129
}
130-
}
131-
132-
module.exports = init;
133130

134-
/**
135-
* Create an instance of the multiton that closes a fixed set of files
136-
* @param files The composition roots for the bundler
137-
* @param options A hash of options for both browserify and internal settings
138-
* @returns {function} a bundle method
139-
*/
140-
function createInstance(files, options) {
131+
/**
132+
* Create an instance of the multiton that closes a fixed set of files
133+
* @param files The composition roots for the bundler
134+
* @param options A hash of options for both browserify and internal settings
135+
* @returns {function} a bundle method
136+
*/
137+
function createInstance(files, options) {
138+
139+
// browserify must be debug mode
140+
var browserifyOpts = merge({}, options);
141+
142+
// create bundler
143+
var bundler = browserify(browserifyOpts);
144+
145+
// must setup pipeline on every reset
146+
bundler.on('reset', setupPipeline);
147+
setupPipeline();
148+
149+
// transforms with possible options
150+
// transform, [opt], transform, [opt], ...
151+
[].concat(options.transforms)
152+
.concat(requireTransform(options.bowerRelative, options.projectRelative))
153+
.forEach(function eachItem(item, i, list) {
154+
if (typeof item === 'function') {
155+
var opts = (typeof list[i + 1] === 'object') ? merge({global: true}, list[i + 1]) : {global: true};
156+
bundler.transform(item, opts);
157+
}
158+
});
141159

142-
// browserify must be debug mode
143-
var browserifyOpts = merge({}, options);
160+
// require statements
161+
[].concat(files)
162+
.forEach(function eachItem(item) {
163+
bundler.require(item, {entry: true});
164+
});
144165

145-
// create bundler
146-
var bundler = browserify(browserifyOpts);
166+
// create instance
167+
return {
168+
bundle: bundle
169+
};
147170

148-
// ensure anonymous module paths when we are minifying
149-
if (options.anonymous) {
150-
bundler.pipeline
151-
.get('label')
152-
.push(anonymousLabeler());
153-
}
171+
/**
172+
* Setup the browserify pipeline
173+
*/
174+
function setupPipeline() {
154175

155-
// transforms with possible options
156-
// transform, [opt], transform, [opt], ...
157-
[].concat(options.transforms)
158-
.concat(requireTransform(options.bowerRelative, options.projectRelative))
159-
.forEach(function eachItem(item, i, list) {
160-
if (typeof item === 'function') {
161-
var opts = (typeof list[i + 1] === 'object') ? merge({global: true}, list[i + 1]) : {global: true};
162-
bundler.transform(item, opts);
176+
// ensure anonymous module paths when we are minifying
177+
if (options.anonymous) {
178+
bundler.pipeline
179+
.get('label')
180+
.push(anonymousLabeler());
163181
}
164-
});
165182

166-
// require statements
167-
[].concat(files)
168-
.forEach(function eachItem(item) {
169-
bundler.require(item, {entry: true});
170-
});
183+
// populate the module-deps cache to achieve incremental compile
184+
var deps = bundler.pipeline.get('deps');
185+
deps.push(populateCache(deps._streams[0].cache));
186+
// TODO this cache is inexplicably different from options.cache
187+
}
171188

172-
// create instance
173-
return {
174-
bundle: bundle
175-
};
189+
/**
190+
* Compile any number of files into a bundle
191+
* @param {stream.Through} stream A stream to push files to
192+
* @param {Array.<string>|string} files Any number of files to bundle
193+
* @param {string} bundleName The name for the output file
194+
* @param {function} done Callback for completion
195+
*/
196+
function bundle(stream, bundleName) {
176197

177-
/**
178-
* Compile any number of files into a bundle
179-
* @param {stream.Through} stream A stream to push files to
180-
* @param {Array.<string>|string} files Any number of files to bundle
181-
* @param {string} bundleName The name for the output file
182-
* @param {function} done Callback for completion
183-
*/
184-
function bundle(stream, bundleName) {
198+
// create a promise
199+
var deferred = q.defer();
185200

186-
// create a promise
187-
var deferred = q.defer();
201+
// setup
202+
var outPath = path.resolve(bundleName);
203+
var mapPath = path.basename(bundleName) + '.map';
204+
var errors = [];
205+
var timeout;
188206

189-
// setup
190-
var outPath = path.resolve(bundleName);
191-
var mapPath = path.basename(bundleName) + '.map';
192-
var errors = [];
193-
var timeout;
207+
// compile
208+
bundler.bundle()
209+
.on('error', errorHandler)
210+
.pipe(outputStream());
194211

195-
// compile
196-
bundler.bundle()
197-
.on('error', errorHandler)
198-
.pipe(outputStream());
212+
// return the promise
213+
return deferred.promise;
199214

200-
// return the promise
201-
return deferred.promise;
215+
// handle an error in the context of the timeout
216+
function errorHandler(error) {
202217

203-
// handle an error in the context of the timeout
204-
function errorHandler(error) {
218+
// parse the error text
219+
var message = parseError(error.toString());
205220

206-
// parse the error text
207-
var message = parseError(error.toString());
221+
// add unique
222+
if (errors.indexOf(message) < 0) {
223+
errors.push(message);
224+
}
208225

209-
// add unique
210-
if (errors.indexOf(message) < 0) {
211-
errors.push(message);
226+
// complete overall only once there are no further errors
227+
// ensure idempotent in the case there are some late errors
228+
clearTimeout(timeout);
229+
timeout = setTimeout(onTimeout, 100);
212230
}
213231

214-
// complete overall only once there are no further errors
215-
// ensure idempotent in the case there are some late errors
216-
clearTimeout(timeout);
217-
timeout = setTimeout(onTimeout, 100);
218-
}
232+
// error has occurred and a delay has passed with no further errors
233+
function onTimeout() {
234+
deferred.reject(errors); // complete overall
235+
}
219236

220-
// error has occurred and a delay has passed with no further errors
221-
function onTimeout() {
222-
deferred.reject(errors); // complete overall
223-
}
237+
// handle the output of the bundler (as a stream)
238+
function outputStream() {
239+
var code = '';
224240

225-
// handle the output of the bundler (as a stream)
226-
function outputStream() {
227-
var code = '';
241+
function transform(buffer, encoding, done) {
242+
code += buffer.toString(); // accumulate code
243+
done();
244+
}
228245

229-
function transform(buffer, encoding, done) {
230-
code += buffer.toString(); // accumulate code
231-
done();
232-
}
246+
function flush(done) {
247+
var sourceMap = convert.fromComment(code).toObject();
248+
var external = code.replace(convert.commentRegex, '//# sourceMappingURL=' + mapPath);
249+
delete sourceMap.file;
250+
delete sourceMap.sourceRoot;
251+
delete sourceMap.sourcesContent;
252+
sourceMap.sources
253+
.forEach(rootRelative);
254+
pushFileToStream(outPath + '.map', JSON.stringify(sourceMap, null, 2));
255+
pushFileToStream(outPath, external);
256+
done();
257+
deferred.resolve(); // complete overall
258+
}
233259

234-
function flush(done) {
235-
var sourceMap = convert.fromComment(code).toObject();
236-
var external = code.replace(convert.commentRegex, '//# sourceMappingURL=' + mapPath);
237-
delete sourceMap.file;
238-
delete sourceMap.sourceRoot;
239-
delete sourceMap.sourcesContent;
240-
sourceMap.sources
241-
.forEach(rootRelative);
242-
pushFileToStream(outPath + '.map', JSON.stringify(sourceMap, null, 2));
243-
pushFileToStream(outPath, external);
244-
done();
245-
deferred.resolve(); // complete overall
260+
return through.obj(transform, flush);
246261
}
247262

248-
return through.obj(transform, flush);
263+
// stream output
264+
function pushFileToStream(path, text) {
265+
stream.push(new gutil.File({
266+
path : path,
267+
contents: new Buffer(text)
268+
}));
269+
}
249270
}
250271

251-
// stream output
252-
function pushFileToStream(path, text) {
253-
stream.push(new gutil.File({
254-
path : path,
255-
contents: new Buffer(text)
256-
}));
272+
/**
273+
* Determine the root relative form of the given file path.
274+
* If the file path is outside the project directory then just return its name.
275+
* @param {string} filePath The input path string
276+
* @param {number} An index for <code>Array.map()</code> type operations
277+
* @param {object} The array for <code>Array.map()</code> type operations
278+
* @return {string} The transformed file path
279+
*/
280+
function rootRelative(filePath, i, array) {
281+
var rootRelPath = slash(path.relative(process.cwd(), path.resolve(filePath))); // resolve relative references
282+
var isProject = (rootRelPath.slice(0, 2) !== '..');
283+
var result = [
284+
options.sourceMapBase || '',
285+
isProject ? rootRelPath : path.basename(rootRelPath)
286+
].join('/');
287+
if ((typeof i === 'number') && (typeof array === 'object')) {
288+
array[i] = result;
289+
}
290+
return result;
257291
}
258-
}
259292

260-
/**
261-
* Determine the root relative form of the given file path.
262-
* If the file path is outside the project directory then just return its name.
263-
* @param {string} filePath The input path string
264-
* @param {number} An index for <code>Array.map()</code> type operations
265-
* @param {object} The array for <code>Array.map()</code> type operations
266-
* @return {string} The transformed file path
267-
*/
268-
function rootRelative(filePath, i, array) {
269-
var rootRelPath = slash(path.relative(process.cwd(), path.resolve(filePath))); // resolve relative references
270-
var isProject = (rootRelPath.slice(0, 2) !== '..');
271-
var result = [
272-
options.sourceMapBase || '',
273-
isProject ? rootRelPath : path.basename(rootRelPath)
274-
].join('/');
275-
if ((typeof i === 'number') && (typeof array === 'object')) {
276-
array[i] = result;
293+
/**
294+
* A pipeline 'deps' stage that populates cache for incremental compile.
295+
* Called on fully transformed row but only when there is no cache hit.
296+
* @param cache The cache used by module-deps
297+
* @returns {stream.Through} a through stream
298+
*/
299+
function populateCache(cache) {
300+
function transform(row, encoding, done) {
301+
/* jshint validthis:true */
302+
var filename = row.file;
303+
304+
// set the new transformed row output
305+
internalCache[filename] = {
306+
input : fs.readFileSync(filename).toString(),
307+
output: {
308+
id : filename,
309+
source: row.source,
310+
deps : merge({}, row.deps),
311+
file : filename
312+
}
313+
};
314+
315+
// we need to use a getter as it is the only hook at which we can perform comparison
316+
// getters cannot be redefined so we create on first access and retain, hence the need
317+
// for the internal cache to store the value above
318+
if (!cache.hasOwnProperty(filename)) {
319+
Object.defineProperty(cache, filename, {
320+
get: function () {
321+
// file read and comparison is in the order of 100us
322+
var cached = internalCache[filename];
323+
var input = fs.readFileSync(filename).toString();
324+
var isMatch = (cached.input === input);
325+
return isMatch ? cached.output : undefined;
326+
}
327+
});
328+
}
329+
330+
// complete
331+
this.push(row);
332+
done();
333+
}
334+
return through.obj(transform);
277335
}
278-
return result;
279336
}
280337
}
281338

339+
module.exports = init;
340+
282341
/**
283342
* A pipeline labeler that ensures that final file names are anonymousd in the final output
284343
* @returns {stream.Through} A through stream for the labelling stage

0 commit comments

Comments
 (0)