Home Reference Source Test Repository

src/config-parser.js

import _ from 'lodash';
import fs from 'fs';
import path from 'path';
import mkdirp from 'mkdirp';
import {pfs} from './promise';

import FileChangedCache from './file-change-cache';
import CompilerHost from './compiler-host';
import { initializeProtocolHook } from './protocol-hook';
import registerRequireExtension from './require-hook';

const d = require('debug')('electron-compile:config-parser');

// NB: We intentionally delay-load this so that in production, you can create
// cache-only versions of these compilers
let allCompilerClasses = null;

function statSyncNoException(fsPath) {
  if ('statSyncNoException' in fs) {
    return fs.statSyncNoException(fsPath);
  }
  
  try {
    return fs.statSync(fsPath);
  } catch (e) {
    return null;
  }
}


/**
 * Initialize the global hooks (protocol hook for file:, node.js hook) 
 * independent of initializing the compiler. This method is usually called by
 * init instead of directly
 * 
 * @param {CompilerHost} compilerHost  The compiler host to use.
 *  
 */ 
export function initializeGlobalHooks(compilerHost) {
  const { app } = require('electron');
  
  registerRequireExtension(compilerHost);

  let protoify = function() { initializeProtocolHook(compilerHost); };
  if (app.isReady()) {
    protoify();
  } else {
    app.on('ready', protoify);
  }
}


/**
 * Initialize electron-compile and set it up, either for development or 
 * production use. This is almost always the only method you need to use in order
 * to use electron-compile.
 *  
 * @param  {string} appRoot  The top-level directory for your application (i.e.
 *                           the one which has your package.json).
 *
 * @param  {string} mainModule  The module to require in, relative to the module
 *                              calling init, that will start your app. Write this 
 *                              as if you were writing a require call from here.
 *
 * @param  {bool} productionMode   If explicitly True/False, will set read-only
 *                                 mode to be disabled/enabled. If not, we'll
 *                                 guess based on the presence of a production
 *                                 cache.
 */ 
export function init(appRoot, mainModule, productionMode = null) {
  let compilerHost = null;
  let cacheDir = path.join(appRoot, '.cache');
  
  if (productionMode === null) {
    productionMode = !!statSyncNoException(cacheDir);
  }
  
  if (productionMode) {
    // In read-only mode, we'll assume that everything is in `appRoot/.cache`
    compilerHost = CompilerHost.createReadonlyFromConfigurationSync(cacheDir, appRoot);
  } else {
    compilerHost = createCompilerHostFromProjectRootSync(appRoot);
  }
  
  initializeGlobalHooks(compilerHost);
  require.main.require(mainModule);
}


/**
 * Creates a {@link CompilerHost} with the given information. This method is
 * usually called by {@link createCompilerHostFromProjectRoot}.
 *  
 * @private
 */ 
export function createCompilerHostFromConfiguration(info) {
  let compilers = createCompilers();
  let rootCacheDir = info.rootCacheDir || calculateDefaultCompileCacheDirectory();
  
  d(`Creating CompilerHost: ${JSON.stringify(info)}, rootCacheDir = ${rootCacheDir}`);
  let fileChangeCache = new FileChangedCache(info.appRoot);
  let ret = new CompilerHost(rootCacheDir, compilers, fileChangeCache, false, compilers['text/plain']);
  
  _.each(Object.keys(info.options || {}), (x) => {
    let opts = info.options[x];
    if (!(x in compilers)) {
      throw new Error(`Found compiler settings for missing compiler: ${x}`);
    }
    
    d(`Setting options for ${x}: ${JSON.stringify(opts)}`);
    compilers[x].compilerOptions = opts;
  });
  
  // NB: It's super important that we guarantee that the configuration is saved
  // out, because we'll need to re-read it in the renderer process
  d(`Created compiler host with options: ${JSON.stringify(info)}`);
  ret.saveConfigurationSync();
  return ret;
}

