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

/** @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;