<commit_before>import electron from 'electron';
import {forEach, values, some} from 'lodash';
import {Observable, Disposable, CompositeDisposable, SerialDisposable} from 'rx-lite';
import {getSubstitutionRegExp, getSmartQuotesRegExp, getSmartDashesRegExp,
  scrubInputString, formatReplacement} from './regular-expressions';
import {isUndoRedoEvent} from './undo-redo-event';

const packageName = 'electron-text-substitutions';
const d = process.type === 'browser' ?
  require('debug')(packageName) :
  console.log.bind(window.console);

const registerForPreferenceChangedIpcMessage = `${packageName}-register-renderer`;
const unregisterForPreferenceChangedIpcMessage = `${packageName}-unregister-renderer`;
const preferenceChangedIpcMessage = `${packageName}-preference-changed`;

const userDefaultsTextSubstitutionsKey = 'NSUserDictionaryReplacementItems';
const userDefaultsSmartQuotesKey = 'NSAutomaticQuoteSubstitutionEnabled';
const userDefaultsSmartDashesKey = 'NSAutomaticDashSubstitutionEnabled';

const textPreferenceChangedKeys = [
  'IMKTextReplacementDidChangeNotification',
  'NSAutomaticQuoteSubstitutionEnabledChanged',
  'NSAutomaticDashSubstitutionEnabledChanged'
];

let ipcMain, ipcRenderer, systemPreferences;
let registeredWebContents = {};

/**
 * Adds an `input` event listener to the given element (an <input> or
 * <textarea>) that will substitute text based on the user's replacements in
 * `NSUserDefaults`.
 *
 * In addition, this method will listen for changes to `NSUserDefaults` and
 * update accordingly.
 *
 * @param  {EventTarget} element          The DOM node to listen to; should fire the `input` event
 * @param  {Object} preferenceOverrides   Used to override text preferences in testing
 *
 * @return {Disposable}                   A `Disposable` that will clean up everything this method did
 */
export default function performTextSubstitution(element, preferenceOverrides = null) {
  if (!element || !element.addEventListener) throw new Error(`Element is null or not an EventTarget`);
  if (!process || !process.type === 'renderer') throw new Error(`Not in an Electron renderer context`);
  if (process.platform !== 'darwin') throw new Error(`Only supported on OS X`);

  ipcRenderer = ipcRenderer || electron.ipcRenderer;
  systemPreferences = systemPreferences || electron.remote.systemPreferences;

  if (!systemPreferences || !systemPreferences.getUserDefault) {
    throw new Error(`Electron ${process.versions.electron} is not supported`);
  }

  ipcRenderer.send(registerForPreferenceChangedIpcMessage);

  window.addEventListener('beforeunload', () => {
    d(`Window unloading, unregister any listeners`);
    ipcRenderer.send(unregisterForPreferenceChangedIpcMessage);
  });

  let currentAttach = assignDisposableToListener(element, preferenceOverrides);

  ipcRenderer.on(preferenceChangedIpcMessage, () => {
    d(`User modified text preferences, reattaching listener`);
    assignDisposableToListener(element, preferenceOverrides, currentAttach);
  });

  return currentAttach;
}

/**
 * Subscribes to text preference changed notifications and notifies listeners
 * in renderer processes. This method must be called from the main process, and
 * should be called before any renderer process calls `performTextSubstitution`.
 *
 * @return {Disposable}  A `Disposable` that will clean up everything this method did
 */
export function listenForPreferenceChanges() {
  if (!process || !process.type === 'browser') throw new Error(`Not in an Electron browser context`);
  if (process.platform !== 'darwin') throw new Error(`Only supported on OS X`);

  ipcMain = ipcMain || electron.ipcMain;
  systemPreferences = systemPreferences || electron.systemPreferences;

  ipcMain.on(registerForPreferenceChangedIpcMessage, ({sender}) => {
    let id = sender.getId();
    d(`Registering webContents ${id} for preference changes`);
    registeredWebContents[id] = { id, sender };
  });

  ipcMain.on(unregisterForPreferenceChangedIpcMessage, ({sender}) => {
    d(`Unregistering webContents ${sender.getId()}`);
    delete registeredWebContents[sender.getId()];
  });

  let notificationDisp = Observable.fromArray(textPreferenceChangedKeys)
    .flatMap((key) => observableForPreferenceNotification(key))
    .debounce(1000)
    .subscribe(notifyAllListeners);

  return new CompositeDisposable(
    notificationDisp,
    new Disposable(() => ipcMain.removeAllListeners(registerForPreferenceChangedIpcMessage)),
    new Disposable(() => ipcMain.removeAllListeners(unregisterForPreferenceChangedIpcMessage))
  );
}

/**
 * Creates an Observable that will `onNext` when the given key in
 * `NSUserDefaults` changes.
 *
 * @param  {String} preferenceChangedKey  The key to listen for
 * @return {Disposable}                   A Disposable that will unsubscribe the listener
 */
function observableForPreferenceNotification(preferenceChangedKey) {
  return Observable.create((subj) => {
    let subscriberId = systemPreferences.subscribeNotification(preferenceChangedKey, () => {
      d(`Got a ${preferenceChangedKey}`);
      subj.onNext(preferenceChangedKey);
    });

    return new Disposable(() => systemPreferences.unsubscribeNotification(subscriberId));
  });
}

/**
 * Sends an IPC message to each `WebContents` that is doing text substitution,
 * unless it has been destroyed, in which case remove it from our list.
 */
function notifyAllListeners() {
  forEach(values(registeredWebContents), ({id, sender}) => {
    if (sender.isDestroyed() || sender.isCrashed()) {
      d(`WebContents ${id} is gone, removing it`);
      delete registeredWebContents[id];
    } else {
      sender.send(preferenceChangedIpcMessage);
    }
  });
}

function assignDisposableToListener(element, preferenceOverrides, currentAttach = null) {
  let textPreferences = preferenceOverrides || readSystemTextPreferences();
  let replacementItems = getReplacementItems(textPreferences);

  currentAttach = currentAttach || new SerialDisposable();
  currentAttach.setDisposable(addInputListener(element, replacementItems));
  return currentAttach;
}

function readSystemTextPreferences() {
  return {
    substitutions: systemPreferences.getUserDefault(userDefaultsTextSubstitutionsKey, 'array') || [],
    useSmartQuotes: systemPreferences.getUserDefault(userDefaultsSmartQuotesKey, 'boolean'),
    useSmartDashes: systemPreferences.getUserDefault(userDefaultsSmartDashesKey, 'boolean')
  };
}

/**
 * @typedef {Object} TextSubstitution
 * @property {String} replace The text to replace
 * @property {String} with    The replacement text
 * @property {Bool}   on      True if this substitution is enabled
 */

 /**
  * Creates a regular expression for each text substitution entry, in addition
  * to expressions for smart quotes and dashes (if they're enabled).
  *
  * @param  {Array<TextSubstitution>} {substitutions  An array of text substitution entries
  * @param  {Bool} useSmartQuotes                     True if smart quotes is on
  * @param  {Bool} useSmartDashes}                    True if smart dashes is on
  * @return {Array<ReplacementItem>}                  An array of replacement items
  */
function getReplacementItems({substitutions, useSmartQuotes, useSmartDashes}) {
  d(`Smart quotes are ${useSmartQuotes ? 'on' : 'off'}`);
  d(`Smart dashes are ${useSmartDashes ? 'on' : 'off'}`);

  let additionalReplacements = [
    ...(useSmartQuotes ? getSmartQuotesRegExp() : []),
    ...(useSmartDashes ? getSmartDashesRegExp() : [])
  ];

  d(`Found ${substitutions.length} substitutions in NSUserDictionaryReplacementItems`);

  // NB: Run each replacement string through our smart quotes & dashes regex,
  // so that an input event doesn't cause chained substitutions. Also sort
  // replacements by length, to handle nested substitutions.
  let userDictionaryReplacements = substitutions
    .filter((substitution) => substitution.on !== false)
    .sort((a, b) => b.replace.length - a.replace.length)
    .map((substitution) => getSubstitutionRegExp(substitution.replace,
      scrubInputString(substitution.with, additionalReplacements)));

  return [
    ...userDictionaryReplacements,
    ...additionalReplacements
  ];
}

/**
 * Subscribes to the `input` event and performs text substitution.
 *
 * @param  {EventTarget} element                      The DOM node to listen to
 * @param  {Array<ReplacementItem>} replacementItems  An array of replacement items
 * @return {Disposable}                               A `Disposable` that will remove the listener
 */
function addInputListener(element, replacementItems) {
  let ignoreEvent = false;

  let inputListener = () => {
    if (ignoreEvent) return;
    ignoreEvent = true;

    for (let {regExp, replacement} of replacementItems) {
      // Rather than search the entire input, we're just going to check the word
      // immediately before the caret (along with its surrounding whitespace).
      // This is to avoid substitutions after, say, a paste or an undo.
      let searchStartIndex = lastIndexOfWhitespace(element.value, element.selectionEnd);
      let lastWordBlock = element.value.substring(searchStartIndex, element.selectionEnd);
      let match = lastWordBlock.match(regExp);

      if (match && match.length === 3) {
        d(`Got a match of length ${match[0].length} at index ${match.index}: ${JSON.stringify(match)}`);

        if (some(replacementItems, (item) => item.match === match[0])) {
          d(`The match is a prefix of another replacement item (${match[0]}), skip it`);
          continue;
        }

        let selection = {
          startIndex: searchStartIndex + match.index,
          endIndex: searchStartIndex + match.index + match[0].length
        };

        replaceText(element, selection, formatReplacement(match, replacement));
      }
    }

    ignoreEvent = false;
  };

  let keyDownListener = (e) => {
    if (isUndoRedoEvent(e)) {
      d(`Undo or redo text from ${e.target.value}`);
      ignoreEvent = true;
    }
  };

  let pasteListener = () => {
    ignoreEvent = true;
  };

  let keyUpListener = () => {
    ignoreEvent = false;
  };

  element.addEventListener('keydown', keyDownListener, true);
  element.addEventListener('paste', pasteListener, true);
  element.addEventListener('keyup', keyUpListener, true);
  element.addEventListener('input', inputListener);

  d(`Added input listener to ${element.id} matching against ${replacementItems.length} replacements`);

  return new Disposable(() => {
    element.removeEventListener('keydown', keyDownListener);
    element.removeEventListener('paste', pasteListener);
    element.removeEventListener('keyup', keyUpListener);
    element.removeEventListener('input', inputListener);

    d(`Removed input listener from ${element.id}`);
  });
}

function lastIndexOfWhitespace(value, fromIndex) {
  let lastIndex = 0;
  let whitespace = /\s/g;
  let textToCaret = value.substring(0, fromIndex).trimRight();

  while (whitespace.exec(textToCaret) !== null) {
    lastIndex = whitespace.lastIndex;
  }
  return lastIndex;
}

