lkwdwrd/extendable-yeoman

View on GitHub
src/base.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * This file contains main Pluggable Yeoman object.
 *
 * This object extends the base Yeoman generator object and exposes a new
 * version of it that supports a plugin architecture allowing plugins to
 * be used with generators built on top of it.
 */

// Import dependencies
import Yeoman from 'yeoman-generator';
import path from 'path';
import globby from 'globby';

/**
 * The Pluggable Yo base object
 *
 */
const Base = Yeoman.Base.extend({
    _extensionLookups: [
        '.',
        'extensions',
        'lib/extensions'
    ],
    _extensions: {},
    /**
     * Sets up the object, registering methods with the Yeoman run loop.
     *
     * @return {Object} The resulting MakeBase object.
     */
    constructor: function(){
        // Run the baser constructor.
        Yeoman.Base.apply(this, arguments);
        // Set the name
        this._generatorName = this.options.namespace.split(':')[0];
        // Find Extensions
        this._gatherExtensions();
        this._initExtensions();
    },
    /**
     * Initializes any extensions or runs any dynamic subgenerators.
     */
    _initExtensions: function(argv = process.argv){
        let generator = false;
        let ns = this.options.namespace;
        const rawNS = argv[2].split(/:/);
        const last = rawNS.pop();

        if(last[0] === '/'){
            ns = rawNS.concat(last.slice(1)).join(':');
            generator = true;
            this.run = function(cb){
                if(typeof cb === 'function'){
                    cb();
                }
            };
        }
        if(!(ns in this._extensions)){
            if(generator){
                this.env.error(`The dynamic sub-generator ${ns} does not exist.`);
            }
            return false;
        }
        for(let i = 0, length = this._extensions[ns].length; i < length; i++){
            if(generator){
                this.env.register(this._extensions[ns][i], ns + i);
                this.env.run([ns + i].concat(this.args), this.options);
            }else{
                require(this._extensions[ns][i])(this);
            }
        }
        return this;
    },
    /**
     * Search for generator extensions.
     *
     * A generator extension can modify the behavior of the extended generator,
     * or even add new sub-generators to it.
     *
     * Defaults lookups are:
     *   - ./
     *   - extensions/
     *   - lib/extensions/
     *
     * So this index file
     * `node_modules/ext-dummy-modification/lib/extensions/yo/index.js` would
     * automatically invoked when the `dummy:yo` generator is invoked.
     */
    _gatherExtensions: function(){
        const extensionModules = this._searchForExtensions(this._getNpmPaths());
        const patterns = [];

        this._extensionLookups.forEach(function(lookup){
            extensionModules.forEach(function(modulePath){
                patterns.push(path.join(modulePath, lookup));
            });
        });
        patterns.forEach(pattern => {
            globby.sync(
                ['*/index.js', '*/*/index.js', '!node_modules/*/index.js'],
                {cwd: pattern}
            ).forEach(filename => {
                const ns = `${this._generatorName}:${this.env.namespace(filename)}`;
                if(!(ns in this._extensions)){
                    this._extensions[ns] = [];
                }
                this._extensions[ns].push(path.join(pattern, filename));
            });
        });
    },
    /**
     * Search npm for every available generator extensions.
     *
     * Generator Extensions are npm packages who's name start with
     * `ext-<generator-name>-` and are place in the top level `node_module`
     * path. They can be installed globally or locally.
     *
     * @param {Array}  List of search paths
     * @return {Array} List of the generator modules path
     */
    _searchForExtensions: function(searchPaths){
        const modules = [];

        searchPaths.forEach(root => {
            if(!root){
                return;
            }
            modules.push(...globby.sync(
                    this._getExtensionPrefixes(),
                    {cwd: root}
                ).map(match => path.join(root, match)));
        });

        return modules;
    },
    /**
     * Get the npm lookup directories (`node_modules/`)
     *
     * @return {Array} lookup paths
     */
    _getNpmPaths: function(proc = process, directory = __dirname){
        const win32 = proc.platform === 'win32';
        const paths = [];

        // Add NVM prefix directory
        if(proc.env.NVM_PATH){
            paths.push(path.join(path.dirname(proc.env.NVM_PATH), 'node_modules'));
        }

        // Adding global npm directories
        // We tried using npm to get the global modules path, but it hasn't worked 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(proc.env.NODE_PATH){
            paths.push(...proc.env.NODE_PATH.split(path.delimiter).filter(path => !!path));
        }

        // global node_modules should be 4 or 2 directory up this one (most of the time)
        paths.push(path.join(directory, '../../../..'));
        paths.push(path.join(directory, '../..'));

        // adds support for generator resolving when yeoman-generator has been linked
        if(proc.argv[1]){
            paths.push(path.join(path.dirname(proc.argv[1]), '../..'));
        }

        // Default paths for each system
        if(win32){
            paths.push(path.join(proc.env.APPDATA, 'npm/node_modules'));
        }else{
            paths.push(path.join(path.sep, 'usr', 'lib', 'node_modules'));
        }

        // Walk up the CWD and add `node_modules/` folder lookup on each level
        proc.cwd().split(path.sep).forEach((part, i, parts) => {
            const prefix = !win32 ? path.sep : '';
            paths.push( prefix + path.join.apply(path, parts.slice(0, i + 1).concat(['node_modules'])));
        });

        return paths.reverse();
    },
    /**
     * Gets the default prefixes when searching for extensions via Globby.
     *
     * You can override this in your own base module if you would like your
     * extension previs to be different than the default.
     *
     * @return {Array} An array of glob strings for extension search.
     */
    _getExtensionPrefixes: function(){
        return [
            'ext-' + this._generatorName + '-*',
            '@*/ext-' + this._generatorName + '-*'
        ];
    }
} );

export default Base;