Home Reference Source Test Repository

src/PackageUtil.js

import fs from 'fs';

/**
 * Provides several utility methods for working with `package.json`.
 */
export default class PackageUtil
{
   /**
    * Get essential info for the given package object.
    *
    * @param {NPMPackageObject} packageObj - A loaded `package.json` object.
    *
    * @returns {NPMPackageData}
    */
   static getPackageData(packageObj = {})
   {
      let bugsURL, repoURL;

      // Sanity case to create empty object.
      if (packageObj === null || typeof packageObj === 'undefined')
      {
         packageObj = {};
      }

      // Parse repository URL.
      if (packageObj.repository)
      {
         repoURL = s_PARSE_URL(packageObj.repository.url ? packageObj.repository.url : packageObj.repository);
      }

      // Parse bugs URL.
      if (packageObj.bugs)
      {
         bugsURL = s_PARSE_URL(packageObj.bugs.url ? packageObj.bugs.url : packageObj.bugs);
      }

      /**
       * Creates NPMPackageData result.
       * @type {NPMPackageData}
       */
      const packageData =
      {
         name: packageObj.name,
         version: packageObj.version,
         description: packageObj.description,
         author: packageObj.author,
         homepage: packageObj.homepage,
         license: packageObj.license,
         main: packageObj.main,
         repository: { url: repoURL },
         bugs: { url: bugsURL }
      };

      let formattedMessage = '';

      if (packageData.name)
      {
         formattedMessage += `name: ${packageData.name}${packageData.version ? ` (${packageData.version})` : ''}`;
      }

      if (packageData.description) { formattedMessage += `\ndescription: ${packageData.description}`; }
      if (packageData.bugs.url) { formattedMessage += `\nbugs / issues: ${packageData.bugs.url}`; }
      if (packageData.repository.url) { formattedMessage += `\nrepository: ${packageData.repository.url}`; }
      if (packageData.homepage) { formattedMessage += `\nhomepage: ${packageData.homepage}`; }

      packageData.formattedMessage = formattedMessage;

      // Index info.

      return packageData;
   }

   /**
    * Attempts to load any associated `package.json` file from any NPM module detected in the first line of the
    * error trace. The logger is queried with the error generating a filtered stack trace.
    *
    * @param {Array<string>|Error} errOrTrace - A stack trace as an array of strings or error with stack trace to
    *                                           examine.
    *
    * @returns {NPMPackageData|undefined}
    */
   static getPackageDataFromError(errOrTrace)
   {
      // Covert any Error with a stack to an array of strings.
      if (errOrTrace instanceof Error && typeof errOrTrace.stack === 'string')
      {
         errOrTrace = errOrTrace.stack.split('\n');
      }

      let packageInfo;

      if (Array.isArray(errOrTrace))
      {
         // Walk through the stack trace array of strings until the first entry with a `node_modules` pattern is found
         // then attempt to parse that NPM module package.
         for (let cntr = 0; cntr < errOrTrace.length; cntr++)
         {
            // Matches full path to last NPM module, last node_module directory name
            const matches = (/^.*\((\/.*(\/node_modules\/(.*?)\/))/g).exec(`${errOrTrace[cntr]}`);

            const modulePath = matches !== null && matches.length >= 1 ? matches[1] : void 0;

            if (typeof modulePath === 'string')
            {
               try
               {
                  const packageObj = JSON.parse(fs.readFileSync(`${modulePath}package.json`, { encode: 'utf8' }));

                  packageInfo = PackageUtil.getPackageData(packageObj);

                  break;
               }
               catch (packageErr)
               { /* nop */ }
            }
         }
      }

      return packageInfo;
   }
}

/**
 * Creates several general utility methods bound to the eventbus.
 *
 * @param {PluginEvent}    ev - An event proxy for the main eventbus.
 */
export function onPluginLoad(ev)
{
   const eventbus = ev.eventbus;

   eventbus.on('typhonjs:util:package:get:data', PackageUtil.getPackageData, PackageUtil);
   eventbus.on('typhonjs:util:package:get:data:from:error', PackageUtil.getPackageDataFromError, PackageUtil);
}

// Module private ---------------------------------------------------------------------------------------------------

/**
 * Parses an URL for Github SCM link.
 *
 * @param {string}   parseURL - URL to parse.
 *
 * @returns {string}
 * @ignore
 */
const s_PARSE_URL = (parseURL) =>
{
   let url;

   if (typeof parseURL === 'string')
   {
      if (parseURL.indexOf('git@github.com:') === 0)
      {
         // url: git@github.com:foo/bar.git
         const matched = parseURL.match(/^git@github\.com:(.*)\.git$/);

         if (matched && matched[1])
         {
            url = `https://github.com/${matched[1]}`;
         }
      }
      else if (parseURL.match(/^[\w\d\-_]+\/[\w\d\-_]+$/))
      {
         // url: foo/bar
         url = `https://github.com/${parseURL}`;
      }
      else if (parseURL.match(/^git\+https:\/\/github.com\/.*\.git$/))
      {
         // git+https://github.com/foo/bar.git
         const matched = parseURL.match(/^git\+(https:\/\/github.com\/.*)\.git$/);

         url = matched[1];
      }
      else if (parseURL.match(/(https?:\/\/.*$)/))
      {
         // other url
         const matched = parseURL.match(/(https?:\/\/.*$)/);

         url = matched[1];
      }
      else
      {
         url = '';
      }
   }

   return url;
};