/**
 * Performs the actual text replacement using `dispatchEvent`. We use events to
 * preserve the user's cursor index and make the substitution undoable.
 *
 * @param  {EventTarget} element  The DOM node where text is being substituted
 * @param  {Number} {startIndex   Start index of the text to replace
 * @param  {Number} endIndex}     End index of the text to replace
 * @param  {String} newText       The text being inserted
 */
function replaceText(element, {startIndex, endIndex}, newText) {
  let textEvent = document.createEvent('TextEvent');
  textEvent.initTextEvent('textInput', true, true, null, newText);

  element.selectionStart = startIndex;
  element.selectionEnd = endIndex;

  d(`Replacing ${element.value.substring(startIndex, endIndex)} with ${newText}`);
  element.dispatchEvent(textEvent);
}
<commit_msg>Log this so we can prove where time is being spent.<commit_after>import electron from 'electron';
import {forEach, values, some} from 'lodash';
import {Observable, Disposable, CompositeDisposable, SerialDisposable} from 'rx-lite';
import {getSubstitutionRegExp, getSmartQuotesRegExp, getSmartDashesRegExp,
  scrubInputString, formatReplacement} from './regular-expressions';
import {isUndoRedoEvent} from './undo-redo-event';

const packageName = 'electron-text-substitutions';
const d = process.type === 'browser' ?
  require('debug')(packageName) :
  console.log.bind(window.console);

const registerForPreferenceChangedIpcMessage = `${packageName}-register-renderer`;
const unregisterForPreferenceChangedIpcMessage = `${packageName}-unregister-renderer`;
const preferenceChangedIpcMessage = `${packageName}-preference-changed`;

const userDefaultsTextSubstitutionsKey = 'NSUserDictionaryReplacementItems';
const userDefaultsSmartQuotesKey = 'NSAutomaticQuoteSubstitutionEnabled';
const userDefaultsSmartDashesKey = 'NSAutomaticDashSubstitutionEnabled';

const textPreferenceChangedKeys = [
  'IMKTextReplacementDidChangeNotification',
  'NSAutomaticQuoteSubstitutionEnabledChanged',
  'NSAutomaticDashSubstitutionEnabledChanged'
];

let ipcMain, ipcRenderer, systemPreferences;
let registeredWebContents = {};

/**
 * Adds an `input` event listener to the given element (an <input> or
 * <textarea>) that will substitute text based on the user's replacements in
 * `NSUserDefaults`.
 *
 * In addition, this method will listen for changes to `NSUserDefaults` and
 * update accordingly.
 *
 * @param  {EventTarget} element          The DOM node to listen to; should fire the `input` event
 * @param  {Object} preferenceOverrides   Used to override text preferences in testing
 *
 * @return {Disposable}                   A `Disposable` that will clean up everything this method did
 */
export default function performTextSubstitution(element, preferenceOverrides = null) {
  if (!element || !element.addEventListener) throw new Error(`Element is null or not an EventTarget`);
  if (!process || !process.type === 'renderer') throw new Error(`Not in an Electron renderer context`);
  if (process.platform !== 'darwin') throw new Error(`Only supported on OS X`);

  ipcRenderer = ipcRenderer || electron.ipcRenderer;
  systemPreferences = systemPreferences || electron.remote.systemPreferences;

  if (!systemPreferences || !systemPreferences.getUserDefault) {
    throw new Error(`Electron ${process.versions.electron} is not supported`);
  }

  ipcRenderer.send(registerForPreferenceChangedIpcMessage);

  window.addEventListener('beforeunload', () => {
    d(`Window unloading, unregister any listeners`);
    ipcRenderer.send(unregisterForPreferenceChangedIpcMessage);
  });

  let currentAttach = assignDisposableToListener(element, preferenceOverrides);

  ipcRenderer.on(preferenceChangedIpcMessage, () => {
    d(`User modified text preferences, reattaching listener`);
    assignDisposableToListener(element, preferenceOverrides, currentAttach);
  });

  return currentAttach;
}

/**
 * Subscribes to text preference changed notifications and notifies listeners
 * in renderer processes. This method must be called from the main process, and
 * should be called before any renderer process calls `performTextSubstitution`.
 *
 * @return {Disposable}  A `Disposable` that will clean up everything this method did
 */
export function listenForPreferenceChanges() {
  if (!process || !process.type === 'browser') throw new Error(`Not in an Electron browser context`);
  if (process.platform !== 'darwin') throw new Error(`Only supported on OS X`);

  ipcMain = ipcMain || electron.ipcMain;
  systemPreferences = systemPreferences || electron.systemPreferences;

  ipcMain.on(registerForPreferenceChangedIpcMessage, ({sender}) => {
    let id = sender.getId();
    d(`Registering webContents ${id} for preference changes`);
    registeredWebContents[id] = { id, sender };
  });

  ipcMain.on(unregisterForPreferenceChangedIpcMessage, ({sender}) => {
    d(`Unregistering webContents ${sender.getId()}`);
    delete registeredWebContents[sender.getId()];
  });

  let notificationDisp = Observable.fromArray(textPreferenceChangedKeys)
    .flatMap((key) => observableForPreferenceNotification(key))
    .debounce(1000)
    .subscribe(notifyAllListeners);

  return new CompositeDisposable(
    notificationDisp,
    new Disposable(() => ipcMain.removeAllListeners(registerForPreferenceChangedIpcMessage)),
    new Disposable(() => ipcMain.removeAllListeners(unregisterForPreferenceChangedIpcMessage))
  );
}

/**
 * Creates an Observable that will `onNext` when the given key in
 * `NSUserDefaults` changes.
 *
 * @param  {String} preferenceChangedKey  The key to listen for
 * @return {Disposable}                   A Disposable that will unsubscribe the listener
 */
function observableForPreferenceNotification(preferenceChangedKey) {
  return Observable.create((subj) => {
    let subscriberId = systemPreferences.subscribeNotification(preferenceChangedKey, () => {
      d(`Got a ${preferenceChangedKey}`);
      subj.onNext(preferenceChangedKey);
    });

    return new Disposable(() => systemPreferences.unsubscribeNotification(subscriberId));
  });
}

/**
 * Sends an IPC message to each `WebContents` that is doing text substitution,
 * unless it has been destroyed, in which case remove it from our list.
 */
function notifyAllListeners() {
  forEach(values(registeredWebContents), ({id, sender}) => {
    if (sender.isDestroyed() || sender.isCrashed()) {
      d(`WebContents ${id} is gone, removing it`);
      delete registeredWebContents[id];
    } else {
      sender.send(preferenceChangedIpcMessage);
    }
  });
}

function assignDisposableToListener(element, preferenceOverrides, currentAttach = null) {
  let textPreferences = preferenceOverrides || readSystemTextPreferences();
  let replacementItems = getReplacementItems(textPreferences);

  currentAttach = currentAttach || new SerialDisposable();
  currentAttach.setDisposable(addInputListener(element, replacementItems));
  return currentAttach;
}

function readSystemTextPreferences() {
  return {
    substitutions: systemPreferences.getUserDefault(userDefaultsTextSubstitutionsKey, 'array') || [],
    useSmartQuotes: systemPreferences.getUserDefault(userDefaultsSmartQuotesKey, 'boolean'),
    useSmartDashes: systemPreferences.getUserDefault(userDefaultsSmartDashesKey, 'boolean')
  };
}

/**
 * @typedef {Object} TextSubstitution
 * @property {String} replace The text to replace
 * @property {String} with    The replacement text
 * @property {Bool}   on      True if this substitution is enabled
 */

 /**
  * Creates a regular expression for each text substitution entry, in addition
  * to expressions for smart quotes and dashes (if they're enabled).
  *
  * @param  {Array<TextSubstitution>} {substitutions  An array of text substitution entries
  * @param  {Bool} useSmartQuotes                     True if smart quotes is on
  * @param  {Bool} useSmartDashes}                    True if smart dashes is on
  * @return {Array<ReplacementItem>}                  An array of replacement items
  */
function getReplacementItems({substitutions, useSmartQuotes, useSmartDashes}) {
  d(`Smart quotes are ${useSmartQuotes ? 'on' : 'off'}`);
  d(`Smart dashes are ${useSmartDashes ? 'on' : 'off'}`);

  let additionalReplacements = [
    ...(useSmartQuotes ? getSmartQuotesRegExp() : []),
    ...(useSmartDashes ? getSmartDashesRegExp() : [])
  ];

  d(`Found ${substitutions.length} substitutions in NSUserDictionaryReplacementItems`);

  // NB: Run each replacement string through our smart quotes & dashes regex,
  // so that an input event doesn't cause chained substitutions. Also sort
  // replacements by length, to handle nested substitutions.
  let startTime = Date.now();
  let userDictionaryReplacements = substitutions
    .filter((substitution) => substitution.on !== false)
    .sort((a, b) => b.replace.length - a.replace.length)
    .map((substitution) => getSubstitutionRegExp(substitution.replace,
      scrubInputString(substitution.with, additionalReplacements)));

  let elapsed = Date.now() - startTime;
  d(`Spent ${elapsed} ms in getReplacementItems`);

  return [
    ...userDictionaryReplacements,
    ...additionalReplacements
  ];
}

/**
 * Subscribes to the `input` event and performs text substitution.
 *
 * @param  {EventTarget} element                      The DOM node to listen to
 * @param  {Array<ReplacementItem>} replacementItems  An array of replacement items
 * @return {Disposable}                               A `Disposable` that will remove the listener
 */
function addInputListener(element, replacementItems) {
  let ignoreEvent = false;

  let inputListener = () => {
    if (ignoreEvent) return;
    ignoreEvent = true;

    for (let {regExp, replacement} of replacementItems) {
      // Rather than search the entire input, we're just going to check the word
      // immediately before the caret (along with its surrounding whitespace).
      // This is to avoid substitutions after, say, a paste or an undo.
      let searchStartIndex = lastIndexOfWhitespace(element.value, element.selectionEnd);
      let lastWordBlock = element.value.substring(searchStartIndex, element.selectionEnd);
      let match = lastWordBlock.match(regExp);

      if (match && match.length === 3) {
        d(`Got a match of length ${match[0].length} at index ${match.index}: ${JSON.stringify(match)}`);

        if (some(replacementItems, (item) => item.match === match[0])) {
          d(`The match is a prefix of another replacement item (${match[0]}), skip it`);
          continue;
        }

        let selection = {
          startIndex: searchStartIndex + match.index,
          endIndex: searchStartIndex + match.index + match[0].length
        };

        replaceText(element, selection, formatReplacement(match, replacement));
      }
    }

    ignoreEvent = false;
  };

  let keyDownListener = (e) => {
    if (isUndoRedoEvent(e)) {
      d(`Undo or redo text from ${e.target.value}`);
      ignoreEvent = true;
    }
  };

  let pasteListener = () => {
    ignoreEvent = true;
  };

  let keyUpListener = () => {
    ignoreEvent = false;
  };

  element.addEventListener('keydown', keyDownListener, true);
  element.addEventListener('paste', pasteListener, true);
  element.addEventListener('keyup', keyUpListener, true);
  element.addEventListener('input', inputListener);

  d(`Added input listener to ${element.id} matching against ${replacementItems.length} replacements`);

  return new Disposable(() => {
    element.removeEventListener('keydown', keyDownListener);
    element.removeEventListener('paste', pasteListener);
    element.removeEventListener('keyup', keyUpListener);
    element.removeEventListener('input', inputListener);

    d(`Removed input listener from ${element.id}`);
  });
}

