1 | 'use strict';
|
2 |
|
3 | const debug = require('debug')('autod');
|
4 | const assert = require('assert');
|
5 | const glob = require('glob');
|
6 | const path = require('path');
|
7 | const fs = require('fs');
|
8 | const readdir = require('fs-readdir-recursive');
|
9 | const crequire = require('crequire');
|
10 | const EventEmitter = require('events');
|
11 | const co = require('co');
|
12 | const urllib = require('urllib');
|
13 | const semver = require('semver');
|
14 |
|
15 | const DEFAULT_EXCLUDE = [ '.git', 'cov', 'coverage', '.vscode' ];
|
16 | const DEFAULT_TEST = [ 'test', 'tests', 'test.js', 'benchmark', 'example', 'example.js' ];
|
17 | const USER_AGENT = `autod@${require('./package').version} ${urllib.USER_AGENT}`;
|
18 | const MODULE_REG = /^(@[0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*\/)?([0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*)/;
|
19 |
|
20 |
|
21 | class 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 |
|
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 |
|
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 |
|
60 | this.dependencyMap = {};
|
61 |
|
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 |
|
70 | if (fullname.indexOf(`${path.sep}node_modules${path.sep}`) >= 0) return false;
|
71 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
248 | Autod.prototype.findVersions = co.wrap(Autod.prototype.findVersions);
|
249 |
|
250 | module.exports = Autod;
|