You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
7.5 KiB
239 lines
7.5 KiB
/** @module env/repository */
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const semver = require('semver');
|
|
const debug = require('debug')('yeoman:environment:repository');
|
|
const execa = require('execa');
|
|
|
|
const logger = require('./log');
|
|
|
|
const REPOSITORY_FOLDER = '.yo-repository';
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
class YeomanRepository {
|
|
constructor(repositoryPath = REPOSITORY_FOLDER) {
|
|
this.log = logger();
|
|
|
|
this._repositoryPath = path.resolve(repositoryPath);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @property
|
|
* Repository absolute path (npm --prefix).
|
|
*/
|
|
get repositoryPath() {
|
|
return this._repositoryPath;
|
|
}
|
|
|
|
set repositoryPath(repositoryPath) {
|
|
this._repositoryPath = path.resolve(repositoryPath);
|
|
delete this._nodeModulesPath;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @property nodeModulesPath
|
|
* Path to the repository's node_modules.
|
|
*/
|
|
get nodeModulesPath() {
|
|
if (!this._nodeModulesPath) {
|
|
this._nodeModulesPath = this.runPackageManager('root').stdout;
|
|
}
|
|
return this._nodeModulesPath;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Create the repositoryPath if it doesn't exists.
|
|
*/
|
|
createRepositoryFolder() {
|
|
if (!fs.existsSync(this.repositoryPath)) {
|
|
fs.mkdirSync(this.repositoryPath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Resolve the package name module path inside the [repository path]{@link repository.repositoryPath}
|
|
* @param {String} packageName - Package name. If packageName is a absolute path then modulePath is joined to it.
|
|
* @param {String} [modulePath] - Path to a module inside the package path.
|
|
* @returns {String} Absolute module path.
|
|
*/
|
|
resolvePackagePath(packageName = '', modulePath = '') {
|
|
if (path.isAbsolute(packageName)) {
|
|
return path.join(packageName, modulePath);
|
|
}
|
|
return path.join(this.nodeModulesPath, packageName, modulePath);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Remove the package from node's cache, necessary for a reinstallation.
|
|
* Removes only package.json by default, it's used to version verification.
|
|
* Removes only cache of the repository's packages.
|
|
* @param {String} packageName - Package name.
|
|
* @param {Boolean} [force=false] - If true removes every cache the package.
|
|
* @throw Error if force === false and any file other the package.json is loaded.
|
|
*/
|
|
cleanupPackageCache(packageName, force = false) {
|
|
if (!packageName) {
|
|
throw new Error('You must provide a packageName');
|
|
}
|
|
debug('Cleaning cache of %s', packageName);
|
|
const packagePath = this.resolvePackagePath(packageName);
|
|
const toCleanup = Object.keys(require.cache).filter(cache => cache.startsWith(packagePath));
|
|
if (!force && toCleanup.find(cache => !cache.endsWith('package.json'))) {
|
|
throw new Error(`Package ${packageName} already loaded`);
|
|
}
|
|
|
|
toCleanup.forEach(cache => {
|
|
delete require.cache[cache];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Run the packageManager.
|
|
* @param {String} cmd - The command.
|
|
* @param {String[]} [args] - Additional arguments.
|
|
* @param {Object} [options] - Config to be passed to execa.
|
|
*/
|
|
runPackageManager(cmd, args, options) {
|
|
const allArgs = [cmd, '-g', '--prefix', this.repositoryPath, '--loglevel', 'error'];
|
|
if (cmd === 'install') {
|
|
allArgs.push('--no-optional');
|
|
}
|
|
if (args) {
|
|
allArgs.push(...args);
|
|
}
|
|
debug('Running npm with args %o', allArgs);
|
|
return execa.sync('npm', allArgs, options);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Verify if the package is installed and matches the version range.
|
|
* @param {String} packageName - Package name.
|
|
* @param {String} versionRange - Version range of the package.
|
|
* @returns {Boolean} True if the package is installed and matches the version.
|
|
* @throws Error.
|
|
*/
|
|
verifyInstalledVersion(packageName, versionRange) {
|
|
const packagePath = this.resolvePackagePath(packageName);
|
|
const installedVersion = this.getPackageVersion(packagePath);
|
|
let installed;
|
|
if (installedVersion && !versionRange) {
|
|
installed = true;
|
|
} else if (installedVersion) {
|
|
installed = semver.validRange(versionRange) && semver.satisfies(installedVersion, versionRange);
|
|
}
|
|
return installed ? installedVersion : undefined;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Install a package into the repository.
|
|
* @param {String} packageName - Package name.
|
|
* @param {String} versionRange - range Version of the package.
|
|
* @returns {String} Package path.
|
|
* @throws Error.
|
|
*/
|
|
installPackage(packageName, versionRange) {
|
|
const pkgs = {};
|
|
if (packageName === 'yeoman-environment' && versionRange && (!semver.validRange(versionRange) || semver.lt(semver.minVersion(versionRange), '2.9.0'))) {
|
|
// Workaround buggy dependencies.
|
|
pkgs['rxjs-compat'] = '^6.0.0';
|
|
}
|
|
|
|
pkgs[packageName] = versionRange;
|
|
const success = this.installPackages(pkgs) === true;
|
|
if (!success) {
|
|
throw new Error(`Error installing package ${packageName}, version ${versionRange}.`);
|
|
}
|
|
debug(`Package ${packageName} sucessfully installed`);
|
|
return this.resolvePackagePath(packageName);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Install packages.
|
|
* @param {Object} packages - Packages to be installed.
|
|
* @returns {Boolean}
|
|
* @example
|
|
* repository.installPackages({ 'yeoman-environment': '2.3.0' });
|
|
*/
|
|
installPackages(packages) {
|
|
this.createRepositoryFolder();
|
|
debug('Installing packages %o', packages);
|
|
const packagesArgs = Object
|
|
.entries(packages)
|
|
.filter(([packageName, _]) => {
|
|
this.cleanupPackageCache(packageName);
|
|
return true;
|
|
})
|
|
.map(([packageName, version]) => version ? (semver.validRange(version) ? `${packageName}@${version}` : version) : packageName);
|
|
const result = this.runPackageManager('install', [...packagesArgs], {stdio: 'inherit'});
|
|
const success = result.exitCode === 0;
|
|
Object.keys(packages).forEach(packageName => {
|
|
if (success) {
|
|
this.log.ok(`${packageName} installed.`);
|
|
} else {
|
|
this.log.error(`${packageName} cannot be installed.`);
|
|
}
|
|
});
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Get the installed package version.
|
|
* @param {String} packageName - Package name.
|
|
* @returns {String|undefined} Package version or undefined.
|
|
*/
|
|
getPackageVersion(packageName) {
|
|
try {
|
|
const packageJson = this.resolvePackagePath(packageName, 'package.json');
|
|
return require(packageJson).version;
|
|
} catch (_) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @method
|
|
* Require a module from the repository.
|
|
* @param {String} packageName - Package name.
|
|
* @param {String} versionRange - Version range of the package.
|
|
* @param {String} modulePath - Package name.
|
|
* @returns {Object} Module.
|
|
* @throws Error.
|
|
*/
|
|
requireModule(packageName, versionRange, modulePath = '') {
|
|
const installedVersion = this.verifyInstalledVersion(packageName, versionRange);
|
|
if (installedVersion) {
|
|
debug(`Using ${packageName} installed version ${installedVersion}`);
|
|
} else {
|
|
// Installs the package if no version is installed or the version is provided and don't match the installed version.
|
|
debug(`Found ${packageName} version ${installedVersion} but requires version ${versionRange}`);
|
|
this.installPackage(packageName, versionRange);
|
|
}
|
|
const absolutePath = this.resolvePackagePath(packageName, modulePath);
|
|
debug('Loading module at %s', absolutePath);
|
|
return require(absolutePath);
|
|
}
|
|
}
|
|
|
|
module.exports = YeomanRepository;
|
|
|