function lastIndexOfWhitespace(value, fromIndex) {
  let lastIndex = 0;
  let whitespace = /\s/g;
  let textToCaret = value.substring(0, fromIndex).trimRight();

  while (whitespace.exec(textToCaret) !== null) {
    lastIndex = whitespace.lastIndex;
  }
  return lastIndex;
}

/**
 * Performs the actual text replacement using `dispatchEvent`. We use events to
 * preserve the user's cursor index and make the substitution undoable.
 *
 * @param  {EventTarget} element  The DOM node where text is being substituted
 * @param  {Number} {startIndex   Start index of the text to replace
 * @param  {Number} endIndex}     End index of the text to replace
 * @param  {String} newText       The text being inserted
 */
function replaceText(element, {startIndex, endIndex}, newText) {
  let textEvent = document.createEvent('TextEvent');
  textEvent.initTextEvent('textInput', true, true, null, newText);

  element.selectionStart = startIndex;
  element.selectionEnd = endIndex;

  d(`Replacing ${element.value.substring(startIndex, endIndex)} with ${newText}`);
  element.dispatchEvent(textEvent);
}
<|endoftext|>


<commit_before>#!/usr/bin/env node
import http from 'q-io/http';
import fs from 'q-io/fs';
import keytar from 'keytar';
import { exec } from 'child_process';
import readline from 'readline';
import os from 'os';

const gitPath = Promise.resolve('git');
const home = process.platform === 'darwin' ? process.env.HOME : process.env.HOMEDRIVE + process.env.HOMEPATH;
const configFileName = '.clonejs';
const keytarService = 'clone-leeroy';
const keytarAccount = 'github-token';
let gitHubAccessToken = '';

getGitHubAccessToken()
  .then(token => {
    if (!token) throw new Error('GitHub Personal Access Token was not specified.');
    gitHubAccessToken = token;
    return process.argv;
  })
  .then(([, , project, ...flags]) => {
    const parsedFlags = parseFlags(flags);
    if (!project) return readLeeroyConfig();
    else if (parsedFlags.save) return createLeeroyConfig(project);
    else return project;
  })
  .then(project => {
    if (!project) throw new Error('Usage: clone-leeroy [CONFIGNAME, file, url] [--save]');
    return fetchProjectConfiguration(project)
      .catch(() => { throw new Error(`Couldn't get Leeroy config file`); });
  })
  .then(data => {
    const config = JSON.parse(data);
    if (!config.submodules) throw new Error(`Leeroy config file is missing 'submodules' configuration.`);
    return config;
  })
  .then(config => {
    const createPromise = createSolutionInfos();
    return createPromise.then(() => {
      const promises = Object.keys(config.submodules).map(name => processSubmodule(name, config));
      return Promise.all(promises);
    });
  })
  .catch(error => {
    console.error('\x1b[31m' + (error.message || `Error: ${error}`) + '\x1b[0m');
    process.exitCode = 1;
  });

function getGitHubAccessToken() {
  const gitHubAccessToken = process.env.GITHUB_TOKEN;
  if (gitHubAccessToken) return Promise.resolve(gitHubAccessToken);
  return keytar.getPassword(keytarService, keytarAccount)
    .then(token =>
      {
        if (token) return token;
        return new Promise((resolve, reject) => {
          console.error(`A GitHub Personal Access Token is required. You can specify this with the
GITHUB_TOKEN environment variable, or enter one now (which will be saved
securely in your keychain).
To create a PAT, go to https://git/settings/tokens and create one named
'clone-leeroy' with 'public_repo' permissions.`);

          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
          });

          rl.question('Enter token or press Ctrl+C to use an environment variable: ', (enteredToken) => {
            rl.close();
            if (!enteredToken) {
              reject('GitHub Personal Access Token is required');
            } else {
              resolve(keytar.setPassword(keytarService, keytarAccount, enteredToken)
                .then(() => enteredToken));
            }
          });
        });
      });
}

function readLeeroyConfig() {
  return fs.read(configFileName)
    .then(content => {
      const settings = JSON.parse(content);
      return settings.leeroyConfig;
    })
    .catch(() => null);
}

function createLeeroyConfig(project) {
  const settings = { leeroyConfig: project };
  return fs.write(configFileName, JSON.stringify(settings))
    .then(() => project);
}

function createSolutionInfos() {
  const solutionInfos = [{
    name: 'SolutionInfo.cs',
    data: `using System.Reflection;

[assembly: AssemblyVersion("9.99.0.0")]
[assembly: AssemblyCompany("Faithlife")]
[assembly: AssemblyCopyright("Copyright 2015 Faithlife")]
[assembly: AssemblyDescription("Local Build")]
`
  },
  {
    name: 'SolutionInfo.h',
    data: `#pragma once

#define ASSEMBLY_VERSION_MAJOR 9
#define ASSEMBLY_VERSION_MINOR 99
#define ASSEMBLY_VERSION_BUILD 0
#define ASSEMBLY_VERSION_MAJOR_MINOR_BUILD 1337
#define ASSEMBLY_VERSION_REVISION 0
#define ASSEMBLY_VERSION_STRING "9.99.0.0"

#define ASSEMBLY_COMPANY "Faithlife"
#define ASSEMBLY_COPYRIGHT "Copyright 2015 Faithlife"
`
  }];

  return Promise.all(solutionInfos.map(info => fs.exists(info.name)
    .then(exists => exists || fs.write(info.name, info.data))
  ));
}

function processSubmodule(name, config) {
  const branch = config.submodules[name];

  const [owner, repo] = name.split('/');
  const submodule = {
    _logs: [],
    owner,
    repo,
    branch,
    remoteUrl: `git@git:${owner}/${repo}.git`,
    log(message, indent=0) { this._logs = this._logs.concat(message.split(/\r?\n/).map((line) => { return Array(indent + 1).join(' ') + line; })); },
    output() { return this._logs.join(os.EOL); }
  };
  submodule.log(`Processing ${name}`);

  return fs.exists(submodule.repo)
    .then(exists =>
      !exists ?
        clone(submodule) :
        checkRemote(submodule)
          .then(() => fetchOrigin(submodule))
          .then(() => checkBranch(submodule))
          .then(() => pull(submodule))
    )
    .then(() => {
      console.log(submodule.output());
    }).catch(error => {
        submodule.log(error.message || error, 2);
        return Promise.reject(submodule.output());
    });
}

function clone(submodule) {
  submodule.log(`Directory ${submodule.repo} does not exist; cloning it.`, 2);
  return exec_git(`clone --recursive --branch ${submodule.branch} ${submodule.remoteUrl}`, {});
}

function checkRemote(submodule) {
  return exec_git('config --get remote.origin.url', { cwd: submodule.repo })
    .then(currentRemoteUrl => {
      if (currentRemoteUrl !== submodule.remoteUrl) {
        submodule.log(`Changing origin URL from ${currentRemoteUrl} to ${submodule.remoteUrl}`, 2);
        return changeRemoteUrl(submodule);
      } else {
        return null;
    }
    });
}

function changeRemoteUrl(submodule) {
  return exec_git('remote rm origin', { cwd: submodule.repo })
    .then(() => exec_git(`remote add origin ${submodule.remoteUrl}`, { cwd: submodule.repo }));
}

function fetchOrigin(submodule) {
  submodule.log('Fetching commits from origin...', 2);
  return exec_git('fetch origin', { cwd: submodule.repo });
}

function checkBranch(submodule) {
  return exec_git('symbolic-ref --short -q HEAD', { cwd: submodule.repo })
    .then(currentBranch => {
      if (currentBranch !== submodule.branch) {
        submodule.log(`Switching branches from ${currentBranch} to ${submodule.branch}`, 2);
        return exec_git(`branch --list -q --no-color ${submodule.branch}`, { cwd: submodule.repo })
          .then(existingTargetBranch => {
            if (existingTargetBranch !== submodule.branch) {
              return exec_git(`checkout -B ${submodule.branch} --track origin/${submodule.branch}`, { cwd: submodule.repo });
            } else {
              return exec_git(`checkout ${submodule.branch}`, { cwd: submodule.repo });
            }
          });
        } else {
          return null;
        }
      });
}

function pull(submodule) {
  return exec_git(`pull --rebase origin ${submodule.branch}`, { cwd: submodule.repo })
    .then(pullOutput => Promise.all([pullOutput, exec_git('submodule update --init --recursive', { cwd: submodule.repo })]))
    .then(([pullOutput]) => {
        submodule.log(`${pullOutput}`, 2);
      });
}

function exec_git(args, options) {
  options.env = options.env || {};
  options.env.HOME = home;

  // Might be necessary on Mac OS X to set SSH_AUTH_SOCK if user
  // has configured SSH Agent Forwarding
  // See https://help.github.com/articles/using-ssh-agent-forwarding
  options.env.SSH_AUTH_SOCK = process.env.SSH_AUTH_SOCK;
  return gitPath.then(path =>
    new Promise((resolve, reject) => {
      exec(`${path} ${args}`, options, (error, stdout) => {
        if (error) {
          reject(error);
        } else {
          resolve(stdout.toString().trim());
        }
      });
  }));
}

function parseFlags(flags) {
  // --save
  const parsedFlags = {};
  for (let flagIndex = 0; flagIndex < (flags || []).length; flagIndex++) {
    const flag = flags[flagIndex];
    const flagName = flag.startsWith('--') ? flag.substring(2) : null;
    if (flagName === 'save') {
      parsedFlags.save = true;
    }
    else {
      parsedFlags[flagName] = flags[++flagIndex];
    }
  }
  return parsedFlags;
}

