Home Reference Source Test

source/Module.js

import {EventEmitter} from 'events';

import {dirname, join} from 'path';

import * as Utility from './utility';

import { readFileSync } from 'fs';

import { toES_5 } from '@tech_query/node-toolkit';


const AMD_CJS = ['require', 'exports', 'module'];

/**
 * Key for the replacement of a module name, and value for a {@link RegExp}
 *
 * @typedef {Object} NameMap
 */

/**
 * Key for module path & value for local variable
 *
 * @typedef {Object} DependencyMap
 */


/**
 * CommonJS or AMD module
 */
export default  class Module extends EventEmitter {
    /**
     * @param {string}  name               - Path of this module
     * @param {string}  [path='.']         - Root path of the package which this module belongs to
     * @param {boolean} [includeAll=false] - Treat NPM modules as CommonJS modules
     * @param {NameMap} [nameMap]          - Map to replace some dependencies to others
     */
    constructor(name,  path = '.',  includeAll = false,  nameMap) {
        /**
         * Path of this module
         *
         * @type {string}
         */
        super().name = name.replace(/\\/g, '/');

        /**
         * Directory path of this module
         *
         * @type {string}
         */
        this.base = dirname( this.name );

        /**
         * Full name of this module file
         *
         * @type {string}
         */
        this.fileName = join(path, `${this.name}.js`);

        /**
         * @type     {Object}
         * @property {DependencyMap} compile - Compile-time dependency from AMD
         * @property {DependencyMap} runtime - Runtime dependency from CommonJS
         * @property {DependencyMap} outside - Outside dependency from NPM
         */
        this.dependency = {
            compile:  { },
            runtime:  { },
            outside:  includeAll ? null : { }
        };

        /**
         * CLI engine (Hash bang)
         *
         * @type {String}
         */
        this.CLI = '';

        /**
         * @type {NameMap}
         */
        this.nameMap = nameMap  ||  { };
    }

    /**
     * @param {string} name - Name of a module
     *
     * @return {string}
     */
    static identifierOf(name) {  return  name.replace(/\W+/g, '_');  }

    /**
     * Identifier of this module in JS code
     *
     * @type {string}
     */
    get identifier() {  return  Module.identifierOf( this.name );  }

    /**
     * Paths of all the dependency needed to be bundled
     *
     * @type {string[]}
     */
    get dependencyPath() {

        return  Object.keys( this.dependency.compile ).concat(
            Object.keys( this.dependency.runtime )
        );
    }

    /**
     * @protected
     *
     * @return {?string} Entry file path of this module in `./node_modules/`
     */
    searchNPM() {

        const list = ['getNPMFile', 'getNPMPackage', 'getNPMIndex'];

        while ( list[0] ) {

            let path = Utility[ list.shift() ]( this.name );

            if ( path )  return path;
        }
    }

    /**
     * @protected
     *
     * @return {string} Original source code of this module
     */
    load() {

        this.source = readFileSync(
            ((! this.dependency.outside)  &&  Utility.outPackage( this.name ))  ?
                this.searchNPM() : this.fileName
        ) + '';

        this.source = this.source.replace(
            /^#!.+[\r\n]+/,  engine => (this.CLI = engine.trim()) && ''
        );

        if (/^(import|export) /m.test( this.source ))
            this.source = toES_5(this.source, this.fileName, true);

        return  this.source = this.source.replace(/\r\n/g, '\n');
    }

    /**
     * @param {string} module - Module name from source code
     *
     * @emits {ReplaceEvent}
     *
     * @return {?string} New module name if `module` is matched in the name map
     */
    mapName(module) {

        for (let name in this.nameMap) {

            let result = module.replace(this.nameMap[ name ],  name);

            if (result !== module) {

                this.emit('replace', module, result);  return result;
            }
        }
    }

    /**
     * Add a depended module of this module
     *
     * @protected
     *
     * @param {string} type      - `compile` for AMD & `runtime` for CJS
     * @param {string} name      - Name of a module
     * @param {string} [varName] - Variable name of a module in another module
     *
     * @return {?String} Replaced module name
     */
    addChild(type, name, varName) {

        if ((type === 'compile')  &&  AMD_CJS.includes( name ))  return;

        name = Utility.outPackage( name )  ?  name  :  (
            './'  +  join(this.base, name).replace(/\\/g, '/')
        );

        const newName = this.mapName( name );  name = newName || name;

        if (this.dependency.outside  &&  (name[0] !== '.'))  type = 'outside';

        this.dependency[ type ][ name ] = varName  ||  Module.identifierOf( name );

        return newName;
    }

    /**
     * @protected
     *
     * @return {DependencyMap} Compile-time dependency
     */
    parseAMD() {

        this.source = this.source.replace(
            /define\((?:\s*\[([\s\S]*?)\]\s*,)?\s*function\s*\(([\s\S]*?)\)\s*\{([\s\S]+)\}\s*\);?/,
            (_, modName, varName, body) => {

                varName = varName.trim().split( /\s*,\s*/ );

                (modName || '').replace(
                    /(?:'|")(.+?)(?:'|")/g,  (_, name)  =>
                        this.addChild('compile', name, varName.shift())
                );

                return  body.replace(/^\n([\s\S]+)\n$/, '$1');
            }
        );

        return this.dependency.compile;
    }

    /**
     * @protected
     *
     * @return {DependencyMap} Runtime dependency
     */
    parseCJS() {

        this.source = this.source.replace(
            /((?:var|let|const)\s+\w+?\s*=.*?)?require\(\s*(?:'|")(.+?)(?:'|")\s*\)/mg,
            (_, assign, modName)  =>
                `${assign || ''}require('${
                    this.addChild('runtime', modName)  ||  modName
                }')`
        );

        return this.dependency.runtime;
    }

    /**
     * @return {string} Factory code of this parsed module
     */
    parse() {

        this.load();

        this.parseAMD();

        this.parseCJS();

        return  this.source = `function (${

            Object.values( this.dependency.compile ).concat( AMD_CJS ).join(', ')
        }) {${
            this.source
        }}`;
    }

    /**
     * @return {string} Factory code of this parsed module
     */
    toString() {

        return  ('source' in this)  ?  (this.source + '')  :  '';
    }
}