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.
452 lines
16 KiB
452 lines
16 KiB
'use strict';
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const _ = require('lodash');
|
|
const globby = require('globby');
|
|
const debug = require('debug')('yeoman:environment');
|
|
const {execaOutput} = require('./util/util');
|
|
|
|
const win32 = process.platform === 'win32';
|
|
const nvm = process.env.NVM_HOME;
|
|
|
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
|
|
const PACKAGE_NAME = require(path.join(PROJECT_ROOT, 'package.json')).name;
|
|
|
|
const packageLookup = {};
|
|
|
|
/**
|
|
* @mixin
|
|
* @alias env/resolver
|
|
*/
|
|
const resolver = module.exports;
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
resolver.packageLookup = packageLookup;
|
|
|
|
/**
|
|
* Search for generators and their sub generators.
|
|
*
|
|
* A generator is a `:lookup/:name/index.js` file placed inside an npm package.
|
|
*
|
|
* Defaults lookups are:
|
|
* - ./
|
|
* - generators/
|
|
* - lib/generators/
|
|
*
|
|
* So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
|
|
* registered as `dummy:yo` generator.
|
|
*
|
|
* @param {boolean|Object} [options]
|
|
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
|
|
* globally-installed generators.
|
|
* @param {string|Array} [options.packagePaths] - Paths to look for generators.
|
|
* @param {string|Array} [options.npmPaths] - Repository paths to look for generators packages.
|
|
* @param {string|Array} [options.filePatterns='*\/index.js'] - File pattern to look for.
|
|
* @param {string|Array} [options.packagePatterns='generator-*'] - Package pattern to look for.
|
|
* @param {boolean} [options.singleResult=false] - Set true to stop lookup on the first match.
|
|
* @param {Number} [options.globbyDeep] - Deep option to be passed to globby.
|
|
* @param {function} cb - Callback called once the lookup is done. Take err as first
|
|
* parameter.
|
|
* @return {Object[]} List of generators
|
|
*/
|
|
resolver.lookup = function (options = {localOnly: false}, cb = undefined) {
|
|
// Resolve signature where options is omitted.
|
|
if (typeof options === 'function') {
|
|
cb = options;
|
|
options = {localOnly: false};
|
|
// Resolve signature where options is boolean.
|
|
} else if (typeof options === 'boolean') {
|
|
options = {localOnly: options};
|
|
}
|
|
|
|
const lookups = options.lookups || this.lookups;
|
|
// Js generators should be after, last will override registered one.
|
|
options.filePatterns = options.filePatterns ||
|
|
lookups.reduce((acc, prefix) => acc.concat([path.join(prefix, '*/index.ts'), path.join(prefix, '*/index.js')]), []);
|
|
|
|
// Backward compatibility
|
|
options.filterPaths = options.filterPaths === undefined ? false : options.filterPaths;
|
|
options.packagePatterns = options.packagePatterns || 'generator-*';
|
|
options.globbyDeep = options.globbyDeep === undefined ? 1 : options.globbyDeep;
|
|
// We want to register high priorities packages after.
|
|
options.reverse = options.reverse === undefined ? !options.singleResult : options.reverse;
|
|
|
|
const registerToScope = options.registerToScope;
|
|
|
|
const generators = [];
|
|
this.packageLookup.sync(options, module => {
|
|
const generatorPath = module.filePath;
|
|
const packagePath = module.packagePath;
|
|
let repositoryPath = path.join(packagePath, '..');
|
|
if (path.basename(repositoryPath).startsWith('@')) {
|
|
// Scoped package
|
|
repositoryPath = path.join(repositoryPath, '..');
|
|
}
|
|
let namespace = this.namespace(path.relative(repositoryPath, generatorPath), lookups);
|
|
if (registerToScope && !namespace.startsWith('@')) {
|
|
namespace = `@${registerToScope}/${namespace}`;
|
|
}
|
|
const registered = this._tryRegistering(generatorPath, packagePath, namespace);
|
|
generators.push({generatorPath, packagePath, namespace, registered});
|
|
return options.singleResult && registered;
|
|
});
|
|
|
|
if (typeof cb === 'function') {
|
|
return cb(null);
|
|
}
|
|
return generators;
|
|
};
|
|
|
|
/**
|
|
* Search for npm packages.
|
|
*
|
|
* @private
|
|
* @method
|
|
*
|
|
* @param {boolean|Object} [options]
|
|
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
|
|
* globally-installed generators.
|
|
* @param {string|Array} [options.packagePaths] - Paths to look for generators.
|
|
* @param {string|Array} [options.npmPaths] - Repository paths to look for generators packages.
|
|
* @param {string|Array} [options.filePatterns='*\/index.js'] - File pattern to look for.
|
|
* @param {string|Array} [options.packagePatterns='lookup'] - Package pattern to look for.
|
|
* @param {boolean} [options.reverse = false] - Set true reverse npmPaths/packagePaths order
|
|
* @param {function} [find] Executed for each match, return true to stop lookup.
|
|
*/
|
|
packageLookup.sync = function (options = {}, find = module => module) {
|
|
debug('Running lookup with options: %o', options);
|
|
options.filePatterns = options.filePatterns || 'package.json';
|
|
options.globbyDeep = options.globbyDeep === undefined ? 0 : options.globbyDeep;
|
|
|
|
if (options.packagePaths) {
|
|
options.packagePaths = Array.isArray(options.packagePaths) ? options.packagePaths : [options.packagePaths];
|
|
if (options.reverse) {
|
|
options.packagePaths = options.packagePaths.reverse();
|
|
}
|
|
} else {
|
|
options.npmPaths = options.npmPaths || this.getNpmPaths(options);
|
|
options.npmPaths = Array.isArray(options.npmPaths) ? options.npmPaths : [options.npmPaths];
|
|
if (options.reverse) {
|
|
options.npmPaths = options.npmPaths.reverse();
|
|
}
|
|
options.packagePaths = this.findPackagesIn(options.npmPaths, options);
|
|
}
|
|
|
|
debug('Lookup calculated options: %o', options);
|
|
|
|
const modules = [];
|
|
for (const packagePath of options.packagePaths) {
|
|
if (!fs.existsSync(packagePath) || (!fs.lstatSync(packagePath).isDirectory() && !fs.lstatSync(packagePath).isSymbolicLink())) {
|
|
continue;
|
|
}
|
|
for (const filePath of globby.sync(options.filePatterns, {cwd: packagePath, absolute: true, deep: options.globbyDeep})) {
|
|
const module = {filePath, packagePath};
|
|
if (find(module)) {
|
|
return [module];
|
|
}
|
|
modules.push(module);
|
|
}
|
|
}
|
|
return modules;
|
|
};
|
|
|
|
/**
|
|
* Search npm for every available generators.
|
|
* Generators are npm packages who's name start with `generator-` and who're placed in the
|
|
* top level `node_module` path. They can be installed globally or locally.
|
|
*
|
|
* @deprecated
|
|
* @method
|
|
*
|
|
* @param {Array} List of search paths
|
|
* @param {Object} [options]
|
|
* @param {boolean} [options.packagePatterns='generator-*'] - Pattern pattern.
|
|
* @return {Array} List of the generator modules path
|
|
*/
|
|
resolver.findGeneratorsIn = function (searchPaths, options = {}) {
|
|
if (typeof options === 'string') {
|
|
options = {packagePatterns: options};
|
|
}
|
|
|
|
options.packagePatterns = options.packagePatterns || 'generator-*';
|
|
searchPaths = Array.isArray(searchPaths) ? searchPaths : [searchPaths];
|
|
|
|
return this.packageLookup.findPackagesIn(searchPaths, options);
|
|
};
|
|
|
|
/**
|
|
* Search npm for every available generators.
|
|
* Generators are npm packages who's name start with `generator-` and who're placed in the
|
|
* top level `node_module` path. They can be installed globally or locally.
|
|
*
|
|
* @method
|
|
* @private
|
|
*
|
|
* @param {Array} searchPaths List of search paths
|
|
* @param {Object} [options]
|
|
* @param {boolean} [options.packagePatterns='generator-*'] - Pattern pattern.
|
|
* @return {Array} List of the generator modules path
|
|
*/
|
|
packageLookup.findPackagesIn = function (searchPaths, options = {}) {
|
|
options.packagePatterns = options.packagePatterns || PACKAGE_NAME;
|
|
|
|
// Remove undefined values and convert paths to absolute
|
|
searchPaths = searchPaths.filter(npmPath => npmPath).map(npmPath => path.resolve(npmPath));
|
|
|
|
let modules = [];
|
|
for (const root of searchPaths) {
|
|
if (!fs.existsSync(root) || (!fs.lstatSync(root).isDirectory() && !fs.lstatSync(root).isSymbolicLink())) {
|
|
continue;
|
|
}
|
|
// Some folders might not be readable to the current user. For those, we add a try
|
|
// catch to handle the error gracefully as globby doesn't have an option to skip
|
|
// restricted folders.
|
|
try {
|
|
modules = modules.concat(globby.sync(
|
|
options.packagePatterns,
|
|
{cwd: root, onlyDirectories: true, absolute: true, deep: 0}
|
|
));
|
|
|
|
// To limit recursive lookups into non-namespace folders within globby,
|
|
// fetch all namespaces in root, then search each namespace separately
|
|
// for generator modules
|
|
const scopes = globby.sync(
|
|
['@*'],
|
|
{cwd: root, onlyDirectories: true, absolute: true, deep: 0}
|
|
);
|
|
|
|
for (const scope of scopes) {
|
|
modules = modules.concat(globby.sync(
|
|
options.packagePatterns,
|
|
{cwd: scope, onlyDirectories: true, absolute: true, deep: 0}
|
|
));
|
|
}
|
|
} catch (error) {
|
|
debug('Could not access %s (%s)', root, error);
|
|
}
|
|
}
|
|
|
|
return modules;
|
|
};
|
|
|
|
/**
|
|
* Try registering a Generator to this environment.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {String} generatorReference A generator reference, usually a file path.
|
|
* @param {String} [packagePath] - Generator's package path.
|
|
* @param {String} [namespace] - namespace of the generator.
|
|
* @return {boolean} true if the generator have been registered.
|
|
*/
|
|
resolver._tryRegistering = function (generatorReference, packagePath, namespace) {
|
|
const realPath = fs.realpathSync(generatorReference);
|
|
|
|
try {
|
|
debug('found %s, trying to register', generatorReference);
|
|
|
|
if (!namespace && realPath !== generatorReference) {
|
|
namespace = this.namespace(generatorReference);
|
|
}
|
|
|
|
this.register(realPath, namespace, packagePath);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Unable to register %s (Error: %s)', generatorReference, error.message);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the npm lookup directories (`node_modules/`)
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @param {boolean|Object} [options]
|
|
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
|
|
* globally-installed generators.
|
|
* @param {boolean} [options.filterPaths = false] - Remove paths that don't ends
|
|
* with a supported path (don't touch at NODE_PATH paths).
|
|
* @return {Array} lookup paths
|
|
*/
|
|
resolver.getNpmPaths = function (options = {}) {
|
|
// Resolve signature where options is boolean (localOnly).
|
|
if (typeof options === 'boolean') {
|
|
options = {localOnly: options};
|
|
}
|
|
|
|
// Backward compatibility
|
|
options.filterPaths = options.filterPaths === undefined ? false : options.filterPaths;
|
|
|
|
return this.packageLookup.getNpmPaths(options);
|
|
};
|
|
|
|
/**
|
|
* Get the npm lookup directories (`node_modules/`)
|
|
*
|
|
* @method
|
|
* @private
|
|
*
|
|
* @param {boolean|Object} [options]
|
|
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
|
|
* globally-installed generators.
|
|
* @param {boolean} [options.filterPaths = false] - Remove paths that don't ends
|
|
* with a supported path (don't touch at NODE_PATH paths).
|
|
* @return {Array} lookup paths
|
|
*/
|
|
packageLookup.getNpmPaths = function (options = {}) {
|
|
// Resolve signature where options is boolean (localOnly).
|
|
if (typeof options === 'boolean') {
|
|
options = {localOnly: options};
|
|
}
|
|
// Start with the local paths.
|
|
let paths = this._getLocalNpmPaths();
|
|
|
|
// Append global paths, unless they should be excluded.
|
|
if (!options.localOnly) {
|
|
paths = paths.concat(this._getGlobalNpmPaths(options.filterPaths));
|
|
}
|
|
|
|
return _.uniq(paths);
|
|
};
|
|
|
|
/**
|
|
* Get the local npm lookup directories
|
|
* @private
|
|
* @return {Array} lookup paths
|
|
*/
|
|
packageLookup._getLocalNpmPaths = function () {
|
|
const paths = [];
|
|
|
|
// Walk up the CWD and add `node_modules/` folder lookup on each level
|
|
process.cwd().split(path.sep).forEach((part, i, parts) => {
|
|
let lookup = path.join(...parts.slice(0, i + 1), 'node_modules');
|
|
|
|
if (!win32) {
|
|
lookup = `/${lookup}`;
|
|
}
|
|
|
|
paths.push(lookup);
|
|
});
|
|
|
|
return _.uniq(paths.reverse());
|
|
};
|
|
|
|
/**
|
|
* Get the global npm lookup directories
|
|
* Reference: https://nodejs.org/api/modules.html
|
|
* @private
|
|
* @return {Array} lookup paths
|
|
*/
|
|
packageLookup._getGlobalNpmPaths = function (filterPaths = true) {
|
|
let paths = [];
|
|
|
|
// Node.js will search in the following list of GLOBAL_FOLDERS:
|
|
// 1: $HOME/.node_modules
|
|
// 2: $HOME/.node_libraries
|
|
// 3: $PREFIX/lib/node
|
|
const filterValidNpmPath = function (path, ignore = false) {
|
|
return ignore ? path : (['/node_modules', '/.node_modules', '/.node_libraries', '/node'].find(dir => path.endsWith(dir)) ? path : undefined);
|
|
};
|
|
|
|
// Default paths for each system
|
|
if (nvm) {
|
|
paths.push(path.join(process.env.NVM_HOME, process.version, 'node_modules'));
|
|
} else if (win32) {
|
|
paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
|
|
} else {
|
|
paths.push('/usr/lib/node_modules');
|
|
paths.push('/usr/local/lib/node_modules');
|
|
}
|
|
|
|
// Add NVM prefix directory
|
|
if (process.env.NVM_PATH) {
|
|
paths.push(path.join(path.dirname(process.env.NVM_PATH), 'node_modules'));
|
|
}
|
|
|
|
// Adding global npm directories
|
|
// We tried using npm to get the global modules path, but it haven't work out
|
|
// because of bugs in the parseable implementation of `ls` command and mostly
|
|
// performance issues. So, we go with our best bet for now.
|
|
if (process.env.NODE_PATH) {
|
|
paths = _.compact(process.env.NODE_PATH.split(path.delimiter)).concat(paths);
|
|
}
|
|
|
|
// Global node_modules should be 4 or 2 directory up this one (most of the time)
|
|
// Ex: /usr/another_global/node_modules/yeoman-denerator/node_modules/yeoman-environment/lib (1 level dependency)
|
|
paths.push(filterValidNpmPath(path.join(PROJECT_ROOT, '../../..'), !filterPaths));
|
|
// Ex: /usr/another_global/node_modules/yeoman-environment/lib (installed directly)
|
|
paths.push(path.join(PROJECT_ROOT, '..'));
|
|
|
|
// Get yarn global directory and infer the module paths from there
|
|
const yarnBase = execaOutput('yarn', ['global', 'dir'], {encoding: 'utf8'});
|
|
if (yarnBase) {
|
|
paths.push(path.resolve(yarnBase, 'node_modules'));
|
|
paths.push(path.resolve(yarnBase, '../link/'));
|
|
}
|
|
|
|
// Get npm global prefix and infer the module paths from there
|
|
const globalInstall = execaOutput('npm', ['root', '-g'], {encoding: 'utf8'});
|
|
if (globalInstall) {
|
|
paths.push(path.resolve(globalInstall));
|
|
}
|
|
|
|
// Adds support for generator resolving when yeoman-generator has been linked
|
|
if (process.argv[1]) {
|
|
paths.push(filterValidNpmPath(path.join(path.dirname(process.argv[1]), '../..'), !filterPaths));
|
|
}
|
|
|
|
return _.uniq(paths.filter(path => path).reverse());
|
|
};
|
|
|
|
/**
|
|
* Get or create an alias.
|
|
*
|
|
* Alias allows the `get()` and `lookup()` methods to search in alternate
|
|
* filepath for a given namespaces. It's used for example to map `generator-*`
|
|
* npm package to their namespace equivalent (without the generator- prefix),
|
|
* or to default a single namespace like `angular` to `angular:app` or
|
|
* `angular:all`.
|
|
*
|
|
* Given a single argument, this method acts as a getter. When both name and
|
|
* value are provided, acts as a setter and registers that new alias.
|
|
*
|
|
* If multiple alias are defined, then the replacement is recursive, replacing
|
|
* each alias in reverse order.
|
|
*
|
|
* An alias can be a single String or a Regular Expression. The finding is done
|
|
* based on .match().
|
|
*
|
|
* @param {String|RegExp} match
|
|
* @param {String} value
|
|
*
|
|
* @example
|
|
*
|
|
* env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
|
|
* env.alias(/^([^:]+)$/, '$1:app');
|
|
* env.alias(/^([^:]+)$/, '$1:all');
|
|
* env.alias('foo');
|
|
* // => generator-foo:all
|
|
*/
|
|
resolver.alias = function (match, value) {
|
|
if (match && value) {
|
|
this.aliases.push({
|
|
match: match instanceof RegExp ? match : new RegExp(`^${match}$`),
|
|
value
|
|
});
|
|
return this;
|
|
}
|
|
|
|
const aliases = this.aliases.slice(0).reverse();
|
|
|
|
return aliases.reduce((res, alias) => {
|
|
if (!alias.match.test(res)) {
|
|
return res;
|
|
}
|
|
|
|
return res.replace(alias.match, alias.value);
|
|
}, match);
|
|
};
|
|
|