function fetchProjectConfiguration(projectish) {
  return new Promise( (resolve, reject) => {
    fs.exists(projectish).then( fileExists => {
      if (fileExists)
      {
        fs.read(projectish).then( data => {
          console.log(`Read configuration from ${projectish}`);
          resolve(data);
        });
        return;
      }

      process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      http
        .read(projectish)
        .then(data => {
          console.log(`Read configuration from ${projectish}`);
          resolve(data);
        })
        .catch(() => {
          const url = `https://git/raw/Build/Configuration/master/${projectish}.json`;
          http
            .read({
              url,
              headers: {
                'Accept': 'application/vnd.github.v3.raw',
                'Authorization': 'token ' + gitHubAccessToken
              }
            })
            .then(data => {
              console.log(`Read configuration from ${url}`);
              resolve(data);
            })
            .catch(err => {
              console.error('Couldn\'t read Leeroy configuration from GitHub; is your GitHub PAT correct?');
              keytar.deletePassword(keytarService, keytarAccount)
                .then(() => reject(err));
            });
        });
    });
  });
}
<commit_msg>Pull checking out branches into functions<commit_after>#!/usr/bin/env node
import http from 'q-io/http';
import fs from 'q-io/fs';
import keytar from 'keytar';
import { exec } from 'child_process';
import readline from 'readline';
import os from 'os';

const gitPath = Promise.resolve('git');
const home = process.platform === 'darwin' ? process.env.HOME : process.env.HOMEDRIVE + process.env.HOMEPATH;
const configFileName = '.clonejs';
const keytarService = 'clone-leeroy';
const keytarAccount = 'github-token';
let gitHubAccessToken = '';

getGitHubAccessToken()
  .then(token => {
    if (!token) throw new Error('GitHub Personal Access Token was not specified.');
    gitHubAccessToken = token;
    return process.argv;
  })
  .then(([, , project, ...flags]) => {
    const parsedFlags = parseFlags(flags);
    if (!project) return readLeeroyConfig();
    else if (parsedFlags.save) return createLeeroyConfig(project);
    else return project;
  })
  .then(project => {
    if (!project) throw new Error('Usage: clone-leeroy [CONFIGNAME, file, url] [--save]');
    return fetchProjectConfiguration(project)
      .catch(() => { throw new Error(`Couldn't get Leeroy config file`); });
  })
  .then(data => {
    const config = JSON.parse(data);
    if (!config.submodules) throw new Error(`Leeroy config file is missing 'submodules' configuration.`);
    return config;
  })
  .then(config => {
    const createPromise = createSolutionInfos();
    return createPromise.then(() => {
      const promises = Object.keys(config.submodules).map(name => processSubmodule(name, config));
      return Promise.all(promises);
    });
  })
  .catch(error => {
    console.error('\x1b[31m' + (error.message || `Error: ${error}`) + '\x1b[0m');
    process.exitCode = 1;
  });

function getGitHubAccessToken() {
  const gitHubAccessToken = process.env.GITHUB_TOKEN;
  if (gitHubAccessToken) return Promise.resolve(gitHubAccessToken);
  return keytar.getPassword(keytarService, keytarAccount)
    .then(token =>
      {
        if (token) return token;
        return new Promise((resolve, reject) => {
          console.error(`A GitHub Personal Access Token is required. You can specify this with the
GITHUB_TOKEN environment variable, or enter one now (which will be saved
securely in your keychain).
To create a PAT, go to https://git/settings/tokens and create one named
'clone-leeroy' with 'public_repo' permissions.`);

          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
          });

          rl.question('Enter token or press Ctrl+C to use an environment variable: ', (enteredToken) => {
            rl.close();
            if (!enteredToken) {
              reject('GitHub Personal Access Token is required');
            } else {
              resolve(keytar.setPassword(keytarService, keytarAccount, enteredToken)
                .then(() => enteredToken));
            }
          });
        });
      });
}

function readLeeroyConfig() {
  return fs.read(configFileName)
    .then(content => {
      const settings = JSON.parse(content);
      return settings.leeroyConfig;
    })
    .catch(() => null);
}

function createLeeroyConfig(project) {
  const settings = { leeroyConfig: project };
  return fs.write(configFileName, JSON.stringify(settings))
    .then(() => project);
}

function createSolutionInfos() {
  const solutionInfos = [{
    name: 'SolutionInfo.cs',
    data: `using System.Reflection;

[assembly: AssemblyVersion("9.99.0.0")]
[assembly: AssemblyCompany("Faithlife")]
[assembly: AssemblyCopyright("Copyright 2015 Faithlife")]
[assembly: AssemblyDescription("Local Build")]
`
  },
  {
    name: 'SolutionInfo.h',
    data: `#pragma once

#define ASSEMBLY_VERSION_MAJOR 9
#define ASSEMBLY_VERSION_MINOR 99
#define ASSEMBLY_VERSION_BUILD 0
#define ASSEMBLY_VERSION_MAJOR_MINOR_BUILD 1337
#define ASSEMBLY_VERSION_REVISION 0
#define ASSEMBLY_VERSION_STRING "9.99.0.0"

#define ASSEMBLY_COMPANY "Faithlife"
#define ASSEMBLY_COPYRIGHT "Copyright 2015 Faithlife"
`
  }];

  return Promise.all(solutionInfos.map(info => fs.exists(info.name)
    .then(exists => exists || fs.write(info.name, info.data))
  ));
}

function processSubmodule(name, config) {
  const branch = config.submodules[name];

  const [owner, repo] = name.split('/');
  const submodule = {
    _logs: [],
    owner,
    repo,
    branch,
    remoteUrl: `git@git:${owner}/${repo}.git`,
    log(message, indent=0) { this._logs = this._logs.concat(message.split(/\r?\n/).map((line) => { return Array(indent + 1).join(' ') + line; })); },
    output() { return this._logs.join(os.EOL); }
  };
  submodule.log(`Processing ${name}`);

  return fs.exists(submodule.repo)
    .then(exists =>
      !exists ?
        clone(submodule) :
        checkRemote(submodule)
          .then(() => fetchOrigin(submodule))
          .then(() => checkBranch(submodule))
          .then(() => pull(submodule))
    )
    .then(() => {
      console.log(submodule.output());
    }).catch(error => {
        submodule.log(error.message || error, 2);
        return Promise.reject(submodule.output());
    });
}

function clone(submodule) {
  submodule.log(`Directory ${submodule.repo} does not exist; cloning it.`, 2);
  return exec_git(`clone --recursive --branch ${submodule.branch} ${submodule.remoteUrl}`, {});
}

function checkRemote(submodule) {
  return exec_git('config --get remote.origin.url', { cwd: submodule.repo })
    .then(currentRemoteUrl => {
      if (currentRemoteUrl !== submodule.remoteUrl) {
        submodule.log(`Changing origin URL from ${currentRemoteUrl} to ${submodule.remoteUrl}`, 2);
        return changeRemoteUrl(submodule);
      } else {
        return null;
    }
    });
}

function changeRemoteUrl(submodule) {
  return exec_git('remote rm origin', { cwd: submodule.repo })
    .then(() => exec_git(`remote add origin ${submodule.remoteUrl}`, { cwd: submodule.repo }));
}

function fetchOrigin(submodule) {
  submodule.log('Fetching commits from origin...', 2);
  return exec_git('fetch origin', { cwd: submodule.repo });
}

function checkBranch(submodule) {
  const gitOptions = { cwd: submodule.repo }
  return exec_git('symbolic-ref --short -q HEAD', gitOptions)
    .then(currentBranch => {
      if (currentBranch !== submodule.branch) {
        submodule.log(`Switching branches from ${currentBranch} to ${submodule.branch}`, 2);
        return exec_git(`branch --list -q --no-color ${submodule.branch}`, gitOptions)
          .then(existingTargetBranch => {
            if (existingTargetBranch !== submodule.branch) {
              return checkoutAndTrackNewBranch(submodule);
            } else {
              return checkoutBranch(submodule);
            }
          });
        } else {
          return null;
        }
      });
}

function checkoutAndTrackNewBranch(submodule) {
  return exec_git(`checkout -B ${submodule.branch} --track origin/${submodule.branch}`, { cwd: submodule.repo });
}

function checkoutBranch(submodule) {
  return exec_git(`checkout ${submodule.branch}`, { cwd: submodule.repo });
}

function pull(submodule) {
  return exec_git(`pull --rebase origin ${submodule.branch}`, { cwd: submodule.repo })
    .then(pullOutput => Promise.all([pullOutput, exec_git('submodule update --init --recursive', { cwd: submodule.repo })]))
    .then(([pullOutput]) => {
        submodule.log(`${pullOutput}`, 2);
      });
}

function exec_git(args, options) {
  options.env = options.env || {};
  options.env.HOME = home;

  // Might be necessary on Mac OS X to set SSH_AUTH_SOCK if user
  // has configured SSH Agent Forwarding
  // See https://help.github.com/articles/using-ssh-agent-forwarding
  options.env.SSH_AUTH_SOCK = process.env.SSH_AUTH_SOCK;
  return gitPath.then(path =>
    new Promise((resolve, reject) => {
      exec(`${path} ${args}`, options, (error, stdout) => {
        if (error) {
          reject(error);
        } else {
          resolve(stdout.toString().trim());
        }
      });
  }));
}

function parseFlags(flags) {
  // --save
  const parsedFlags = {};
  for (let flagIndex = 0; flagIndex < (flags || []).length; flagIndex++) {
    const flag = flags[flagIndex];
    const flagName = flag.startsWith('--') ? flag.substring(2) : null;
    if (flagName === 'save') {
      parsedFlags.save = true;
    }
    else {
      parsedFlags[flagName] = flags[++flagIndex];
    }
  }
  return parsedFlags;
}

function fetchProjectConfiguration(projectish) {
  return new Promise( (resolve, reject) => {
    fs.exists(projectish).then( fileExists => {
      if (fileExists)
      {
        fs.read(projectish).then( data => {
          console.log(`Read configuration from ${projectish}`);
          resolve(data);
        });
        return;
      }

      process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      http
        .read(projectish)
        .then(data => {
          console.log(`Read configuration from ${projectish}`);
          resolve(data);
        })
        .catch(() => {
          const url = `https://git/raw/Build/Configuration/master/${projectish}.json`;
          http
            .read({
              url,
              headers: {
                'Accept': 'application/vnd.github.v3.raw',
                'Authorization': 'token ' + gitHubAccessToken
              }
            })
            .then(data => {
              console.log(`Read configuration from ${url}`);
              resolve(data);
            })
            .catch(err => {
              console.error('Couldn\'t read Leeroy configuration from GitHub; is your GitHub PAT correct?');
              keytar.deletePassword(keytarService, keytarAccount)
                .then(() => reject(err));
            });
        });
    });
  });
}
<|endoftext|>


<commit_before>import messengerFactory from './lib/messenger';
import config from './config';

