UNPKG

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