Home Reference Source Test Repository

src/protocol-hook.js

import './babel-maybefill';
import url from 'url';
import fs from 'fs';
import mime from 'mime-types';

import CompilerHost from './compiler-host';

const magicWords = "__magic__file__to__help__electron__compile.js";
const magicGlobalForRootCacheDir = '__electron_compile_root_cache_dir';
const magicGlobalForAppRootDir = '__electron_compile_app_root_dir';

const d = require('debug')('electron-compile:protocol-hook');

let protocol = null;

/**
 * Adds our script header to the top of all HTML files
 *  
 * @private
 */ 
export function rigHtmlDocumentToInitializeElectronCompile(doc) {
  let lines = doc.split("\n");
  let replacement = `<head><script src="${magicWords}"></script>`;
  let replacedHead = false;

  for (let i=0; i < lines.length; i++) {
    if (!lines[i].match(/<head>/i)) continue;

    lines[i] = (lines[i]).replace(/<head>/i, replacement);
    replacedHead = true;
    break;
  }

  if (!replacedHead) {
    replacement = `<html$1><head><script src="${magicWords}"></script></head>`;
    for (let i=0; i < lines.length; i++) {
      if (!lines[i].match(/<html/i)) continue;

      lines[i] = (lines[i]).replace(/<html([^>]+)>/i, replacement);
      break;
    }
  }

  return lines.join("\n");
}

function requestFileJob(filePath, finish) {
  fs.readFile(filePath, (err, buf) => {
    if (err) { 
      if (err.errno === 34) {
        finish(-6); // net::ERR_FILE_NOT_FOUND
        return;
      } else {
        finish(-2); // net::FAILED
        return;
      }
    }
    
    finish({
      data: buf,
      mimeType: mime.lookup(filePath) || 'text/plain'
    });
  });
}

let rendererInitialized = false;

/**
 * Called by our rigged script file at the top of every HTML file to set up
 * the same compilers as the browser process that created us
 *  
 * @private
 */ 
export function initializeRendererProcess(readOnlyMode) {
  if (rendererInitialized) return;
  
  // NB: If we don't do this, we'll get a renderer crash if you enable debug
  require('debug/browser');
  
  let rootCacheDir = require('remote').getGlobal(magicGlobalForRootCacheDir);
  let appRoot = require('remote').getGlobal(magicGlobalForAppRootDir);
  let compilerHost = null;
  
  // NB: This has to be synchronous because we need to block HTML parsing
  // until we're set up
  if (readOnlyMode) {
    d(`Setting up electron-compile in precompiled mode with cache dir: ${rootCacheDir}`);
    compilerHost = CompilerHost.createReadonlyFromConfigurationSync(rootCacheDir, appRoot);
  } else {
    d(`Setting up electron-compile in development mode with cache dir: ${rootCacheDir}`);
    const { createCompilers } = require('./config-parser');
    const compilersByMimeType = createCompilers();
    
    compilerHost = CompilerHost.createFromConfigurationSync(rootCacheDir, appRoot, compilersByMimeType);
  }
  
  require('./x-require');
  require('./require-hook').default(compilerHost);
  rendererInitialized = true;
}


/**
 * Initializes the protocol hook on file: that allows us to intercept files 
 * loaded by Chromium and rewrite them. This method along with 
 * {@link registerRequireExtension} are the top-level methods that electron-compile
 * actually uses to intercept code that Electron loads.
 *  
 * @param  {CompilerHost} compilerHost  The compiler host to use for compilation.
 */ 
export function initializeProtocolHook(compilerHost) {
  protocol = protocol || require('protocol');
  
  global[magicGlobalForRootCacheDir] = compilerHost.rootCacheDir;
  global[magicGlobalForAppRootDir] = compilerHost.appRoot;
  
  const electronCompileSetupCode = `if (window.require) require('electron-compile/lib/protocol-hook').initializeRendererProcess(${compilerHost.readOnlyMode});`;

  protocol.interceptBufferProtocol('file', async function(request, finish) {
    let uri = url.parse(request.url);

    d(`Intercepting url ${request.url}`);
    if (request.url.indexOf(magicWords) > -1) {
      finish({
        mimeType: 'application/javascript',
        data: new Buffer(electronCompileSetupCode, 'utf8')
      });
      
      return;
    }

    // This is a protocol-relative URL that has gone pear-shaped in Electron,
    // let's rewrite it
    if (uri.host && uri.host.length > 1) {
      //let newUri = request.url.replace(/^file:/, "https:");
      // TODO: Jump off this bridge later
      d(`TODO: Found bogus protocol-relative URL, can't fix it up!!`);
      finish(-2);
    }

    let filePath = decodeURIComponent(uri.pathname);

    // NB: pathname has a leading '/' on Win32 for some reason
    if (process.platform === 'win32') {
      filePath = filePath.slice(1);
    }

    // NB: Special-case files coming from atom.asar or node_modules
    if (filePath.match(/[\/\\]atom.asar/) || filePath.match(/[\/\\]node_modules/)) {
      requestFileJob(filePath, finish);
      return;
    }
    
    try {
      let result = await compilerHost.compile(filePath);
      
      if (filePath.match(/\.html?$/i)) {
        result.code = rigHtmlDocumentToInitializeElectronCompile(result.code);
      }
      
      if (result.binaryData || result.code instanceof Buffer) {
        finish({ data: result.binaryData || result.code, mimeType: result.mimeType });
        return;
      } else {
        finish({ data: new Buffer(result.code), mimeType: result.mimeType });
        return;
      }
    } catch (e) {
      let err = `Failed to compile ${filePath}: ${e.message}\n${e.stack}`;
      d(err);
      
      if (e.errno === 34 /*ENOENT*/) {
        finish(-6); // net::ERR_FILE_NOT_FOUND
        return;
      }

      finish({ mimeType: 'text/plain', data: new Buffer(err) });
      return;
    }
  });
}