function createBot() {
  if (process.env.NODE_ENV === 'production') {
    const bot = new TelegramBot(
      config.telegram.token,
      {
        webHook: {
          port: config.telegram.port,
          host: config.telegram.host,
        },
      }
      );
    bot.setWebHook(`${config.telegram.externalUrl}:443/bot${config.telegram.token}`);
    return bot;
  }
  return new TelegramBot(config.telegram.token, { polling: true });
}

const telegram = messengerFactory(createBot());

telegram.listen().then(() => console.log('Listening'));
<commit_msg>Add missing import.<commit_after>import TelegramBot from 'node-telegram-bot-api';
import messengerFactory from './lib/messenger';
import config from './config';

function createBot() {
  if (process.env.NODE_ENV === 'production') {
    const bot = new TelegramBot(
      config.telegram.token,
      {
        webHook: {
          port: config.telegram.port,
          host: config.telegram.host,
        },
      }
    );
    bot.setWebHook(`${config.telegram.externalUrl}:443/bot${config.telegram.token}`);
    return bot;
  }
  return new TelegramBot(config.telegram.token, { polling: true });
}

const telegram = messengerFactory(createBot());

telegram.listen().then(() => console.log('Listening'));
<|endoftext|>


<commit_before>/* eslint-disable react/jsx-filename-extension */
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Route, BrowserRouter } from 'react-router-dom';
import store from 'store';
import { getMetadata } from 'actions';
import App from 'components/App';
import constants from 'components/constants';
import { injectGlobal } from 'styled-components';
// import unregisterServiceWorker from './registerServiceWorker';
import { unregister } from './registerServiceWorker';

// Inject global styles
injectGlobal([`
body {
  margin: 0;
  font-family: ${constants.fontFamily};
}

a {
  color: ${constants.primaryLinkColor};
  text-decoration: none;
  transition: ${constants.normalTransition};

  &:hover {
    color: color(${constants.primaryLinkColor} lightness(-33%));
  }
}

li {
  list-style-type: none;
}

#react {
  height: 100%;
  overflow-x: hidden;
  min-height: 100vh;
  background-color: #192023;
  background-image: -webkit-linear-gradient(315deg, #2e2d45, #1c2127);
  background-image: linear-gradient(135deg, #2e2d45, #1c2127);
  color: ${constants.primaryTextColor};
}

[data-tip="true"] {
  cursor: help;
}

[data-id="tooltip"] {
  padding: 8px 12px !important;
  border-radius: 2px !important;
  background-color: ${constants.almostBlack} !important;
  color: ${constants.textColorPrimary} !important;
  white-space: pre-wrap;
  line-height: 1.5 !important;
  text-align: left;
  margin: -3px !important;

  &:matches(::after, ::before) {
    content: none !important;
  }
}

@keyframes tooltip-appear {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

[data-hint] {
  &::before,
  &::after {
    position: absolute;
    display: inline-block;
    opacity: 0;
    z-index: 10000;
    pointer-events: none;
  }

  &::before {
    content: "";
    width: 0;
    height: 0;
  }

  &::after {
    content: attr(data-hint);
    background-color: ${constants.almostBlack};
    color: ${constants.textColorPrimary};
    border-radius: 2px;
    padding: 5px 8px;
    font-weight: ${constants.fontWeightLight};
    text-transform: none;
    font-size: 13px;
    line-height: 1.3;
    white-space: nowrap;
  }

  &:hover {
    cursor: help;

    &::before,
    &::after {
      animation-name: tooltip-appear;
      animation-duration: 0.1s;
      animation-fill-mode: forwards;
      animation-timing-function: ease-in;
      animation-delay: 0.4s;
    }
  }
}

[data-hint-position="top"] {
  &::after {
    bottom: 100%;
    margin-bottom: 3px;
    margin-left: -24px;
  }

  &::before {
    border-style: solid;
    border-width: 3px 6px 0 6px;
    border-color: ${constants.almostBlack} transparent transparent transparent;
    top: -3px;
  }
}

[data-hint-position="bottom"] {
  &::after {
    top: 100%;
    margin-top: 3px;
    margin-left: -24px;
  }

  &::before {
    border-style: solid;
    border-width: 0 6px 3px 6px;
    border-color: transparent transparent ${constants.almostBlack} transparent;
    bottom: -3px;
  }
}
`]);

// Fetch metadata (used on all pages)
store.dispatch(getMetadata());

const reactElement = document.getElementById('react');
const app = (
  <Provider store={store}>
    <BrowserRouter>
      <Route component={App} />
    </BrowserRouter>
  </Provider>);
render(app, reactElement);
// registerServiceWorker();
unregister();
<commit_msg>re-enable service worker<commit_after>/* eslint-disable react/jsx-filename-extension */
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Route, BrowserRouter } from 'react-router-dom';
import store from 'store';
import { getMetadata } from 'actions';
import App from 'components/App';
import constants from 'components/constants';
import { injectGlobal } from 'styled-components';
import registerServiceWorker from './registerServiceWorker';
// import { unregister } from './registerServiceWorker';

// Inject global styles
injectGlobal([`
body {
  margin: 0;
  font-family: ${constants.fontFamily};
}

a {
  color: ${constants.primaryLinkColor};
  text-decoration: none;
  transition: ${constants.normalTransition};

  &:hover {
    color: color(${constants.primaryLinkColor} lightness(-33%));
  }
}

li {
  list-style-type: none;
}

#react {
  height: 100%;
  overflow-x: hidden;
  min-height: 100vh;
  background-color: #192023;
  background-image: -webkit-linear-gradient(315deg, #2e2d45, #1c2127);
  background-image: linear-gradient(135deg, #2e2d45, #1c2127);
  color: ${constants.primaryTextColor};
}

[data-tip="true"] {
  cursor: help;
}

[data-id="tooltip"] {
  padding: 8px 12px !important;
  border-radius: 2px !important;
  background-color: ${constants.almostBlack} !important;
  color: ${constants.textColorPrimary} !important;
  white-space: pre-wrap;
  line-height: 1.5 !important;
  text-align: left;
  margin: -3px !important;

  &:matches(::after, ::before) {
    content: none !important;
  }
}

@keyframes tooltip-appear {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

[data-hint] {
  &::before,
  &::after {
    position: absolute;
    display: inline-block;
    opacity: 0;
    z-index: 10000;
    pointer-events: none;
  }

  &::before {
    content: "";
    width: 0;
    height: 0;
  }

  &::after {
    content: attr(data-hint);
    background-color: ${constants.almostBlack};
    color: ${constants.textColorPrimary};
    border-radius: 2px;
    padding: 5px 8px;
    font-weight: ${constants.fontWeightLight};
    text-transform: none;
    font-size: 13px;
    line-height: 1.3;
    white-space: nowrap;
  }

  &:hover {
    cursor: help;

    &::before,
    &::after {
      animation-name: tooltip-appear;
      animation-duration: 0.1s;
      animation-fill-mode: forwards;
      animation-timing-function: ease-in;
      animation-delay: 0.4s;
    }
  }
}

[data-hint-position="top"] {
  &::after {
    bottom: 100%;
    margin-bottom: 3px;
    margin-left: -24px;
  }

  &::before {
    border-style: solid;
    border-width: 3px 6px 0 6px;
    border-color: ${constants.almostBlack} transparent transparent transparent;
    top: -3px;
  }
}

[data-hint-position="bottom"] {
  &::after {
    top: 100%;
    margin-top: 3px;
    margin-left: -24px;
  }

  &::before {
    border-style: solid;
    border-width: 0 6px 3px 6px;
    border-color: transparent transparent ${constants.almostBlack} transparent;
    bottom: -3px;
  }
}
`]);

// Fetch metadata (used on all pages)
store.dispatch(getMetadata());

const reactElement = document.getElementById('react');
const app = (
  <Provider store={store}>
    <BrowserRouter>
      <Route component={App} />
    </BrowserRouter>
  </Provider>);
render(app, reactElement);
registerServiceWorker();
// unregister();
<|endoftext|>


<commit_before>import composeDecorators from './compose'
import decorate from './decorate'
import makeDecorator from './makeDecorator'

export default {
  composeDecorators,
  decorate,
  makeDecorator,
}
<commit_msg>use a normal export for index<commit_after>import composeDecorators from './compose'
import decorate from './decorate'
import makeDecorator from './makeDecorator'

module.exports = {
  composeDecorators,
  decorate,
  makeDecorator,
}
<|endoftext|>


<commit_before>import fs from 'fs'
import cssAstFormatter from 'css-fork-pocketjoso'
import puppeteer from 'puppeteer'

import generateCriticalCss from './core'
import normalizeCss from './normalize-css'
import nonMatchingMediaQueryRemover from './non-matching-media-query-remover'

const DEFAULT_VIEWPORT_WIDTH = 1300 // px
const DEFAULT_VIEWPORT_HEIGHT = 900 // px
const DEFAULT_TIMEOUT = 30000 // ms
const DEFAULT_MAX_EMBEDDED_BASE64_LENGTH = 1000 // chars
const DEFAULT_USER_AGENT = 'Penthouse Critical Path CSS Generator'
const DEFAULT_RENDER_WAIT_TIMEOUT = 100
const DEFAULT_BLOCK_JS_REQUESTS = true
const DEFAULT_PROPERTIES_TO_REMOVE = [
  '(.*)transition(.*)',
  'cursor',
  'pointer-events',
  '(-webkit-)?tap-highlight-color',
  '(.*)user-select'
]

function exitHandler () {
  if (browser && browser.close) {
    browser && browser.close && browser.close()
    browser = null
  }
  process.exit(0)
}

// shared between penthouse calls
let browser = null
let _browserLaunchPromise = null
// browser.pages is not implemented, so need to count myself to not close browser
// until all pages used by penthouse are closed (i.e. individual calls are done)
let _browserPagesOpen = 0
const launchBrowserIfNeeded = async function (debuglog) {
  if (browser) {
    return
  }
  if (!_browserLaunchPromise) {
    debuglog('no browser instance, launching new browser..')
    _browserLaunchPromise = puppeteer
      .launch({
        ignoreHTTPSErrors: true,
        args: ['--disable-setuid-sandbox', '--no-sandbox']
      })
      .then(browser => {
        debuglog('new browser launched')
        return browser
      })
  }
  browser = await _browserLaunchPromise
  _browserLaunchPromise = null
}

async function browserIsRunning () {
  try {
    // will throw 'Not opened' error if browser is not running
    await browser.version()
    return true
  } catch (e) {
    return false
  }
}

function readFilePromise (filepath, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(filepath, encoding, (err, content) => {
      if (err) {
        return reject(err)
      }
      resolve(content)
    })
  })
}

