1 | var fs = require('fs');
|
2 | var path = require('path');
|
3 | var relativePath = require('cached-path-relative')
|
4 |
|
5 | var browserResolve = require('browser-resolve');
|
6 | var nodeResolve = require('resolve');
|
7 | var detective = require('detective');
|
8 | var through = require('through2');
|
9 | var concat = require('concat-stream');
|
10 | var parents = require('parents');
|
11 | var combine = require('stream-combiner2');
|
12 | var duplexer = require('duplexer2');
|
13 | var xtend = require('xtend');
|
14 | var defined = require('defined');
|
15 |
|
16 | var inherits = require('inherits');
|
17 | var Transform = require('readable-stream').Transform;
|
18 |
|
19 | module.exports = Deps;
|
20 | inherits(Deps, Transform);
|
21 |
|
22 | function 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 |
|
59 |
|
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 |
|
73 | Deps.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 |
|
85 | Deps.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 |
|
119 | Deps.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 |
|
148 | Deps.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 |
|
196 | Deps.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 |
|
212 | Deps.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 |
|
306 | Deps.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 |
|
323 |
|
324 | var builtin = has(parent.modules, id);
|
325 |
|
326 | if (rec.expose) {
|
327 |
|
328 |
|
329 |
|
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 |
|
442 | Deps.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 |
|
462 | Deps.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 |
|
522 | function 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 |
|
538 | function nextTick (cb) {
|
539 | var args = [].slice.call(arguments, 1);
|
540 | process.nextTick(function () { cb.apply(null, args) });
|
541 | }
|
542 |
|
543 | function 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 |
|
553 | function has (obj, key) {
|
554 | return obj && Object.prototype.hasOwnProperty.call(obj, key);
|
555 | }
|
556 |
|
557 | function 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 | }
|