123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- 'use strict';
- const debug = require('debug')('autod');
- const assert = require('assert');
- const glob = require('glob');
- const path = require('path');
- const fs = require('fs');
- const readdir = require('fs-readdir-recursive');
- const crequire = require('crequire');
- const EventEmitter = require('events');
- const co = require('co');
- const urllib = require('urllib');
- const semver = require('semver');
- const DEFAULT_EXCLUDE = [ '.git', 'cov', 'coverage', '.vscode' ];
- const DEFAULT_TEST = [ 'test', 'tests', 'test.js', 'benchmark', 'example', 'example.js' ];
- const USER_AGENT = `autod@${require('./package').version} ${urllib.USER_AGENT}`;
- const MODULE_REG = /^(@[0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*\/)?([0-9a-zA-Z\-\_][0-9a-zA-Z\.\-\_]*)/;
- class Autod extends EventEmitter {
- constructor(options) {
- super();
- this.options = Object.assign({}, options);
- this.prepare();
- }
- prepare() {
- const options = this.options;
- assert(options.root, 'options.root required');
- // default options
- options.semver = options.semver || {};
- options.registry = options.registry || 'https://registry.npmmirror.com';
- options.registry = options.registry.replace(/\/?$/, '');
- options.dep = options.dep || [];
- options.devdep = options.devdep || [];
- options.root = path.resolve(this.options.root);
- if (options.plugin) {
- try {
- const pluginPath = path.join(options.root, 'node_modules', options.plugin);
- options.plugin = require(pluginPath);
- } catch (err) {
- throw new Error(`plugin ${options.plugin} not exist!`);
- }
- }
- // parse exclude and test
- const exclude = (options.exclude || []).concat(DEFAULT_EXCLUDE);
- const test = (options.test || []).concat(DEFAULT_TEST);
- options.exclude = [];
- options.test = [];
- exclude.forEach(e => {
- options.exclude = options.exclude.concat(glob.sync(path.join(options.root, e)).map(path.normalize));
- });
- test.forEach(t => {
- options.test = options.test.concat(glob.sync(path.join(options.root, t)).map(path.normalize));
- });
- // store dependencies appear in which files
- this.dependencyMap = {};
- // store fetch npm error message
- this.errors = [];
- debug('autod inited with root: %s, exclude: %j, test: %j', options.root, options.exclude, options.test);
- }
- findJsFile() {
- const files = readdir(this.options.root, (name, index, dir) => {
- const fullname = path.join(dir, name);
- // ignore all node_modules
- if (fullname.indexOf(`${path.sep}node_modules${path.sep}`) >= 0) return false;
- // ignore specified exclude directories or files
- if (this._contains(fullname, this.options.exclude)) return false;
- if (fs.statSync(fullname).isDirectory()) return true;
- const extname = path.extname(name);
- if (extname !== '.js' && extname !== '.jsx') return false;
- return true;
- });
- const jsFiles = [];
- const jsTestFiles = [];
- files.forEach(file => {
- file = path.join(this.options.root, file);
- if (this._contains(file, this.options.test)) jsTestFiles.push(file);
- else jsFiles.push(file);
- });
- debug('findJsFile jsFiles(%j), jsTestFiles(%j)', jsFiles, jsTestFiles);
- return {
- jsFiles, jsTestFiles,
- };
- }
- findDependencies() {
- const files = this.findJsFile();
- const dependencies = new Set();
- const devDependencies = new Set();
- // add to dependencies set
- files.jsFiles.forEach(file => {
- const modules = this._getDependencies(file);
- modules.forEach(module => dependencies.add(module));
- });
- (this.options.dep || []).forEach(dev => {
- dependencies.add(dev);
- });
- // exclude dependencies, add to devDependencies set
- files.jsTestFiles.forEach(file => {
- const modules = this._getDependencies(file);
- modules.forEach(module => {
- if (!dependencies.has(module)) devDependencies.add(module);
- });
- });
- (this.options.devdep || []).forEach(dev => {
- if (!dependencies.has(module)) devDependencies.add(dev);
- });
- return {
- dependencies: Array.from(dependencies),
- devDependencies: Array.from(devDependencies),
- };
- }
- * findVersions() {
- const allDependencies = this.findDependencies();
- let versions = {};
- allDependencies.dependencies.forEach(name => {
- versions[name] = this._fetchVersion(name);
- });
- allDependencies.devDependencies.forEach(name => {
- versions[name] = this._fetchVersion(name);
- });
- versions = yield versions;
- const dependencies = {};
- const devDependencies = {};
- allDependencies.dependencies.forEach(name => {
- dependencies[name] = versions[name];
- });
- allDependencies.devDependencies.forEach(name => {
- devDependencies[name] = versions[name];
- });
- return { dependencies, devDependencies };
- }
- * _fetchVersion(name) {
- try {
- const tag = this.options.semver.hasOwnProperty(name)
- ? this.options.semver[name]
- : 'latest';
- let url = `${this.options.registry}/${name}/${tag}`;
- let isAllVersions = false;
- // npm don't support range now
- if (semver.validRange(tag)) {
- url = `${this.options.registry}/${name}`;
- isAllVersions = true;
- }
- const res = yield urllib.request(url, {
- headers: {
- 'user-agent': USER_AGENT,
- // npm will response less data
- accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
- },
- gzip: true,
- timeout: 10000,
- dataType: 'json',
- });
- if (res.status !== 200) {
- throw new Error(`request ${url} response status ${res.status}`);
- }
- let version;
- if (isAllVersions) {
- // match semver in local
- const versions = res.data && res.data.versions;
- if (versions) version = semver.maxSatisfying(Object.keys(versions), tag);
- } else {
- version = res.data && res.data.version;
- }
- if (!version) {
- throw new Error(`no match remote version for ${name}@${tag}`);
- }
- return version;
- } catch (err) {
- this.errors.push(err);
- }
- }
- _getDependencies(filePath) {
- let file;
- try {
- file = fs.readFileSync(filePath, 'utf-8');
- if (!this.options.notransform && file.includes('import')) {
- const res = require('babel-core').transform(file, {
- presets: [ require('babel-preset-react'), require('babel-preset-env'), require('babel-preset-stage-0') ],
- });
- file = res.code;
- }
- } catch (err) {
- this.emit('warn', `Read(or transfrom) file ${filePath} error: ${err.message}`);
- }
- const modules = [];
- crequire(file, true).forEach(r => {
- const parsed = MODULE_REG.exec(r.path);
- if (!parsed) return;
- const scope = parsed[1];
- let name = parsed[2];
- if (scope) name = scope + name;
- if (this._isCoreModule(name)) return;
- modules.push(name);
- this.dependencyMap[name] = this.dependencyMap[name] || [];
- this.dependencyMap[name].push(filePath);
- });
- // support plugin parse file
- if (this.options.plugin) {
- const pluginModules = this.options.plugin(filePath, file, modules) || [];
- pluginModules.forEach(name => {
- modules.push(name);
- this.dependencyMap[name] = this.dependencyMap[name] || [];
- this.dependencyMap[name].push(filePath);
- });
- }
- debug('file %s get modules %j', filePath, modules);
- return modules;
- }
- _contains(path, matchs) {
- for (const match of matchs) {
- if (path.startsWith(match)) return true;
- }
- }
- _isCoreModule(name) {
- let filename;
- try {
- filename = require.resolve(name);
- } catch (err) {
- return false;
- }
- return filename === name;
- }
- }
- Autod.prototype.findVersions = co.wrap(Autod.prototype.findVersions);
- module.exports = Autod;
|