function prepareForceIncludeForSerialization (forceInclude = []) {
  // need to annotate forceInclude values to allow RegExp to pass through JSON serialization
  return forceInclude.map(function (forceIncludeValue) {
    if (
      typeof forceIncludeValue === 'object' &&
      forceIncludeValue.constructor.name === 'RegExp'
    ) {
      return {
        type: 'RegExp',
        source: forceIncludeValue.source,
        flags: forceIncludeValue.flags
      }
    }
    return { value: forceIncludeValue }
  })
}

const astFromCss = async function astFromCss (options, { debuglog, stdErr }) {
  const css = options.cssString.replace(//g, '\f042')

  let ast = cssAstFormatter.parse(css, { silent: true })
  const parsingErrors = ast.stylesheet.parsingErrors.filter(function (err) {
    // the forked version of the astParser used fixes these errors itself
    return err.reason !== 'Extra closing brace'
  })
  if (parsingErrors.length === 0) {
    stdErr += debuglog('parsed ast (without errors)')
    return ast
  }

  // had breaking parsing errors
  // NOTE: only informing about first error, even if there were more than one.
  const parsingErrorMessage = parsingErrors[0].message
  if (options.strict === true) {
    // TODO: filename will be 'undefined', could enhance this error message
    throw new Error(parsingErrorMessage)
  }

  stdErr += debuglog(
    "Failed ast formatting css '" + parsingErrorMessage + "': "
  )

  let normalizedCss
  try {
    _browserPagesOpen++
    debuglog('adding browser page for normalize, now: ' + _browserPagesOpen)
    normalizedCss = await normalizeCss({
      css,
      browser,
      debuglog
    })
    _browserPagesOpen--
    debuglog('removing browser page for normalize, now: ' + _browserPagesOpen)
  } catch (e) {
    _browserPagesOpen--
    debuglog(
      'removing browser page for normalize after error, now: ' +
        _browserPagesOpen
    )
    throw e
  }

  stdErr += debuglog(
    'normalized css: ' +
      (normalizedCss ? normalizedCss.length : typeof normalizedCss)
  )
  if (!normalizedCss) {
    throw new Error(
      "Failed to normalize CSS errors. Run Penthouse with 'strict: true' option to see these css errors."
    )
  }
  ast = cssAstFormatter.parse(normalizedCss, { silent: true })
  stdErr += debuglog('parsed normalised css into ast')
  const finalParsingErrors = ast.stylesheet.parsingErrors.filter(function (err) {
    // the forked version of the astParser used fixes these errors itself
    return err.reason !== 'Extra closing brace'
  })
  if (finalParsingErrors.length > 0) {
    stdErr += debuglog('..with parsingErrors: ' + finalParsingErrors[0].reason)
  }
  return ast
}

// const so not hoisted, so can get regeneratorRuntime inlined above, needed for Node 4
const generateCriticalCssWrapped = async function generateCriticalCssWrapped (
  options,
  ast,
  { debuglog, stdErr, START_TIME, forceTryRestartBrowser }
) {
  const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10)
  const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10)
  const timeoutWait = options.timeout || DEFAULT_TIMEOUT

  // Merge properties with default ones
  const propertiesToRemove =
    options.propertiesToRemove || DEFAULT_PROPERTIES_TO_REMOVE
  // first strip out non matching media queries
  const astRules = nonMatchingMediaQueryRemover(
    ast.stylesheet.rules,
    width,
    height
  )
  stdErr += debuglog('stripped out non matching media queries')

  // always forceInclude '*', 'html', and 'body' selectors
  const forceInclude = prepareForceIncludeForSerialization(
    [{ value: '*' }, { value: 'html' }, { value: 'body' }].concat(
      options.forceInclude || []
    )
  )

  // promise so we can handle errors and reject,
  // instead of throwing what would otherwise be uncaught errors in node process
  return new Promise(async (resolve, reject) => {
    const cleanupAndExit = ({ returnValue, error }) => {
      process.removeListener('exit', exitHandler)
      process.removeListener('SIGTERM', exitHandler)
      process.removeListener('SIGINT', exitHandler)

      if (error) {
        reject(error)
      } else {
        resolve(returnValue)
      }
    }

    stdErr += debuglog('call generateCriticalCssWrapped')
    let formattedCss
    try {
      _browserPagesOpen++
      debuglog(
        'adding browser page for generateCriticalCss, now: ' + _browserPagesOpen
      )
      formattedCss = await generateCriticalCss({
        browser,
        url: options.url,
        astRules,
        width,
        height,
        forceInclude,
        userAgent: options.userAgent || DEFAULT_USER_AGENT,
        renderWaitTime: options.renderWaitTime || DEFAULT_RENDER_WAIT_TIMEOUT,
        timeout: timeoutWait,
        blockJSRequests: options.blockJSRequests || DEFAULT_BLOCK_JS_REQUESTS,
        customPageHeaders: options.customPageHeaders,
        screenshots: options.screenshots,
        // postformatting
        propertiesToRemove,
        maxEmbeddedBase64Length:
          typeof options.maxEmbeddedBase64Length === 'number'
            ? options.maxEmbeddedBase64Length
            : DEFAULT_MAX_EMBEDDED_BASE64_LENGTH,
        debuglog
      })
      _browserPagesOpen--
      debuglog(
        'remove browser page for generateCriticalCss, now: ' + _browserPagesOpen
      )
    } catch (e) {
      _browserPagesOpen--
      debuglog(
        'remove browser page for generateCriticalCss after ERROR, now: ' +
          _browserPagesOpen
      )
      if (!forceTryRestartBrowser && !await browserIsRunning()) {
        console.error(
          'Chromium unexpecedly not opened - crashed? ' +
            '\n_browserPagesOpen: ' +
            (_browserPagesOpen + 1) +
            '\nurl: ' +
            options.url +
            '\nastRules: ' +
            astRules.length
        )
        // for some reason Chromium is no longer opened;
        // perhaps it crashed
        if (_browserLaunchPromise) {
          // in this case the browser is already restarting
          await _browserLaunchPromise
        } else {
          console.log('restarting chrome after crash')
          browser = null
          await launchBrowserIfNeeded(debuglog)
        }
        // retry
        resolve(
          generateCriticalCssWrapped(options, ast, {
            debuglog,
            stdErr,
            START_TIME,
            forceTryRestartBrowser
          })
        )
        return
      }
      stdErr += e
      const err = new Error(stdErr)
      err.stderr = stdErr
      cleanupAndExit({ error: err })
      return
    }
    stdErr += debuglog('generateCriticalCss done')
    if (formattedCss.trim().length === 0) {
      // TODO: this error should surface to user
      stdErr += debuglog(
        'Note: Generated critical css was empty for URL: ' + options.url
      )
      cleanupAndExit({ returnValue: '' })
      return
    }

    cleanupAndExit({ returnValue: formattedCss })
  })
}

const m = (module.exports = function (options, callback) {
  // init logging and debug output
  normalizeCss.DEBUG = m.DEBUG
  const START_TIME = Date.now()
  const debuglog = function (msg, isError) {
    if (m.DEBUG) {
      const errMsg =
        'time: ' +
        (Date.now() - START_TIME) +
        ' | ' +
        (isError ? 'ERR: ' : '') +
        msg
      console.error(errMsg)
      return errMsg
    }
    return ''
  }
  const logging = {
    debuglog,
    stdErr: '',
    START_TIME
  }

  process.on('exit', exitHandler)
  process.on('SIGTERM', exitHandler)
  process.on('SIGINT', exitHandler)

  return new Promise(async (resolve, reject) => {
    // still supporting legacy callback way of calling Penthouse
    const cleanupAndExit = ({ returnValue, error = null }) => {
      if (browser && !options.unstableKeepBrowserAlive) {
        if (_browserPagesOpen > 0) {
          debuglog(
            'keeping browser open as _browserPagesOpen: ' + _browserPagesOpen
          )
        } else {
          browser.close()
          browser = null
          _browserLaunchPromise = null
          debuglog('closed browser')
        }
      }

      if (callback) {
        callback(error, returnValue)
        return
      }
      if (error) {
        reject(error)
      } else {
        resolve(returnValue)
      }
    }

    // support legacy mode of passing in css file path instead of string
    if (!options.cssString && options.css) {
      try {
        const cssString = await readFilePromise(options.css, 'utf8')
        options = Object.assign({}, options, { cssString })
      } catch (err) {
        debuglog('error reading css file: ' + options.css + ', error: ' + err)
        cleanupAndExit({ error: err })
        return
      }
    }
    if (!options.cssString) {
      debuglog('Passed in css is empty')
      cleanupAndExit({ error: new Error('css should not be empty') })
      return
    }

    await launchBrowserIfNeeded(debuglog)
    try {
      const ast = await astFromCss(options, logging)
      const criticalCss = await generateCriticalCssWrapped(
        options,
        ast,
        logging
      )
      cleanupAndExit({ returnValue: criticalCss })
    } catch (err) {
      cleanupAndExit({ error: err })
    }
  })
})
<commit_msg>rm redundant guard<commit_after>import fs from 'fs'
import cssAstFormatter from 'css-fork-pocketjoso'
import puppeteer from 'puppeteer'

import generateCriticalCss from './core'
import normalizeCss from './normalize-css'
import nonMatchingMediaQueryRemover from './non-matching-media-query-remover'

const DEFAULT_VIEWPORT_WIDTH = 1300 // px
const DEFAULT_VIEWPORT_HEIGHT = 900 // px
const DEFAULT_TIMEOUT = 30000 // ms
const DEFAULT_MAX_EMBEDDED_BASE64_LENGTH = 1000 // chars
const DEFAULT_USER_AGENT = 'Penthouse Critical Path CSS Generator'
const DEFAULT_RENDER_WAIT_TIMEOUT = 100
const DEFAULT_BLOCK_JS_REQUESTS = true
const DEFAULT_PROPERTIES_TO_REMOVE = [
  '(.*)transition(.*)',
  'cursor',
  'pointer-events',
  '(-webkit-)?tap-highlight-color',
  '(.*)user-select'
]

function exitHandler () {
  if (browser && browser.close) {
    browser.close()
    browser = null
  }
  process.exit(0)
}

// shared between penthouse calls
let browser = null
let _browserLaunchPromise = null
// browser.pages is not implemented, so need to count myself to not close browser
// until all pages used by penthouse are closed (i.e. individual calls are done)
let _browserPagesOpen = 0
const launchBrowserIfNeeded = async function (debuglog) {
  if (browser) {
    return
  }
  if (!_browserLaunchPromise) {
    debuglog('no browser instance, launching new browser..')
    _browserLaunchPromise = puppeteer
      .launch({
        ignoreHTTPSErrors: true,
        args: ['--disable-setuid-sandbox', '--no-sandbox']
      })
      .then(browser => {
        debuglog('new browser launched')
        return browser
      })
  }
  browser = await _browserLaunchPromise
  _browserLaunchPromise = null
}