/**
 * Creates a compiler host from a .babelrc file. This method is usually called
 * from {@link createCompilerHostFromProjectRoot} instead of used directly.
 *  
 * @param  {string} file  The path to a .babelrc file
 *
 * @param  {string} rootCacheDir (optional)  The directory to use as a cache.
 *
 * @return {Promise<CompilerHost>}  A set-up compiler host
 */ 
export async function createCompilerHostFromBabelRc(file, rootCacheDir=null) {
  let info = JSON.parse(await pfs.readFile(file, 'utf8'));
  
  // package.json
  if ('babel' in info) {
    info = info.babel;
  }
  
  if ('env' in info) {
    let ourEnv = process.env.BABEL_ENV || process.env.NODE_ENV || 'development';
    info = info.env[ourEnv];
  }
  
  // Are we still package.json (i.e. is there no babel info whatsoever?)
  if ('name' in info && 'version' in info) {
    return createCompilerHostFromConfiguration({
      appRoot: path.dirname(file),
      options: getDefaultConfiguration(),
      rootCacheDir
    });
  }
  
  return createCompilerHostFromConfiguration({
    appRoot: path.dirname(file),
    options: {
      'application/javascript': info
    },
    rootCacheDir
  });
}


/**
 * Creates a compiler host from a .compilerc file. This method is usually called
 * from {@link createCompilerHostFromProjectRoot} instead of used directly.
 *  
 * @param  {string} file  The path to a .compilerc file
 *
 * @param  {string} rootCacheDir (optional)  The directory to use as a cache.
 *
 * @return {Promise<CompilerHost>}  A set-up compiler host
 */ 
export async function createCompilerHostFromConfigFile(file, rootCacheDir=null) {
  let info = JSON.parse(await pfs.readFile(file, 'utf8'));
  
  if ('env' in info) {
    let ourEnv = process.env.ELECTRON_COMPILE_ENV || process.env.NODE_ENV || 'development';
    info = info.env[ourEnv];
  }
  
  return createCompilerHostFromConfiguration({
    appRoot: path.dirname(file),
    options: info,
    rootCacheDir
  });
}


/**
 * Creates a configured {@link CompilerHost} instance from the project root 
 * directory. This method first searches for a .compilerc, then falls back to the
 * default locations for Babel configuration info. If neither are found, defaults
 * to standard settings
 *  
 * @param  {string} rootDir  The root application directory (i.e. the directory
 *                           that has the app's package.json)
 *
 * @param  {string} rootCacheDir (optional)  The directory to use as a cache.
 *
 * @return {Promise<CompilerHost>}  A set-up compiler host
 */ 
export async function createCompilerHostFromProjectRoot(rootDir, rootCacheDir=null) {
  let compilerc = path.join(rootDir, '.compilerc');
  if (statSyncNoException(compilerc)) {
    d(`Found a .compilerc at ${compilerc}, using it`);
    return await createCompilerHostFromConfigFile(compilerc, rootCacheDir);
  }
  
  let babelrc = path.join(rootDir, '.babelrc');
  if (statSyncNoException(babelrc)) {
    d(`Found a .babelrc at ${babelrc}, using it`);
    return await createCompilerHostFromBabelRc(babelrc, rootCacheDir);
  }
    
  d(`Using package.json or default parameters at ${rootDir}`);
  return await createCompilerHostFromBabelRc(path.join(rootDir, 'package.json'), rootCacheDir);
}

export function createCompilerHostFromBabelRcSync(file, rootCacheDir=null) {
  let info = JSON.parse(fs.readFileSync(file, 'utf8'));
  
  // package.json
  if ('babel' in info) {
    info = info.babel;
  }
  
  if ('env' in info) {
    let ourEnv = process.env.BABEL_ENV || process.env.NODE_ENV || 'development';
    info = info.env[ourEnv];
  }
  
  // Are we still package.json (i.e. is there no babel info whatsoever?)
  if ('name' in info && 'version' in info) {
    return createCompilerHostFromConfiguration({
      appRoot: path.dirname(file),
      options: getDefaultConfiguration(),
      rootCacheDir
    });
  }
  
  return createCompilerHostFromConfiguration({
    appRoot: path.dirname(file),
    options: {
      'application/javascript': info
    },
    rootCacheDir
  });
}

