UNPKG

18 kBJavaScriptView Raw
1var fs = require('fs');
2var path = require('path');
3var relativePath = require('cached-path-relative')
4
5var browserResolve = require('browser-resolve');
6var nodeResolve = require('resolve');
7var detective = require('detective');
8var through = require('through2');
9var concat = require('concat-stream');
10var parents = require('parents');
11var combine = require('stream-combiner2');
12var duplexer = require('duplexer2');
13var xtend = require('xtend');
14var defined = require('defined');
15
16var inherits = require('inherits');
17var Transform = require('readable-stream').Transform;
18
19module.exports = Deps;
20inherits(Deps, Transform);
21
22function Deps (opts) {
23 var self = this;
24 if (!(this instanceof Deps)) return new Deps(opts);
25 Transform.call(this, { objectMode: true });
26
27 if (!opts) opts = {};
28
29 this.basedir = opts.basedir || process.cwd();
30 this.cache = opts.cache;
31 this.fileCache = opts.fileCache;
32 this.pkgCache = opts.packageCache || {};
33 this.pkgFileCache = {};
34 this.pkgFileCachePending = {};
35 this._emittedPkg = {};
36 this.visited = {};
37 this.walking = {};
38 this.entries = [];
39 this._input = [];
40
41 this.paths = opts.paths || process.env.NODE_PATH || '';
42 if (typeof this.paths === 'string') {
43 var delimiter = path.delimiter || (process.platform === 'win32' ? ';' : ':');
44 this.paths = this.paths.split(delimiter);
45 }
46 this.paths = this.paths
47 .filter(Boolean)
48 .map(function (p) {
49 return path.resolve(self.basedir, p);
50 });
51
52 this.transforms = [].concat(opts.transform).filter(Boolean);
53 this.globalTransforms = [].concat(opts.globalTransform).filter(Boolean);
54 this.resolver = opts.resolve || browserResolve;
55 this.options = xtend(opts);
56 if (!this.options.modules) this.options.modules = {};
57
58 // If the caller passes options.expose, store resolved pathnames for exposed
59 // modules in it. If not, set it anyway so it's defined later.
60 if (!this.options.expose) this.options.expose = {};
61 this.pending = 0;
62 this.inputPending = 0;
63
64 var topfile = path.join(this.basedir, '__fake.js');
65 this.top = {
66 id: topfile,
67 filename: topfile,
68 paths: this.paths,
69 basedir: this.basedir
70 };
71}
72
73Deps.prototype._isTopLevel = function (file) {
74 var isTopLevel = this.entries.some(function (main) {
75 var m = relativePath(path.dirname(main), file);
76 return m.split(/[\\\/]/).indexOf('node_modules') < 0;
77 });
78 if (!isTopLevel) {
79 var m = relativePath(this.basedir, file);
80 isTopLevel = m.split(/[\\\/]/).indexOf('node_modules') < 0;
81 }
82 return isTopLevel;
83};
84
85Deps.prototype._transform = function (row, enc, next) {
86 var self = this;
87 if (typeof row === 'string') {
88 row = { file: row };
89 }
90 if (row.transform && row.global) {
91 this.globalTransforms.push([ row.transform, row.options ]);
92 return next();
93 }
94 else if (row.transform) {
95 this.transforms.push([ row.transform, row.options ]);
96 return next();
97 }
98
99 self.pending ++;
100 var basedir = defined(row.basedir, self.basedir);
101
102 if (row.entry !== false) {
103 self.entries.push(path.resolve(basedir, row.file || row.id));
104 }
105
106 self.lookupPackage(row.file, function (err, pkg) {
107 if (err && self.options.ignoreMissing) {
108 self.emit('missing', row.file, self.top);
109 self.pending --;
110 return next();
111 }
112 if (err) return self.emit('error', err)
113 self.pending --;
114 self._input.push({ row: row, pkg: pkg });
115 next();
116 });
117};
118
119Deps.prototype._flush = function () {
120 var self = this;
121 var files = {};
122 self._input.forEach(function (r) {
123 var w = r.row, f = files[w.file || w.id];
124 if (f) {
125 f.row.entry = f.row.entry || w.entry;
126 var ex = f.row.expose || w.expose;
127 f.row.expose = ex;
128 if (ex && f.row.file === f.row.id && w.file !== w.id) {
129 f.row.id = w.id;
130 }
131 }
132 else files[w.file || w.id] = r;
133 });
134
135 Object.keys(files).forEach(function (key) {
136 var r = files[key];
137 var pkg = r.pkg || {};
138 var dir = r.row.file ? path.dirname(r.row.file) : self.basedir;
139 if (!pkg.__dirname) pkg.__dirname = dir;
140 self.walk(r.row, xtend(self.top, {
141 filename: path.join(dir, '_fake.js')
142 }));
143 });
144 if (this.pending === 0) this.push(null);
145 this._ended = true;
146};
147
148Deps.prototype.resolve = function (id, parent, cb) {
149 var self = this;
150 var opts = self.options;
151
152 if (xhas(self.cache, parent.id, 'deps', id)
153 && self.cache[parent.id].deps[id]) {
154 var file = self.cache[parent.id].deps[id];
155 var pkg = self.pkgCache[file];
156 if (pkg) return cb(null, file, pkg);
157 return self.lookupPackage(file, function (err, pkg) {
158 cb(null, file, pkg);
159 });
160 }
161
162 parent.packageFilter = function (p, x) {
163 var pkgdir = path.dirname(x);
164 if (opts.packageFilter) p = opts.packageFilter(p, x);
165 p.__dirname = pkgdir;
166
167 return p;
168 };
169
170 if (opts.extensions) parent.extensions = opts.extensions;
171 if (opts.modules) parent.modules = opts.modules;
172
173 self.resolver(id, parent, function onresolve (err, file, pkg, fakePath) {
174 if (err) return cb(err);
175 if (!file) return cb(new Error(
176 'module not found: "' + id + '" from file '
177 + parent.filename
178 ));
179
180 if (!pkg || !pkg.__dirname) {
181 self.lookupPackage(file, function (err, p) {
182 if (err) return cb(err);
183 if (!p) p = {};
184 if (!p.__dirname) p.__dirname = path.dirname(file);
185 self.pkgCache[file] = p;
186 onresolve(err, file, opts.packageFilter
187 ? opts.packageFilter(p, p.__dirname) : p,
188 fakePath
189 );
190 });
191 }
192 else cb(err, file, pkg, fakePath);
193 });
194};
195
196Deps.prototype.readFile = function (file, id, pkg) {
197 var self = this;
198 if (xhas(this.fileCache, file)) {
199 var tr = through();
200 tr.push(this.fileCache[file]);
201 tr.push(null);
202 return tr;
203 }
204 var rs = fs.createReadStream(file, {
205 encoding: 'utf8'
206 });
207 rs.on('error', function (err) { self.emit('error', err) });
208 this.emit('file', file, id);
209 return rs;
210};
211
212Deps.prototype.getTransforms = function (file, pkg, opts) {
213 if (!opts) opts = {};
214 var self = this;
215
216 var isTopLevel;
217 if (opts.builtin || opts.inNodeModules) isTopLevel = false;
218 else isTopLevel = this._isTopLevel(file);
219
220 var transforms = [].concat(isTopLevel ? this.transforms : [])
221 .concat(getTransforms(pkg, {
222 globalTransform: this.globalTransforms,
223 transformKey: this.options.transformKey
224 }))
225 ;
226 if (transforms.length === 0) return through();
227
228 var pending = transforms.length;
229 var streams = [];
230 var input = through();
231 var output = through();
232 var dup = duplexer(input, output);
233
234 for (var i = 0; i < transforms.length; i++) (function (i) {
235 makeTransform(transforms[i], function (err, trs) {
236 if (err) return self.emit('error', err)
237 streams[i] = trs;
238 if (-- pending === 0) done();
239 });
240 })(i);
241 return dup;
242
243 function done () {
244 var middle = combine.apply(null, streams);
245 middle.on('error', function (err) {
246 err.message += ' while parsing file: ' + file;
247 if (!err.filename) err.filename = file;
248 self.emit('error', err);
249 });
250 input.pipe(middle).pipe(output);
251 }
252
253 function makeTransform (tr, cb) {
254 var trOpts = {};
255 if (Array.isArray(tr)) {
256 trOpts = tr[1] || {};
257 tr = tr[0];
258 }
259 trOpts._flags = trOpts.hasOwnProperty('_flags') ? trOpts._flags : self.options;
260 if (typeof tr === 'function') {
261 var t = tr(file, trOpts);
262 self.emit('transform', t, file);
263 nextTick(cb, null, wrapTransform(t));
264 }
265 else {
266 loadTransform(tr, trOpts, function (err, trs) {
267 if (err) return cb(err);
268 cb(null, wrapTransform(trs));
269 });
270 }
271 }
272
273 function loadTransform (id, trOpts, cb) {
274 var params = { basedir: path.dirname(file) };
275 nodeResolve(id, params, function nr (err, res, again) {
276 if (err && again) return cb && cb(err);
277
278 if (err) {
279 params.basedir = pkg.__dirname;
280 return nodeResolve(id, params, function (e, r) {
281 nr(e, r, true)
282 });
283 }
284
285 if (!res) return cb(new Error(
286 'cannot find transform module ' + tr
287 + ' while transforming ' + file
288 ));
289
290 var r = require(res);
291 if (typeof r !== 'function') {
292 return cb(new Error(
293 'Unexpected ' + typeof r + ' exported by the '
294 + JSON.stringify(res) + ' package. '
295 + 'Expected a transform function.'
296 ));
297 }
298
299 var trs = r(file, trOpts);
300 self.emit('transform', trs, file);
301 cb(null, trs);
302 });
303 }
304};
305
306Deps.prototype.walk = function (id, parent, cb) {
307 var self = this;
308 var opts = self.options;
309 this.pending ++;
310
311 var rec = {};
312 var input;
313 if (typeof id === 'object') {
314 rec = xtend(id);
315 if (rec.entry === false) delete rec.entry;
316 id = rec.file || rec.id;
317 input = true;
318 this.inputPending ++;
319 }
320
321 self.resolve(id, parent, function (err, file, pkg, fakePath) {
322 // this is checked early because parent.modules is also modified
323 // by this function.
324 var builtin = has(parent.modules, id);
325
326 if (rec.expose) {
327 // Set options.expose to make the resolved pathname available to the
328 // caller. They may or may not have requested it, but it's harmless
329 // to set this if they didn't.
330 self.options.expose[rec.expose] =
331 self.options.modules[rec.expose] = file;
332 }
333 if (pkg && !self._emittedPkg[pkg.__dirname]) {
334 self._emittedPkg[pkg.__dirname] = true;
335 self.emit('package', pkg);
336 }
337
338 if (opts.postFilter && !opts.postFilter(id, file, pkg)) {
339 if (--self.pending === 0) self.push(null);
340 if (input) --self.inputPending;
341 return cb && cb(null, undefined);
342 }
343 if (err && rec.source) {
344 file = rec.file;
345
346 var ts = self.getTransforms(file, pkg);
347 ts.pipe(concat(function (body) {
348 rec.source = body.toString('utf8');
349 fromSource(file, rec.source, pkg);
350 }));
351 return ts.end(rec.source);
352 }
353 if (err && self.options.ignoreMissing) {
354 if (--self.pending === 0) self.push(null);
355 if (input) --self.inputPending;
356 self.emit('missing', id, parent);
357 return cb && cb(null, undefined);
358 }
359 if (err) return self.emit('error', err);
360 if (self.visited[file]) {
361 if (-- self.pending === 0) self.push(null);
362 if (input) --self.inputPending;
363 return cb && cb(null, file);
364 }
365 self.visited[file] = true;
366
367 if (rec.source) {
368 var ts = self.getTransforms(file, pkg);
369 ts.pipe(concat(function (body) {
370 rec.source = body.toString('utf8');
371 fromSource(file, rec.source, pkg);
372 }));
373 return ts.end(rec.source);
374 }
375
376 var c = self.cache && self.cache[file];
377 if (c) return fromDeps(file, c.source, c.package, fakePath, Object.keys(c.deps));
378
379 self.readFile(file, id, pkg)
380 .pipe(self.getTransforms(fakePath || file, pkg, {
381 builtin: builtin,
382 inNodeModules: parent.inNodeModules
383 }))
384 .pipe(concat(function (body) {
385 fromSource(file, body.toString('utf8'), pkg, fakePath);
386 }))
387 ;
388 });
389
390 function fromSource (file, src, pkg, fakePath) {
391 var deps = rec.noparse ? [] : self.parseDeps(file, src);
392 if (deps) fromDeps(file, src, pkg, fakePath, deps);
393 }
394
395 function fromDeps (file, src, pkg, fakePath, deps) {
396 var p = deps.length;
397 var resolved = {};
398
399 if (input) --self.inputPending;
400
401 (function resolve () {
402 if (self.inputPending > 0) return setTimeout(resolve);
403 deps.forEach(function (id) {
404 if (opts.filter && !opts.filter(id)) {
405 resolved[id] = false;
406 if (--p === 0) done();
407 return;
408 }
409 var isTopLevel = self._isTopLevel(fakePath || file);
410 var current = {
411 id: file,
412 filename: file,
413 paths: self.paths,
414 package: pkg,
415 inNodeModules: parent.inNodeModules || !isTopLevel
416 };
417 self.walk(id, current, function (err, r) {
418 resolved[id] = r;
419 if (--p === 0) done();
420 });
421 });
422 if (deps.length === 0) done();
423 })();
424
425 function done () {
426 if (!rec.id) rec.id = file;
427 if (!rec.source) rec.source = src;
428 if (!rec.deps) rec.deps = resolved;
429 if (!rec.file) rec.file = file;
430
431 if (self.entries.indexOf(file) >= 0) {
432 rec.entry = true;
433 }
434 self.push(rec);
435
436 if (cb) cb(null, file);
437 if (-- self.pending === 0) self.push(null);
438 }
439 }
440};
441
442Deps.prototype.parseDeps = function (file, src, cb) {
443 if (this.options.noParse === true) return [];
444 if (/\.json$/.test(file)) return [];
445
446 if (Array.isArray(this.options.noParse)
447 && this.options.noParse.indexOf(file) >= 0) {
448 return [];
449 }
450
451 try { var deps = detective(src) }
452 catch (ex) {
453 var message = ex && ex.message ? ex.message : ex;
454 this.emit('error', new Error(
455 'Parsing file ' + file + ': ' + message
456 ));
457 return;
458 }
459 return deps;
460};
461
462Deps.prototype.lookupPackage = function (file, cb) {
463 var self = this;
464
465 var cached = this.pkgCache[file];
466 if (cached) return nextTick(cb, null, cached);
467 if (cached === false) return nextTick(cb, null, undefined);
468
469 var dirs = parents(file ? path.dirname(file) : self.basedir);
470
471 (function next () {
472 if (dirs.length === 0) {
473 self.pkgCache[file] = false;
474 return cb(null, undefined);
475 }
476 var dir = dirs.shift();
477 if (dir.split(/[\\\/]/).slice(-1)[0] === 'node_modules') {
478 return cb(null, undefined);
479 }
480
481 var pkgfile = path.join(dir, 'package.json');
482
483 var cached = self.pkgCache[pkgfile];
484 if (cached) return nextTick(cb, null, cached);
485 else if (cached === false) return next();
486
487 var pcached = self.pkgFileCachePending[pkgfile];
488 if (pcached) return pcached.push(onpkg);
489 pcached = self.pkgFileCachePending[pkgfile] = [];
490
491 fs.readFile(pkgfile, function (err, src) {
492 if (err) return onpkg();
493 try { var pkg = JSON.parse(src) }
494 catch (err) {
495 return onpkg(new Error([
496 err + ' while parsing json file ' + pkgfile
497 ].join('')))
498 }
499 pkg.__dirname = dir;
500
501 self.pkgCache[pkgfile] = pkg;
502 self.pkgCache[file] = pkg;
503 onpkg(null, pkg);
504 });
505
506 function onpkg (err, pkg) {
507 if (self.pkgFileCachePending[pkgfile]) {
508 var fns = self.pkgFileCachePending[pkgfile];
509 delete self.pkgFileCachePending[pkgfile];
510 fns.forEach(function (f) { f(err, pkg) });
511 }
512 if (err) cb(err)
513 else if (pkg) cb(null, pkg)
514 else {
515 self.pkgCache[pkgfile] = false;
516 next();
517 }
518 }
519 })();
520};
521
522function getTransforms (pkg, opts) {
523 var trx = [];
524 if (opts.transformKey) {
525 var n = pkg;
526 var keys = opts.transformKey;
527 for (var i = 0; i < keys.length; i++) {
528 if (n && typeof n === 'object') n = n[keys[i]];
529 else break;
530 }
531 if (i === keys.length) {
532 trx = [].concat(n).filter(Boolean);
533 }
534 }
535 return trx.concat(opts.globalTransform || []);
536}
537
538function nextTick (cb) {
539 var args = [].slice.call(arguments, 1);
540 process.nextTick(function () { cb.apply(null, args) });
541}
542
543function xhas (obj) {
544 if (!obj) return false;
545 for (var i = 1; i < arguments.length; i++) {
546 var key = arguments[i];
547 if (!has(obj, key)) return false;
548 obj = obj[key];
549 }
550 return true;
551}
552
553function has (obj, key) {
554 return obj && Object.prototype.hasOwnProperty.call(obj, key);
555}
556
557function wrapTransform (tr) {
558 if (typeof tr.read === 'function') return tr;
559 var input = through(), output = through();
560 input.pipe(tr).pipe(output);
561 var wrapper = duplexer(input, output);
562 tr.on('error', function (err) { wrapper.emit('error', err) });
563 return wrapper;
564}