async function browserIsRunning () {
  try {
    // will throw 'Not opened' error if browser is not running
    await browser.version()
    return true
  } catch (e) {
    return false
  }
}

function readFilePromise (filepath, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(filepath, encoding, (err, content) => {
      if (err) {
        return reject(err)
      }
      resolve(content)
    })
  })
}

function prepareForceIncludeForSerialization (forceInclude = []) {
  // need to annotate forceInclude values to allow RegExp to pass through JSON serialization
  return forceInclude.map(function (forceIncludeValue) {
    if (
      typeof forceIncludeValue === 'object' &&
      forceIncludeValue.constructor.name === 'RegExp'
    ) {
      return {
        type: 'RegExp',
        source: forceIncludeValue.source,
        flags: forceIncludeValue.flags
      }
    }
    return { value: forceIncludeValue }
  })
}

const astFromCss = async function astFromCss (options, { debuglog, stdErr }) {
  const css = options.cssString.replace(//g, '\f042')

  let ast = cssAstFormatter.parse(css, { silent: true })
  const parsingErrors = ast.stylesheet.parsingErrors.filter(function (err) {
    // the forked version of the astParser used fixes these errors itself
    return err.reason !== 'Extra closing brace'
  })
  if (parsingErrors.length === 0) {
    stdErr += debuglog('parsed ast (without errors)')
    return ast
  }

  // had breaking parsing errors
  // NOTE: only informing about first error, even if there were more than one.
  const parsingErrorMessage = parsingErrors[0].message
  if (options.strict === true) {
    // TODO: filename will be 'undefined', could enhance this error message
    throw new Error(parsingErrorMessage)
  }

  stdErr += debuglog(
    "Failed ast formatting css '" + parsingErrorMessage + "': "
  )

  let normalizedCss
  try {
    _browserPagesOpen++
    debuglog('adding browser page for normalize, now: ' + _browserPagesOpen)
    normalizedCss = await normalizeCss({
      css,
      browser,
      debuglog
    })
    _browserPagesOpen--
    debuglog('removing browser page for normalize, now: ' + _browserPagesOpen)
  } catch (e) {
    _browserPagesOpen--
    debuglog(
      'removing browser page for normalize after error, now: ' +
        _browserPagesOpen
    )
    throw e
  }

  stdErr += debuglog(
    'normalized css: ' +
      (normalizedCss ? normalizedCss.length : typeof normalizedCss)
  )
  if (!normalizedCss) {
    throw new Error(
      "Failed to normalize CSS errors. Run Penthouse with 'strict: true' option to see these css errors."
    )
  }
  ast = cssAstFormatter.parse(normalizedCss, { silent: true })
  stdErr += debuglog('parsed normalised css into ast')
  const finalParsingErrors = ast.stylesheet.parsingErrors.filter(function (err) {
    // the forked version of the astParser used fixes these errors itself
    return err.reason !== 'Extra closing brace'
  })
  if (finalParsingErrors.length > 0) {
    stdErr += debuglog('..with parsingErrors: ' + finalParsingErrors[0].reason)
  }
  return ast
}

// const so not hoisted, so can get regeneratorRuntime inlined above, needed for Node 4
const generateCriticalCssWrapped = async function generateCriticalCssWrapped (
  options,
  ast,
  { debuglog, stdErr, START_TIME, forceTryRestartBrowser }
) {
  const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10)
  const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10)
  const timeoutWait = options.timeout || DEFAULT_TIMEOUT

  // Merge properties with default ones
  const propertiesToRemove =
    options.propertiesToRemove || DEFAULT_PROPERTIES_TO_REMOVE
  // first strip out non matching media queries
  const astRules = nonMatchingMediaQueryRemover(
    ast.stylesheet.rules,
    width,
    height
  )
  stdErr += debuglog('stripped out non matching media queries')

  // always forceInclude '*', 'html', and 'body' selectors
  const forceInclude = prepareForceIncludeForSerialization(
    [{ value: '*' }, { value: 'html' }, { value: 'body' }].concat(
      options.forceInclude || []
    )
  )

  // promise so we can handle errors and reject,
  // instead of throwing what would otherwise be uncaught errors in node process
  return new Promise(async (resolve, reject) => {
    const cleanupAndExit = ({ returnValue, error }) => {
      process.removeListener('exit', exitHandler)
      process.removeListener('SIGTERM', exitHandler)
      process.removeListener('SIGINT', exitHandler)

      if (error) {
        reject(error)
      } else {
        resolve(returnValue)
      }
    }

    stdErr += debuglog('call generateCriticalCssWrapped')
    let formattedCss
    try {
      _browserPagesOpen++
      debuglog(
        'adding browser page for generateCriticalCss, now: ' + _browserPagesOpen
      )
      formattedCss = await generateCriticalCss({
        browser,
        url: options.url,
        astRules,
        width,
        height,
        forceInclude,
        userAgent: options.userAgent || DEFAULT_USER_AGENT,
        renderWaitTime: options.renderWaitTime || DEFAULT_RENDER_WAIT_TIMEOUT,
        timeout: timeoutWait,
        blockJSRequests: options.blockJSRequests || DEFAULT_BLOCK_JS_REQUESTS,
        customPageHeaders: options.customPageHeaders,
        screenshots: options.screenshots,
        // postformatting
        propertiesToRemove,
        maxEmbeddedBase64Length: typeof options.maxEmbeddedBase64Length ===
          'number'
          ? options.maxEmbeddedBase64Length
          : DEFAULT_MAX_EMBEDDED_BASE64_LENGTH,
        debuglog
      })
      _browserPagesOpen--
      debuglog(
        'remove browser page for generateCriticalCss, now: ' + _browserPagesOpen
      )
    } catch (e) {
      _browserPagesOpen--
      debuglog(
        'remove browser page for generateCriticalCss after ERROR, now: ' +
          _browserPagesOpen
      )
      if (!forceTryRestartBrowser && !await browserIsRunning()) {
        console.error(
          'Chromium unexpecedly not opened - crashed? ' +
            '\n_browserPagesOpen: ' +
            (_browserPagesOpen + 1) +
            '\nurl: ' +
            options.url +
            '\nastRules: ' +
            astRules.length
        )
        // for some reason Chromium is no longer opened;
        // perhaps it crashed
        if (_browserLaunchPromise) {
          // in this case the browser is already restarting
          await _browserLaunchPromise
        } else {
          console.log('restarting chrome after crash')
          browser = null
          await launchBrowserIfNeeded(debuglog)
        }
        // retry
        resolve(
          generateCriticalCssWrapped(options, ast, {
            debuglog,
            stdErr,
            START_TIME,
            forceTryRestartBrowser
          })
        )
        return
      }
      stdErr += e
      const err = new Error(stdErr)
      err.stderr = stdErr
      cleanupAndExit({ error: err })
      return
    }
    stdErr += debuglog('generateCriticalCss done')
    if (formattedCss.trim().length === 0) {
      // TODO: this error should surface to user
      stdErr += debuglog(
        'Note: Generated critical css was empty for URL: ' + options.url
      )
      cleanupAndExit({ returnValue: '' })
      return
    }

    cleanupAndExit({ returnValue: formattedCss })
  })
}

const m = (module.exports = function (options, callback) {
  // init logging and debug output
  normalizeCss.DEBUG = m.DEBUG
  const START_TIME = Date.now()
  const debuglog = function (msg, isError) {
    if (m.DEBUG) {
      const errMsg =
        'time: ' +
        (Date.now() - START_TIME) +
        ' | ' +
        (isError ? 'ERR: ' : '') +
        msg
      console.error(errMsg)
      return errMsg
    }
    return ''
  }
  const logging = {
    debuglog,
    stdErr: '',
    START_TIME
  }

  process.on('exit', exitHandler)
  process.on('SIGTERM', exitHandler)
  process.on('SIGINT', exitHandler)

  return new Promise(async (resolve, reject) => {
    // still supporting legacy callback way of calling Penthouse
    const cleanupAndExit = ({ returnValue, error = null }) => {
      if (browser && !options.unstableKeepBrowserAlive) {
        if (_browserPagesOpen > 0) {
          debuglog(
            'keeping browser open as _browserPagesOpen: ' + _browserPagesOpen
          )
        } else {
          browser.close()
          browser = null
          _browserLaunchPromise = null
          debuglog('closed browser')
        }
      }

      if (callback) {
        callback(error, returnValue)
        return
      }
      if (error) {
        reject(error)
      } else {
        resolve(returnValue)
      }
    }

    // support legacy mode of passing in css file path instead of string
    if (!options.cssString && options.css) {
      try {
        const cssString = await readFilePromise(options.css, 'utf8')
        options = Object.assign({}, options, { cssString })
      } catch (err) {
        debuglog('error reading css file: ' + options.css + ', error: ' + err)
        cleanupAndExit({ error: err })
        return
      }
    }
    if (!options.cssString) {
      debuglog('Passed in css is empty')
      cleanupAndExit({ error: new Error('css should not be empty') })
      return
    }

    await launchBrowserIfNeeded(debuglog)
    try {
      const ast = await astFromCss(options, logging)
      const criticalCss = await generateCriticalCssWrapped(
        options,
        ast,
        logging
      )
      cleanupAndExit({ returnValue: criticalCss })
    } catch (err) {
      cleanupAndExit({ error: err })
    }
  })
})
<|endoftext|>


<commit_before>const { createLogger, LEVELS: { INFO } } = require('./loggers')
const LoggerConsole = require('./loggers/console')
const Cluster = require('./cluster')
const createProducer = require('./producer')
const createConsumer = require('./consumer')
const createAdmin = require('./admin')

const { assign } = Object

module.exports = class Client {
  constructor({
    brokers,
    ssl,
    sasl,
    clientId,
    connectionTimeout,
    retry,
    logLevel = INFO,
    logCreator = LoggerConsole,
  }) {
    this.logger = createLogger({ level: logLevel, logCreator })
    this.createCluster = () =>
      new Cluster({
        logger: this.logger,
        brokers,
        ssl,
        sasl,
        clientId,
        connectionTimeout,
        retry,
      })
  }

  /**
   * @public
   */
  producer({ createPartitioner, retry } = {}) {
    const cluster = this.createCluster()
    return createProducer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      createPartitioner,
    })
  }

  /**
   * @public
   */
  consumer(
    {
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
      retry,
    } = {}
  ) {
    const cluster = this.createCluster()
    return createConsumer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
    })
  }

  /**
   * @public
   */
  admin({ retry } = {}) {
    const cluster = this.createCluster()
    return createAdmin({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
    })
  }
}
<commit_msg>Accept experimental v011 flag when creating the client<commit_after>const { createLogger, LEVELS: { INFO } } = require('./loggers')
const LoggerConsole = require('./loggers/console')
const Cluster = require('./cluster')
const createProducer = require('./producer')
const createConsumer = require('./consumer')
const createAdmin = require('./admin')