export function createCompilerHostFromConfigFileSync(file, rootCacheDir=null) {
  let info = JSON.parse(fs.readFileSync(file, 'utf8'));
  
  if ('env' in info) {
    let ourEnv = process.env.ELECTRON_COMPILE_ENV || process.env.NODE_ENV || 'development';
    info = info.env[ourEnv];
  }
  
  return createCompilerHostFromConfiguration({
    appRoot: path.dirname(file),
    options: info,
    rootCacheDir
  });
}

export function createCompilerHostFromProjectRootSync(rootDir, rootCacheDir=null) {
  let compilerc = path.join(rootDir, '.compilerc');
  if (statSyncNoException(compilerc)) {
    d(`Found a .compilerc at ${compilerc}, using it`);
    return createCompilerHostFromConfigFileSync(compilerc, rootCacheDir);
  }
  
  let babelrc = path.join(rootDir, '.babelrc');
  if (statSyncNoException(babelrc)) {
    d(`Found a .babelrc at ${babelrc}, using it`);
    return createCompilerHostFromBabelRcSync(babelrc, rootCacheDir);
  }
    
  d(`Using package.json or default parameters at ${rootDir}`);
  return createCompilerHostFromBabelRcSync(path.join(rootDir, 'package.json'), rootCacheDir);
}

/**
 * Returns what electron-compile would use as a default rootCacheDir. Usually only
 * used for debugging purposes
 *  
 * @return {string}  A path that may or may not exist where electron-compile would
 *                   set up a development mode cache.
 */ 
export function calculateDefaultCompileCacheDirectory() {
  let tmpDir = process.env.TEMP || process.env.TMPDIR || '/tmp';
  let hash = require('crypto').createHash('md5').update(process.execPath).digest('hex');

  let cacheDir = path.join(tmpDir, `compileCache_${hash}`);
  mkdirp.sync(cacheDir);
  
  d(`Using default cache directory: ${cacheDir}`);
  return cacheDir;
}


/**
 * Returns the default .configrc if no configuration information can be found.
 *  
 * @return {Object}  A list of default config settings for electron-compiler.
 */ 
export function getDefaultConfiguration() {
  return {
    'application/javascript': {
      "presets": ["stage-0", "es2015"],
      "sourceMaps": "inline"
    }
  };
}

/**
 * Allows you to create new instances of all compilers that are supported by 
 * electron-compile and use them directly. Currently supports Babel, CoffeeScript, 
 * TypeScript, LESS, and Jade.
 *  
 * @return {Object}  An Object whose Keys are MIME types, and whose values 
 * are instances of @{link CompilerBase}.
 */ 
export function createCompilers() {
  if (!allCompilerClasses) {
    // First we want to see if electron-compilers itself has been installed with
    // devDependencies. If that's not the case, check to see if
    // electron-compilers is installed as a peer dependency (probably as a
    // devDependency of the root project).
    const locations = ['electron-compilers', '../../electron-compilers'];

    for (let location of locations) {
      try {
        allCompilerClasses = require(location);
      } catch (e) {
        // Yolo
      }
    }

    if (!allCompilerClasses) {
      throw new Error("Electron compilers not found but were requested to be loaded");
    }
  }

  // NB: Note that this code is carefully set up so that InlineHtmlCompiler 
  // (i.e. classes with `createFromCompilers`) initially get an empty object,
  // but will have a reference to the final result of what we return, which
  // resolves the circular dependency we'd otherwise have here.
  let ret = {};
  let instantiatedClasses = _.map(allCompilerClasses, (Klass) => {
    if ('createFromCompilers' in Klass) {
      return Klass.createFromCompilers(ret);
    } else {
      return new Klass();
    }
  });

  _.reduce(instantiatedClasses, (acc,x) => {
    let Klass = Object.getPrototypeOf(x).constructor;

    for (let type of Klass.getInputMimeTypes()) { acc[type] = x; }
    return acc;
  }, ret);
  
  return ret;
}