index.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. 'use strict';
  2. const debug = require('debug')('autod');
  3. const assert = require('assert');
  4. const glob = require('glob');
  5. const path = require('path');
  6. const fs = require('fs');
  7. const readdir = require('fs-readdir-recursive');
  8. const crequire = require('crequire');
  9. const EventEmitter = require('events');
  10. const co = require('co');
  11. const urllib = require('urllib');
  12. const semver = require('semver');
  13. const DEFAULT_EXCLUDE = [ '.git', 'cov', 'coverage', '.vscode' ];
  14. const DEFAULT_TEST = [ 'test', 'tests', 'test.js', 'benchmark', 'example', 'example.js' ];
  15. const USER_AGENT = `autod@${require('./package').version} ${urllib.USER_AGENT}`;
  16. const MODULE_REG = /^(@[0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*\/)?([0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*)/;
  17. class Autod extends EventEmitter {
  18. constructor(options) {
  19. super();
  20. this.options = Object.assign({}, options);
  21. this.prepare();
  22. }
  23. prepare() {
  24. const options = this.options;
  25. assert(options.root, 'options.root required');
  26. // default options
  27. options.semver = options.semver || {};
  28. options.registry = options.registry || 'https://registry.npmmirror.com';
  29. options.registry = options.registry.replace(/\/?$/, '');
  30. options.dep = options.dep || [];
  31. options.devdep = options.devdep || [];
  32. options.root = path.resolve(this.options.root);
  33. if (options.plugin) {
  34. try {
  35. const pluginPath = path.join(options.root, 'node_modules', options.plugin);
  36. options.plugin = require(pluginPath);
  37. } catch (err) {
  38. throw new Error(`plugin ${options.plugin} not exist!`);
  39. }
  40. }
  41. // parse exclude and test
  42. const exclude = (options.exclude || []).concat(DEFAULT_EXCLUDE);
  43. const test = (options.test || []).concat(DEFAULT_TEST);
  44. options.exclude = [];
  45. options.test = [];
  46. exclude.forEach(e => {
  47. options.exclude = options.exclude.concat(glob.sync(path.join(options.root, e)).map(path.normalize));
  48. });
  49. test.forEach(t => {
  50. options.test = options.test.concat(glob.sync(path.join(options.root, t)).map(path.normalize));
  51. });
  52. // store dependencies appear in which files
  53. this.dependencyMap = {};
  54. // store fetch npm error message
  55. this.errors = [];
  56. debug('autod inited with root: %s, exclude: %j, test: %j', options.root, options.exclude, options.test);
  57. }
  58. findJsFile() {
  59. const files = readdir(this.options.root, (name, index, dir) => {
  60. const fullname = path.join(dir, name);
  61. // ignore all node_modules
  62. if (fullname.indexOf(`${path.sep}node_modules${path.sep}`) >= 0) return false;
  63. // ignore specified exclude directories or files
  64. if (this._contains(fullname, this.options.exclude)) return false;
  65. if (fs.statSync(fullname).isDirectory()) return true;
  66. const extname = path.extname(name);
  67. if (extname !== '.js' && extname !== '.jsx') return false;
  68. return true;
  69. });
  70. const jsFiles = [];
  71. const jsTestFiles = [];
  72. files.forEach(file => {
  73. file = path.join(this.options.root, file);
  74. if (this._contains(file, this.options.test)) jsTestFiles.push(file);
  75. else jsFiles.push(file);
  76. });
  77. debug('findJsFile jsFiles(%j), jsTestFiles(%j)', jsFiles, jsTestFiles);
  78. return {
  79. jsFiles, jsTestFiles,
  80. };
  81. }
  82. findDependencies() {
  83. const files = this.findJsFile();
  84. const dependencies = new Set();
  85. const devDependencies = new Set();
  86. // add to dependencies set
  87. files.jsFiles.forEach(file => {
  88. const modules = this._getDependencies(file);
  89. modules.forEach(module => dependencies.add(module));
  90. });
  91. (this.options.dep || []).forEach(dev => {
  92. dependencies.add(dev);
  93. });
  94. // exclude dependencies, add to devDependencies set
  95. files.jsTestFiles.forEach(file => {
  96. const modules = this._getDependencies(file);
  97. modules.forEach(module => {
  98. if (!dependencies.has(module)) devDependencies.add(module);
  99. });
  100. });
  101. (this.options.devdep || []).forEach(dev => {
  102. if (!dependencies.has(module)) devDependencies.add(dev);
  103. });
  104. return {
  105. dependencies: Array.from(dependencies),
  106. devDependencies: Array.from(devDependencies),
  107. };
  108. }
  109. * findVersions() {
  110. const allDependencies = this.findDependencies();
  111. let versions = {};
  112. allDependencies.dependencies.forEach(name => {
  113. versions[name] = this._fetchVersion(name);
  114. });
  115. allDependencies.devDependencies.forEach(name => {
  116. versions[name] = this._fetchVersion(name);
  117. });
  118. versions = yield versions;
  119. const dependencies = {};
  120. const devDependencies = {};
  121. allDependencies.dependencies.forEach(name => {
  122. dependencies[name] = versions[name];
  123. });
  124. allDependencies.devDependencies.forEach(name => {
  125. devDependencies[name] = versions[name];
  126. });
  127. return { dependencies, devDependencies };
  128. }
  129. * _fetchVersion(name) {
  130. try {
  131. const tag = this.options.semver.hasOwnProperty(name)
  132. ? this.options.semver[name]
  133. : 'latest';
  134. let url = `${this.options.registry}/${name}/${tag}`;
  135. let isAllVersions = false;
  136. // npm don't support range now
  137. if (semver.validRange(tag)) {
  138. url = `${this.options.registry}/${name}`;
  139. isAllVersions = true;
  140. }
  141. const res = yield urllib.request(url, {
  142. headers: {
  143. 'user-agent': USER_AGENT,
  144. // npm will response less data
  145. accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
  146. },
  147. gzip: true,
  148. timeout: 10000,
  149. dataType: 'json',
  150. });
  151. if (res.status !== 200) {
  152. throw new Error(`request ${url} response status ${res.status}`);
  153. }
  154. let version;
  155. if (isAllVersions) {
  156. // match semver in local
  157. const versions = res.data && res.data.versions;
  158. if (versions) version = semver.maxSatisfying(Object.keys(versions), tag);
  159. } else {
  160. version = res.data && res.data.version;
  161. }
  162. if (!version) {
  163. throw new Error(`no match remote version for ${name}@${tag}`);
  164. }
  165. return version;
  166. } catch (err) {
  167. this.errors.push(err);
  168. }
  169. }
  170. _getDependencies(filePath) {
  171. let file;
  172. try {
  173. file = fs.readFileSync(filePath, 'utf-8');
  174. if (!this.options.notransform && file.includes('import')) {
  175. const res = require('babel-core').transform(file, {
  176. presets: [ require('babel-preset-react'), require('babel-preset-env'), require('babel-preset-stage-0') ],
  177. });
  178. file = res.code;
  179. }
  180. } catch (err) {
  181. this.emit('warn', `Read(or transfrom) file ${filePath} error: ${err.message}`);
  182. }
  183. const modules = [];
  184. crequire(file, true).forEach(r => {
  185. const parsed = MODULE_REG.exec(r.path);
  186. if (!parsed) return;
  187. const scope = parsed[1];
  188. let name = parsed[2];
  189. if (scope) name = scope + name;
  190. if (this._isCoreModule(name)) return;
  191. modules.push(name);
  192. this.dependencyMap[name] = this.dependencyMap[name] || [];
  193. this.dependencyMap[name].push(filePath);
  194. });
  195. // support plugin parse file
  196. if (this.options.plugin) {
  197. const pluginModules = this.options.plugin(filePath, file, modules) || [];
  198. pluginModules.forEach(name => {
  199. modules.push(name);
  200. this.dependencyMap[name] = this.dependencyMap[name] || [];
  201. this.dependencyMap[name].push(filePath);
  202. });
  203. }
  204. debug('file %s get modules %j', filePath, modules);
  205. return modules;
  206. }
  207. _contains(path, matchs) {
  208. for (const match of matchs) {
  209. if (path.startsWith(match)) return true;
  210. }
  211. }
  212. _isCoreModule(name) {
  213. let filename;
  214. try {
  215. filename = require.resolve(name);
  216. } catch (err) {
  217. return false;
  218. }
  219. return filename === name;
  220. }
  221. }
  222. Autod.prototype.findVersions = co.wrap(Autod.prototype.findVersions);
  223. module.exports = Autod;