const { assign } = Object

module.exports = class Client {
  constructor({
    brokers,
    ssl,
    sasl,
    clientId,
    connectionTimeout,
    retry,
    allowExperimentalV011,
    logLevel = INFO,
    logCreator = LoggerConsole,
  }) {
    this.logger = createLogger({ level: logLevel, logCreator })
    this.createCluster = () =>
      new Cluster({
        logger: this.logger,
        brokers,
        ssl,
        sasl,
        clientId,
        connectionTimeout,
        retry,
        allowExperimentalV011,
      })
  }

  /**
   * @public
   */
  producer({ createPartitioner, retry } = {}) {
    const cluster = this.createCluster()
    return createProducer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      createPartitioner,
    })
  }

  /**
   * @public
   */
  consumer(
    {
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
      retry,
    } = {}
  ) {
    const cluster = this.createCluster()
    return createConsumer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
    })
  }

  /**
   * @public
   */
  admin({ retry } = {}) {
    const cluster = this.createCluster()
    return createAdmin({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
    })
  }
}
<|endoftext|>


<commit_before>import path from 'path';
import http from 'http';
import connect from 'connect';
import modRewrite from 'connect-modrewrite';
import request from 'request';
import gateway from 'gateway';
import getPort from 'get-port';
import debugFn from 'debug';
import qs from 'qs';
import fs from 'fs';
import shell from 'shelljs';

import size from 'lodash/size';
import defaults from 'lodash/defaults';
import isObject from 'lodash/isObject';
import isFunction from 'lodash/isFunction';
import Promise from 'bluebird';

let debug = debugFn('php2html:core');

const host = '127.0.0.1';

/**
 * Get configured port or free port
 * @param {object} opts options passed to php2html
 */
let fetchPort = opts => opts.port ? new Promise(resolve => resolve(opts.port)) : getPort();

/**
 * Compute URI for gateway relative to docroot
 * @param {string} docroot
 * @param {string} file
 * @param {bool} strict
 * @returns {string}
 */
let getUri = (docroot, file, strict) => {
    let win32 = process.platform === 'win32';
    let uri;

    if (strict) {
        var stat = fs.statSync(file);
        // If file ends with a slash apend index file
        if (stat.isDirectory()) {
            file = path.join(file, 'index.php');
        }

        // absolutize
        file = path.resolve(file);
    }

    if (win32) {
        // use the correct slashes for uri
        uri = file.replace(docroot, '').replace(/[\\]/g, '/');
    } else {
        uri = file.replace(docroot, '');
    }

    // ensure that we have an absolute url
    if (uri.substr(0, 1) !== '/') {
        uri = '/' + uri;
    }

    return uri;
};

/**
 * Get connect app
 * @param {object} opts options hash passed to php2html
 */
let getConnect = opts => {
    let app = connect();

    // rewrite requests to router if applicable
    if (opts.router) {
        try {
            var router = getUri(opts.baseDir, opts.router, true);

            debug('Router script:', opts.router);
            debug('Router rewrite:', '^(.*)$ ' + router);
            app.use(modRewrite([
                '^(.*)$ ' + router
            ]));
        } catch (err) {
            debug('ERROR:', err.message);
            return new Promise((resolve, reject) => reject(err));
        }
    }

    app.use(gateway(opts.baseDir, {
        '.php': 'php-cgi'
    }));

    return app;
};

/**
 * Fetch html from
 * @param url
 * @param opts
 */
let fetchHtml = (url, opts) => new Promise((resolve, reject) => {
    request(url, function (error, response, body) {
        // request failed
        if (error) {
            return reject(error);
        }

        if (response.statusCode >= 400) {
            var message = response.statusCode + ' - ' + response.statusMessage;
            return reject(new Error(message));
        }

        // 204 No Content
        if (!body) {
            return reject(new Error('204 - No Content'));
        }

        // replace relative php links with corresponding html link
        if (body && opts.processLinks) {
            let linkRegex = /href=['"]([^'"]+\.php(?:\?[^'"]*)?)['"]/gm;
            body.match(linkRegex).forEach(link => {
                if (link.match(/:\/\//)) {
                    return;
                }
                var hlink = link.replace(/(\w)\.php([^\w])/g, '$1.html$2');

                body = body.replace(link, hlink);
            });
        }
        resolve(body);
    }).end();
});

function compile(file, opts) {
    // check php-cgi dependency
    if (!shell.which('php-cgi')) {
        return new Promise((resolve, reject) => reject(new Error('"php-cgi" not found. See https://git.io/vg20U')));
    }

    // check file
    if (!file) {
        return new Promise((resolve, reject) => reject(new Error('Missing input')));
    }

    return fetchPort(opts)
        .then(port => new Promise((resolve, reject) => {
            let app = getConnect(opts);
            let server = http.createServer(app).listen(port)
                .on('error', reject)
                .on('listening', () => resolve({server: server, port: port}));
        }))
        .then(({server, port}) => {
            var uri = getUri(opts.baseDir, file, !opts.router);
            var url = 'http://' + host + ':' + port + uri;
            if (isObject(opts.getData) && size(opts.getData)) {
                url += '?' + qs.stringify(opts.getData);
            }
            return {server: server, url: url};
        })
        .then(({server, url}) =>
            fetchHtml(url, opts).finally(() => server.close())
        );
}

export default function php2html(file, opts, cb) {
    if (isFunction(opts) && !cb) {
        cb = opts;
        opts = {};
    }

    let options = defaults(opts || {}, {
        processLinks: false,
        getData: {},
        baseDir: process.cwd()
    });

    options.baseDir = path.resolve(options.baseDir);

    let corePromise = compile(file, options);

    if (isFunction(cb)) {
        corePromise.catch(err => {
            cb(err);
            throw new Promise.CancellationError();
        }).then(output => {
            cb(null, output.toString());
        }).catch(Promise.CancellationError, () => {});
    } else {
        return corePromise;
    }
}
<commit_msg>minor fix<commit_after>import path from 'path';
import http from 'http';
import connect from 'connect';
import modRewrite from 'connect-modrewrite';
import request from 'request';
import gateway from 'gateway';
import getPort from 'get-port';
import debugFn from 'debug';
import qs from 'qs';
import fs from 'fs';
import shell from 'shelljs';

import size from 'lodash/size';
import defaults from 'lodash/defaults';
import isObject from 'lodash/isObject';
import isFunction from 'lodash/isFunction';
import Promise from 'bluebird';

let debug = debugFn('php2html:core');

const host = '127.0.0.1';

/**
 * Get configured port or free port
 * @param {object} opts options passed to php2html
 */
let fetchPort = opts => opts.port ? new Promise(resolve => resolve(opts.port)) : getPort();

/**
 * Compute URI for gateway relative to docroot
 * @param {string} docroot
 * @param {string} file
 * @param {bool} strict
 * @returns {string}
 */
let getUri = (docroot, file, strict) => {
    let win32 = process.platform === 'win32';
    let uri;

    if (strict) {
        var stat = fs.statSync(file);
        // If file ends with a slash apend index file
        if (stat.isDirectory()) {
            file = path.join(file, 'index.php');
        }

        // absolutize
        file = path.resolve(file);
    }

    if (win32) {
        // use the correct slashes for uri
        uri = file.replace(docroot, '').replace(/[\\]/g, '/');
    } else {
        uri = file.replace(docroot, '');
    }

    // ensure that we have an absolute url
    if (uri.substr(0, 1) !== '/') {
        uri = '/' + uri;
    }

    return uri;
};

/**
 * Get connect app
 * @param {object} opts options hash passed to php2html
 */
let getConnect = opts => {
    let app = connect();

    // rewrite requests to router if applicable
    if (opts.router) {
        try {
            var router = getUri(opts.baseDir, opts.router, true);

            debug('Router script:', opts.router);
            debug('Router rewrite:', '^(.*)$ ' + router);
            app.use(modRewrite([
                '^(.*)$ ' + router
            ]));
        } catch (err) {
            debug('ERROR:', err.message);
            return new Promise((resolve, reject) => reject(err));
        }
    }

    app.use(gateway(opts.baseDir, {
        '.php': 'php-cgi'
    }));

    return app;
};

/**
 * Fetch html from
 * @param url
 * @param opts
 */
let fetchHtml = (url, opts) => new Promise((resolve, reject) => {
    request(url, function (error, response, body) {
        // request failed
        if (error) {
            return reject(error);
        }

        if (response.statusCode >= 400) {
            var message = response.statusCode + ' - ' + response.statusMessage;
            return reject(new Error(message));
        }

        // 204 No Content
        if (!body) {
            return reject(new Error('204 - No Content'));
        }

        // replace relative php links with corresponding html link
        if (body && opts.processLinks) {
            let linkRegex = /href=['"]([^'"]+\.php(?:\?[^'"]*)?)['"]/gm;
            (body.match(linkRegex) || []).forEach(link => {
                if (link.match(/:\/\//)) {
                    return;
                }
                var hlink = link.replace(/(\w)\.php([^\w])/g, '$1.html$2');

                body = body.replace(link, hlink);
            });
        }
        resolve(body);
    }).end();
});

function compile(file, opts) {
    // check php-cgi dependency
    if (!shell.which('php-cgi')) {
        return new Promise((resolve, reject) => reject(new Error('"php-cgi" not found. See https://git.io/vg20U')));
    }

    // check file
    if (!file) {
        return new Promise((resolve, reject) => reject(new Error('Missing input')));
    }

    return fetchPort(opts)
        .then(port => new Promise((resolve, reject) => {
            let app = getConnect(opts);
            let server = http.createServer(app).listen(port)
                .on('error', reject)
                .on('listening', () => resolve({server: server, port: port}));
        }))
        .then(({server, port}) => {
            var uri = getUri(opts.baseDir, file, !opts.router);
            var url = 'http://' + host + ':' + port + uri;
            if (isObject(opts.getData) && size(opts.getData)) {
                url += '?' + qs.stringify(opts.getData);
            }
            return {server: server, url: url};
        })
        .then(({server, url}) =>
            fetchHtml(url, opts).finally(() => server.close())
        );
}

export default function php2html(file, opts, cb) {
    if (isFunction(opts) && !cb) {
        cb = opts;
        opts = {};
    }

    let options = defaults(opts || {}, {
        processLinks: false,
        getData: {},
        baseDir: process.cwd()
    });

    options.baseDir = path.resolve(options.baseDir);

    let corePromise = compile(file, options);

    if (isFunction(cb)) {
        corePromise.catch(err => {
            cb(err);
            throw new Promise.CancellationError();
        }).then(output => {
            cb(null, output.toString());
        }).catch(Promise.CancellationError, () => {});
    } else {
        return corePromise;
    }
}
<|endoftext|>