<commit_before>var rp = require('request-promise')
var _ = require('underscore')

var cache = require('./cache').cache
var flag = require('./cache').flag
var restart = require('./sjs-restart')

var cnt_early = 1
var lmt_early = 5

function __errorHandlingPost(params, app, job, callback, sjsHome){
  function f(err){
    console.log('ERROR: POST')
    if(err.message == 'Error: socket hang up' && err.response == undefined){
      console.log("WARNING: This should be an error during POST. Retrying after 10 seconds...")
      setTimeout(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), 10000)
    } else if(err.message.includes('Error: connect ECONNREFUSED') && err.response == undefined){
      console.log("ERROR: POST is continuously failing. Restarting Spark...")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    } else if (err.response.body.result){
      console.log("** Original Response** " + JSON.stringify(err.response.body))
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    } else {
      console.log("ERROR: Unknown error during POST. I think we have to restart SJS now. And then, resend a POST request.")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    }
  }
  return f
}

function __errorHandlingGet(params, job_id, app, job, callback, sjsHome){
  function f(err){
    console.log('ERROR: GET')
    // Type 1: SJS is unstable.
    if(err.response == undefined){
      console.log("ERROR: SJS becomes unresponsive during GET. Restarting SJS...")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
      cnt_early = 1
    } else if(typeof err.response.body == 'object'){
      err_result = err.response.body.result
      job_id = err.response.req.path.slice(6)
      console.log("**Original Response**: " + err_result)
      if(typeof err_result == 'object'){
        err_msg = err_result.message
        if (err_msg.includes("akka")){
          console.log("ERROR: akka Error. I think we have to restart SJS now.")
          restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
          cnt_early = 1
        }
      } else{
        if (err_result.includes("No such job ID")){
          if(cnt_early < lmt_early){
            console.log("INFO: Too-Early count: " + cnt_early)
            console.log("WARNING: Ok I see, I was just too early to ask for result. Let's wait and come back again now...")
            __waitAndGet(params, job_id, app, job, callback, sjsHome)
            cnt_early += 1
          } else {
            console.log("INFO: job-server's performance is getting bad. We'll restart SJS.")
            restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
            cnt_early = 1
          }
        }
      }
    } else { // Type 2: SJS is unstable in a different way.
      err_body = err.response.body
      console.log("**Original Response**: " + err_body)
      if(err_body.includes("Resource representation is only available with these Content-Types")){
        console.log("WARNING: Ok I see, GET is not ready yet. Wait and Get again...")
        job_id = err.response.req.path.slice(6)
        console.log(job)
        if(cnt_early < lmt_early){
          console.log("INFO: Too-Early count: " + cnt_early)
          __waitAndGet(params, job_id, app, job, callback, sjsHome)
          cnt_early += 1
        } else {
          console.log("INFO: job-server's performance is getting bad. We'll restart SJS.")
          restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
          cnt_early = 1
        }
      } else{ // In case of knowing nothing about the cause
        console.log(err.response)
        console.log("ERROR: Unknown (either on your machine or on Spark-Jobserver)")
        console.log("INFO: Let's try again.")
        restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
      }
    }
  }
  return f
}

function __waitAndGet(params, job_id, app, job, callback, sjsHome){
  function _thenIterHandling(job_id, app, job){
    function f(fullResponse2){
      var result_body = fullResponse2
      var job_status = result_body.status
      if (job_status == "FINISHED"){
        console.log("INFO: Job is finished")
        var result = result_body.result;
        if(result != null ){
          _pushToCache(job, result)
          console.log("INFO: result data output (only first 500 chars):" +
                      result.substring(0, 500));
          flag(false)
          callback()
        }
      } else if(job_status == "RUNNING"){
        console.log("INFO: Job is not yet finished. Revisiting in 3 seconds.")
        setTimeout(function(){_iter(job_id, app, job)}, 3000)
      } else if(job_status == "ERROR"){
        _pushToCache(job, result_body)
        console.log("INFO: Job returned error. Full error response is stored in cache.")
        console.log("**Original Response (500 chars): " + JSON.stringify(result_body.result).substring(0, 500))
        flag(false)
        callback()
      }
    }
    return f
  }

  function _pushToCache(job, content){
    let query_object = {app: app, input: job}
    let result_object = _.extend(_.clone(query_object), {result: content})
    let all_query_objects = cache.map(x => _.omit(x, "result"))
    let index = _.findIndex(all_query_objects, x => _.isEqual(x, query_object))
    if(index != -1){
      console.log("INFO: Deleting a result object from cache: " + JSON.stringify(query_object))
      cache.splice(index, 1)
    }
    cache.push(result_object)
  }

  // Function within __waitAndGet
  function _iter(job_id, app, job){
    uri = params['url'] + "/jobs/" + job_id
    rp({
        uri: uri,
        json: true
      }).
    then(
      _thenIterHandling(job_id, app, job)
    ).catch(
      __errorHandlingGet(params, job_id, app, job, callback, sjsHome)
    )
  }

  _iter(job_id, app, job)
}

function __sparkJobPost(params, app, job){
  var input_list = Object.keys(job).map(key => job[key])
  uri = params['url'] +
        "/jobs?appName=" + params['appName'] +
        "&classPath=" + params['classPathPrefix'] + app +
        "&context=" + params['context']
  console.log("INFO: sending " + app + " job request to Spark with " + input_list.join('---'))
  return rp({
    method: 'POST',
    uri: uri,
    headers: {
      'content-type': 'application/x-www-form-urlencoded'
    },
    form: {
      'input.string' : input_list.join('---')
    },
    resolveWithFullResponse: true,
    json: true
  })
}

submitAndGetSparkResult = function(params, app, job, callback, sjsHome){
  // Step 1: POST a job
  flag(true)
  PromisePost = __sparkJobPost(params, app, job)

  // Step 2: Ask for result by GET
  // Step 3: If result is available, push the result in cache.
  // Step 4: Then `callback` calls the next iteration.
  PromisePost.then(
    fullResponse => {
      job_id = fullResponse.body.jobId
      console.log("INFO: " + params['url'] + "/jobs/" + job_id)
      setTimeout(function(){
        console.log("INFO: after 4 sec.")
        __waitAndGet(params,
                     job_id,   // Step 2
                     app,
                     job,
                     callback, // Step 3, Step 4
                     sjsHome)
      }, 4000)
    }
  ).catch(
    __errorHandlingPost(params, app, job, callback, sjsHome)
  )
}

module.exports = submitAndGetSparkResult
<commit_msg>Revert "Remove an old, useless record from cache before pushing"<commit_after>var rp = require('request-promise')

var cache = require('./cache').cache
var flag = require('./cache').flag
var restart = require('./sjs-restart')

var cnt_early = 1
var lmt_early = 5

function __errorHandlingPost(params, app, job, callback, sjsHome){
  function f(err){
    console.log('ERROR: POST')
    if(err.message == 'Error: socket hang up' && err.response == undefined){
      console.log("WARNING: This should be an error during POST. Retrying after 10 seconds...")
      setTimeout(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), 10000)
    } else if(err.message.includes('Error: connect ECONNREFUSED') && err.response == undefined){
      console.log("ERROR: POST is continuously failing. Restarting Spark...")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    } else if (err.response.body.result){
      console.log("** Original Response** " + JSON.stringify(err.response.body))
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    } else {
      console.log("ERROR: Unknown error during POST. I think we have to restart SJS now. And then, resend a POST request.")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
    }
  }
  return f
}

function __errorHandlingGet(params, job_id, app, job, callback, sjsHome){
  function f(err){
    console.log('ERROR: GET')
    // Type 1: SJS is unstable.
    if(err.response == undefined){
      console.log("ERROR: SJS becomes unresponsive during GET. Restarting SJS...")
      restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
      cnt_early = 1
    } else if(typeof err.response.body == 'object'){
      err_result = err.response.body.result
      job_id = err.response.req.path.slice(6)
      console.log("**Original Response**: " + err_result)
      if(typeof err_result == 'object'){
        err_msg = err_result.message
        if (err_msg.includes("akka")){
          console.log("ERROR: akka Error. I think we have to restart SJS now.")
          restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
          cnt_early = 1
        }
      } else{
        if (err_result.includes("No such job ID")){
          if(cnt_early < lmt_early){
            console.log("INFO: Too-Early count: " + cnt_early)
            console.log("WARNING: Ok I see, I was just too early to ask for result. Let's wait and come back again now...")
            __waitAndGet(params, job_id, app, job, callback, sjsHome)
            cnt_early += 1
          } else {
            console.log("INFO: job-server's performance is getting bad. We'll restart SJS.")
            restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
            cnt_early = 1
          }
        }
      }
    } else { // Type 2: SJS is unstable in a different way.
      err_body = err.response.body
      console.log("**Original Response**: " + err_body)
      if(err_body.includes("Resource representation is only available with these Content-Types")){
        console.log("WARNING: Ok I see, GET is not ready yet. Wait and Get again...")
        job_id = err.response.req.path.slice(6)
        console.log(job)
        if(cnt_early < lmt_early){
          console.log("INFO: Too-Early count: " + cnt_early)
          __waitAndGet(params, job_id, app, job, callback, sjsHome)
          cnt_early += 1
        } else {
          console.log("INFO: job-server's performance is getting bad. We'll restart SJS.")
          restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
          cnt_early = 1
        }
      } else{ // In case of knowing nothing about the cause
        console.log(err.response)
        console.log("ERROR: Unknown (either on your machine or on Spark-Jobserver)")
        console.log("INFO: Let's try again.")
        restart(() => submitAndGetSparkResult(params, app, job, callback, sjsHome), sjsHome)
      }
    }
  }
  return f
}

function __waitAndGet(params, job_id, app, job, callback, sjsHome){
  function _thenIterHandling(job_id, app, job){
    function f(fullResponse2){
      var result_body = fullResponse2
      var job_status = result_body.status
      if (job_status == "FINISHED"){
        console.log("INFO: Job is finished")
        var result = result_body.result;
        if(result != null ){
          _pushToCache(job, result)
          console.log("INFO: result data output (only first 500 chars):" +
                      result.substring(0, 500));
          flag(false)
          callback()
        }
      } else if(job_status == "RUNNING"){
        console.log("INFO: Job is not yet finished. Revisiting in 3 seconds.")
        setTimeout(function(){_iter(job_id, app, job)}, 3000)
      } else if(job_status == "ERROR"){
        _pushToCache(job, result_body)
        console.log("INFO: Job returned error. Full error response is stored in cache.")
        console.log("**Original Response (500 chars): " + JSON.stringify(result_body.result).substring(0, 500))
        flag(false)
        callback()
      }
    }
    return f
  }

  function _pushToCache(job, content){
    result_object = {app: app, input: job, result: content}
    cache.push(result_object)
  }

  // Function within __waitAndGet
  function _iter(job_id, app, job){
    uri = params['url'] + "/jobs/" + job_id
    rp({
        uri: uri,
        json: true
      }).
    then(
      _thenIterHandling(job_id, app, job)
    ).catch(
      __errorHandlingGet(params, job_id, app, job, callback, sjsHome)
    )
  }

  _iter(job_id, app, job)
}

function __sparkJobPost(params, app, job){
  var input_list = Object.keys(job).map(key => job[key])
  uri = params['url'] +
        "/jobs?appName=" + params['appName'] +
        "&classPath=" + params['classPathPrefix'] + app +
        "&context=" + params['context']
  console.log("INFO: sending " + app + " job request to Spark with " + input_list.join('---'))
  return rp({
    method: 'POST',
    uri: uri,
    headers: {
      'content-type': 'application/x-www-form-urlencoded'
    },
    form: {
      'input.string' : input_list.join('---')
    },
    resolveWithFullResponse: true,
    json: true
  })
}

submitAndGetSparkResult = function(params, app, job, callback, sjsHome){
  // Step 1: POST a job
  flag(true)
  PromisePost = __sparkJobPost(params, app, job)

  // Step 2: Ask for result by GET
  // Step 3: If result is available, push the result in cache.
  // Step 4: Then `callback` calls the next iteration.
  PromisePost.then(
    fullResponse => {
      job_id = fullResponse.body.jobId
      console.log("INFO: " + params['url'] + "/jobs/" + job_id)
      setTimeout(function(){
        console.log("INFO: after 4 sec.")
        __waitAndGet(params,
                     job_id,   // Step 2
                     app,
                     job,
                     callback, // Step 3, Step 4
                     sjsHome)
      }, 4000)
    }
  ).catch(
    __errorHandlingPost(params, app, job, callback, sjsHome)
  )
}

module.exports = submitAndGetSparkResult
<|endoftext|>


<commit_before>var winston = require('winston');

var entities = require('./Entities.js')

/*
 * General game handler
 */
function Game(client1, client2) {
    this.players = [];
    this.balls = [];
    this.powerups = [];

    this.tick = 0;
    this.tick_duration = 50;

    this.init(client1, client2);

    this.ended = false;
}

Game.prototype.start = function() {
    winston.info(
        'Starting new game with ' +
        this.players.length +
        ' players!'
    );

    // say hello
    for(var i in this.players) {
        var cur = this.players[i];

        var names = [];
        for(var j in this.players) {
            if(i != j) {
                var other = this.players[j];
                names.push(other.name);
            }
        }

        cur.connection.emit('ready', {names: names});
    }

    // run the game
    var last_time = Date.now();
    var _update = this.update.bind(this);
    setInterval(_update, this.tick_duration);
}

Game.prototype.end = function(msg) {
    if(this.ended)
        return;
    this.ended = true;

    winston.info(
        'Ending game [' +
        msg +
        ']'
    );
}

Game.prototype.get_connections = function() {
    var conns = [];
    for(var i in this.players) {
        var cur = this.players[i];
        conns.push(cur.connection);
    }

    return conns;
}

Game.prototype.init = function(client1, client2) {
    // add two players to the game
    var p1 = new entities.Player(
        client1.name,
        client1.conn,
        {start: {x:0, y:0}, end: {x:0, y:480}}
    )
    var p2 = new entities.Player(
        client2.name,
        client2.conn,
        {start: {x:640-10, y:0}, end: {x:640-10, y:480}}
    )

    this.players.push(p1);
    this.players.push(p2);

    for(var i in this.players) {
        var cur = this.players[i];

        cur.snake.init(
            this,
            cur.goal_line.start.x,
            (cur.goal_line.end.y-cur.goal_line.start.y)/2
        );
    }

    // add one ball
    var ball = new entities.Ball(this, 640/2-5, 480/2-5, 3, 3);

    this.balls.push(ball);
}

Game.prototype.update = function() {
    // move all balls
    for(var i in this.balls) {
        cur = this.balls[i];

        cur.update(1);
    }

    // move snakes
    for(var i in this.players) {
        cur = this.players[i].snake;

        cur.update(1);
    }

    // check for goal
    for(var i in this.players) {
        p = this.players[i];

        for(var i in this.balls) {
            b = this.balls[i];

            if(p.in_goal(b)) {
                p.increase_score(1);

                b.reset();
            }
        }
    }

    this.tick++;
}

/*
 * Exports
 */
module.exports.Game = Game;<commit_msg>Fixed score system<commit_after>var winston = require('winston');

var entities = require('./Entities.js')

/*
 * General game handler
 */
function Game(client1, client2) {
    this.players = [];
    this.balls = [];
    this.powerups = [];

    this.tick = 0;
    this.tick_duration = 50;

    this.init(client1, client2);

    this.ended = false;
}

Game.prototype.start = function() {
    winston.info(
        'Starting new game with ' +
        this.players.length +
        ' players!'
    );

    // say hello
    for(var i in this.players) {
        var cur = this.players[i];

        var names = [];
        for(var j in this.players) {
            if(i != j) {
                var other = this.players[j];
                names.push(other.name);
            }
        }

        cur.connection.emit('ready', {names: names});
    }

    // run the game
    var last_time = Date.now();
    var _update = this.update.bind(this);
    setInterval(_update, this.tick_duration);
}

Game.prototype.end = function(msg) {
    if(this.ended)
        return;
    this.ended = true;

    winston.info(
        'Ending game [' +
        msg +
        ']'
    );
}

Game.prototype.get_connections = function() {
    var conns = [];
    for(var i in this.players) {
        var cur = this.players[i];
        conns.push(cur.connection);
    }

    return conns;
}

Game.prototype.init = function(client1, client2) {
    // add two players to the game
    var p1 = new entities.Player(
        client1.name,
        client1.conn,
        {start: {x:0, y:0}, end: {x:0, y:480}}
    )
    var p2 = new entities.Player(
        client2.name,
        client2.conn,
        {start: {x:640-10, y:0}, end: {x:640-10, y:480}}
    )

    this.players.push(p1);
    this.players.push(p2);

    for(var i in this.players) {
        var cur = this.players[i];

        cur.snake.init(
            this,
            cur.goal_line.start.x,
            (cur.goal_line.end.y-cur.goal_line.start.y)/2
        );
    }

    // add one ball
    var ball = new entities.Ball(this, 640/2-5, 480/2-5, 3, 3);

    this.balls.push(ball);
}

Game.prototype.update = function() {
    // move all balls
    for(var i in this.balls) {
        cur = this.balls[i];

        cur.update(1);
    }

    // move snakes
    for(var i in this.players) {
        cur = this.players[i].snake;

        cur.update(1);
    }

    // check for goal
    for(var i = 0 ; i <= 1 ; i++) {
        for(var j in this.balls) {
            var b = this.balls[j];
            if(this.players[i].in_goal(b)) {
                this.players[(i+1)%2].increase_score(1);
                b.reset();
            }
        }
    }

    this.tick++;
}

/*
 * Exports
 */
module.exports.Game = Game;<|endoftext|>


<commit_before>/**
 * Time tracking library for Node.js.
 *
 * Written by Natanael Log
 *
 * MIT Licence
 */

var async = require('async');

var doc;
var TRACK_SPLIT_INTERVAL = 2; // max number of hours a track can be before split
var MINUTE_ROUND_MULTIPLE = 5; // the multiple on which to round up the minutes to before tracking

var validDateDashFormats = [
    /^\d{4}[-]\d{1,2}[-]\d{1,2}$/,
    /^\d{2}[-]\d{1,2}[-]\d{1,2}$/
];

var validDateSlashFormats = [
    /^\d{1,2}[\/]\d{1,2}[\/]\d{2}$/,
    /^\d{1,2}[\/]\d{1,2}[\/]\d{4}$/
];

var validTimeFormats = [
    /^\d{1,2}$/,
    /^\d{1,2}[:]\d{1,2}$/,
    /^\d{1,2}[:]\d{1,2}[:]\d{1,2}$/
];

exports.init = function(token, credentials, done) {
  var GoogleSpreadSheet = require('google-spreadsheet');

  doc = new GoogleSpreadSheet(token);
  doc.useServiceAccountAuth(credentials, done);
};

exports.status = function(user, done) {
  getSheet(user, function(err, sheet, rows) {
    if (err) {
      done(err);
    }
    else {
      try {
	done(null, {
	  total: sumTotalTime(rows),
	  latestDate: getLatestDate(rows),
	  latestDescription: getLatestDescription(rows)
	});
      }
      catch (er) {
	done(er);
      }
    }
  });
};

exports.track = function(user, date, from, to, description, done) {
  user = user;
  date = date || 'today';
  from = from || 'latest';
  to = to || 'now';
  description = description;

  getSheet(user, function(err, sheet, rows) {
    if (err) {
      done(err);
    }
    else {
      try {
	if (from == 'latest') {
	  from = getLatestDate(rows);
	}
	else {
	  from = convertToDate(date, from);
	}
	to = convertToDate(date, to);
	addTrack(sheet, from, to, description, done);
      }
      catch (er) {
	done(er);
      }
    }
  });
};

function getSheet(user, done) {
  doc.getInfo(function(err, info) {
    if (err) {
      done(err);
    }
    else {
      var sheet;

      for (var i = 0; i < info.worksheets.length; ++i) {
	if (info.worksheets[i].title.toLowerCase() == user.toLowerCase()) {
	  sheet = info.worksheets[i];
	}
      }

      if (!sheet) {
	done(new Error('User not found in spreadsheet'));
      }
      else {
	sheet.getRows({}, function(err, rows) {
	  if (err) {
	    done(err);
	  }
	  else {
	    done(null, sheet, rows);
	  }
	});
      }
    }
  });
}

function addTrack(sheet, t1, t2, description, done) {
  var t1Hours = t1.getHours();
  var t1Minutes = t1.getMinutes();
  var t2Hours = t2.getHours();
  var t2Minutes = roundUpToMultiple(t2.getMinutes());
  var year = t1.getFullYear();
  var month = t1.getMonth() + 1; // month index goes from 0-11
  var day = t1.getDate();

  // make sure t1 and t2 are on the same day
  if (t1.getFullYear() != t2.getFullYear() ||
      t1.getMonth() != t2.getMonth() ||
      t1.getDate() != t2.getDate()) {
    done(new Error('Because of tight regulations, I can not allow you to track all the way from yesterday.'));
  }
  // make sure t2 is comes after t1
  else if (t2 <= t1) {
    done(new Error('My creator is working full time on the time vortex, but until then, please try to track from t1 to t2, where t1 < t2.'));
  }
  else {
    async.series([
      function addRow(step) {
	sheet.addRow({
	  Date: '=DATE(' + year + ',' + month + ',' + day + ')',
	  Start: '=TIME(' + t1Hours + ',' + t1Minutes + ',0)',
	  End: '=TIME(' + t2Hours + ',' + t2Minutes + ',0)',
	  Total: '=INDIRECT(ADDRESS(ROW(), COLUMN()-1, 4)) - INDIRECT(ADDRESS(ROW(), COLUMN()-2, 4))',
	  Description: description
	}, step);
      },

      function getRow(step) {
	sheet.getRows({}, function(err, rows) {
	  if (err) {
	    done(err);
	  }
	  else {
	    var row = rows[rows.length - 1];
	    done(null, {
	      start: convertToDate(row.date, row.start),
	      end: convertToDate(row.date, row.end),
	      description: description,
	    });
	  }
	});
      }
    ]);
  }
}

function roundUpToMultiple(minute) {
  return Math.ceil(minute / MINUTE_ROUND_MULTIPLE) * MINUTE_ROUND_MULTIPLE;
}

function getLatestDate(rows) {
  if (rows.length === 0) {
    throw new Error('There are no dates to find the latest from!');
  }
  else {
    rows.sort(dateComparator);
    return convertToDate(rows[0].date, rows[0].end);
  }
}

function getLatestDescription(rows) {
  if (rows.length === 0) {
    throw new Error('All the descriptions are GONE! Yikes!');
  }
  else {
    rows.sort(dateComparator);
    return rows[0].description;
  }
}

function dateComparator(row1, row2) {
  var date1 = convertToDate(row1.date, row1.end);
  var date2 = convertToDate(row2.date, row2.end);

  return date2 - date1;
}

function sumTotalTime(rows) {
  var hours = 0;
  var minutes = 0;

  rows.forEach(function(row) {
    var date = convertToDate(row.date, row.total);
    hours += date.getHours();
    minutes += date.getMinutes();
  });

  return {
    hours: hours + Math.floor(minutes / 60),
    minutes: minutes % 60
  };
}

function convertToDate(dateString, timeString) {
  var date, year, month, day, hour, minute;

  if (dateString == 'today') {
    date = new Date();
    day = date.getDate();
    month = date.getMonth();
    year = date.getFullYear();
  }
  else if (dateString == 'yesterday') {
    date = new Date();
    date.setDate(date.getDate() - 1);
    day = date.getDate();
    month = date.getMonth();
    year = date.getFullYear();
  }
  // date format is DD/MM/YYYY
  else if (isValidFormat(dateString, validDateSlashFormats)) {
    date = dateString.split('/');
    day = date[0];
    month = date[1] - 1;
    year = date[2];
  }
  // date format is YYYY-MM-DD
  else if (isValidFormat(dateString, validDateDashFormats)) {
    date = dateString.split('-');
    year = date[0];
    month = date[1];
    day = date[2];
  }
  else {
    throw new Error('Sweet = DD/MM/YY or YY-MM-DD, not ' + dateString);
  }

  if (timeString == 'now') {
    date = new Date();
    hour = date.getHours();
    minute = date.getMinutes();
  }
  else if (isValidFormat(timeString, validTimeFormats)) {
    var time = timeString.split(':');
    hour = time[0];
    minute = time[1] || 0;
  }
  else {
    throw new Error('I\'m terribly sorry, but ' + timeString + ' does not look like a sweet time format to me.');
  }

  return new Date(year, month, day, hour, minute);
}

function isValidFormat(text, validFormats) {
  for (var i = 0; i < validFormats.length; ++i) {
    if (validFormats[i].test(text)) {
      return true;
    }
  }
  return false;
}

exports.convertToDate = convertToDate;
module.exports = exports;

<commit_msg>Make sumTotalTime a little bit faster<commit_after>/**
 * Time tracking library for Node.js.
 *
 * Written by Natanael Log
 *
 * MIT Licence
 */

var async = require('async');

var doc;
var TRACK_SPLIT_INTERVAL = 2; // max number of hours a track can be before split
var MINUTE_ROUND_MULTIPLE = 5; // the multiple on which to round up the minutes to before tracking

var validDateDashFormats = [
    /^\d{4}[-]\d{1,2}[-]\d{1,2}$/,
    /^\d{2}[-]\d{1,2}[-]\d{1,2}$/
];

var validDateSlashFormats = [
    /^\d{1,2}[\/]\d{1,2}[\/]\d{2}$/,
    /^\d{1,2}[\/]\d{1,2}[\/]\d{4}$/
];

var validTimeFormats = [
    /^\d{1,2}$/,
    /^\d{1,2}[:]\d{1,2}$/,
    /^\d{1,2}[:]\d{1,2}[:]\d{1,2}$/
];

exports.init = function(token, credentials, done) {
  var GoogleSpreadSheet = require('google-spreadsheet');

  doc = new GoogleSpreadSheet(token);
  doc.useServiceAccountAuth(credentials, done);
};

exports.status = function(user, done) {
  getSheet(user, function(err, sheet, rows) {
    if (err) {
      done(err);
    }
    else {
      try {
	done(null, {
	  total: sumTotalTime(rows),
	  latestDate: getLatestDate(rows),
	  latestDescription: getLatestDescription(rows)
	});
      }
      catch (er) {
	done(er);
      }
    }
  });
};

exports.track = function(user, date, from, to, description, done) {
  user = user;
  date = date || 'today';
  from = from || 'latest';
  to = to || 'now';
  description = description;

  getSheet(user, function(err, sheet, rows) {
    if (err) {
      done(err);
    }
    else {
      try {
	if (from == 'latest') {
	  from = getLatestDate(rows);
	}
	else {
	  from = convertToDate(date, from);
	}
	to = convertToDate(date, to);
	addTrack(sheet, from, to, description, done);
      }
      catch (er) {
	done(er);
      }
    }
  });
};

function getSheet(user, done) {
  doc.getInfo(function(err, info) {
    if (err) {
      done(err);
    }
    else {
      var sheet;

      for (var i = 0; i < info.worksheets.length; ++i) {
	if (info.worksheets[i].title.toLowerCase() == user.toLowerCase()) {
	  sheet = info.worksheets[i];
	}
      }

      if (!sheet) {
	done(new Error('User not found in spreadsheet'));
      }
      else {
	sheet.getRows({}, function(err, rows) {
	  if (err) {
	    done(err);
	  }
	  else {
	    done(null, sheet, rows);
	  }
	});
      }
    }
  });
}

function addTrack(sheet, t1, t2, description, done) {
  var t1Hours = t1.getHours();
  var t1Minutes = t1.getMinutes();
  var t2Hours = t2.getHours();
  var t2Minutes = roundUpToMultiple(t2.getMinutes());
  var year = t1.getFullYear();
  var month = t1.getMonth() + 1; // month index goes from 0-11
  var day = t1.getDate();

  // make sure t1 and t2 are on the same day
  if (t1.getFullYear() != t2.getFullYear() ||
      t1.getMonth() != t2.getMonth() ||
      t1.getDate() != t2.getDate()) {
    done(new Error('Because of tight regulations, I can not allow you to track all the way from yesterday.'));
  }
  // make sure t2 is comes after t1
  else if (t2 <= t1) {
    done(new Error('My creator is working full time on the time vortex, but until then, please try to track from t1 to t2, where t1 < t2.'));
  }
  else {
    async.series([
      function addRow(step) {
	sheet.addRow({
	  Date: '=DATE(' + year + ',' + month + ',' + day + ')',
	  Start: '=TIME(' + t1Hours + ',' + t1Minutes + ',0)',
	  End: '=TIME(' + t2Hours + ',' + t2Minutes + ',0)',
	  Total: '=INDIRECT(ADDRESS(ROW(), COLUMN()-1, 4)) - INDIRECT(ADDRESS(ROW(), COLUMN()-2, 4))',
	  Description: description
	}, step);
      },

      function getRow(step) {
	sheet.getRows({}, function(err, rows) {
	  if (err) {
	    done(err);
	  }
	  else {
	    var row = rows[rows.length - 1];
	    done(null, {
	      start: convertToDate(row.date, row.start),
	      end: convertToDate(row.date, row.end),
	      description: description,
	    });
	  }
	});
      }
    ]);
  }
}

function roundUpToMultiple(minute) {
  return Math.ceil(minute / MINUTE_ROUND_MULTIPLE) * MINUTE_ROUND_MULTIPLE;
}

function getLatestDate(rows) {
  if (rows.length === 0) {
    throw new Error('There are no dates to find the latest from!');
  }
  else {
    rows.sort(dateComparator);
    return convertToDate(rows[0].date, rows[0].end);
  }
}

function getLatestDescription(rows) {
  if (rows.length === 0) {
    throw new Error('All the descriptions are GONE! Yikes!');
  }
  else {
    rows.sort(dateComparator);
    return rows[0].description;
  }
}

function dateComparator(row1, row2) {
  var date1 = convertToDate(row1.date, row1.end);
  var date2 = convertToDate(row2.date, row2.end);

  return date2 - date1;
}

function sumTotalTime(rows) {
  var hours = 0;
  var minutes = 0;

  rows.forEach(function(row) {
    var total = row.total.split(':');

    hours += parseInt(total[0]);
    minutes += parseInt(total[1]);
  });

  return {
    hours: hours + Math.floor(minutes / 60),
    minutes: minutes % 60
  };
}

function convertToDate(dateString, timeString) {
  var date, year, month, day, hour, minute;

  if (dateString == 'today') {
    date = new Date();
    day = date.getDate();
    month = date.getMonth();
    year = date.getFullYear();
  }
  else if (dateString == 'yesterday') {
    date = new Date();
    date.setDate(date.getDate() - 1);
    day = date.getDate();
    month = date.getMonth();
    year = date.getFullYear();
  }
  // date format is DD/MM/YYYY
  else if (isValidFormat(dateString, validDateSlashFormats)) {
    date = dateString.split('/');
    day = date[0];
    month = date[1] - 1;
    year = date[2];
  }
  // date format is YYYY-MM-DD
  else if (isValidFormat(dateString, validDateDashFormats)) {
    date = dateString.split('-');
    year = date[0];
    month = date[1];
    day = date[2];
  }
  else {
    throw new Error('Sweet = DD/MM/YY or YY-MM-DD, not ' + dateString);
  }

  if (timeString == 'now') {
    date = new Date();
    hour = date.getHours();
    minute = date.getMinutes();
  }
  else if (isValidFormat(timeString, validTimeFormats)) {
    var time = timeString.split(':');
    hour = time[0];
    minute = time[1] || 0;
  }
  else {
    throw new Error('I\'m terribly sorry, but ' + timeString + ' does not look like a sweet time format to me.');
  }

  return new Date(year, month, day, hour, minute);
}

function isValidFormat(text, validFormats) {
  for (var i = 0; i < validFormats.length; ++i) {
    if (validFormats[i].test(text)) {
      return true;
    }
  }
  return false;
}

exports.convertToDate = convertToDate;
module.exports = exports;

<|endoftext|>


<commit_before>// Load in library
var expect = require('chai').expect;
var mustache = require('mustache');
var reverseMustache = require('../');

// Define helper utilities
var reverseMustacheUtils = {
  save: function (params) {
    before(function callReverseMustache () {
      this.result = reverseMustache(params);

      // For behavioral sanity, verify `result + template` matches `content`
      if (this.result) {
        var actualOutput = mustache.render(params.template, this.result.tokensByName);
        expect(actualOutput).to.equal(params.content);
      }
    });
    after(function cleanupResult () {
      delete this.result;
    });
  }
};

// TODO: Embrace the same flat file mantra as mustache.js
// TODO: We should be able to reverse the mustache test suite

describe('A mustache template without any mustache tokens', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result).to.be.an('object');
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello world',
      content: 'hello moon'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with a terminating conditional token', function () {
  describe('when reversed with matching content (boolean true)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}moon{{/world}}',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: true});
    });
  });

  describe('when reversed with matching content (boolean false)', function () {
    reverseMustacheUtils.save({
      template: 'hello{{#world}} moon{{/world}}',
      content: 'hello'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: false});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}moon{{/world}}',
      content: 'hello there'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an inline conditional token', function () {
  describe('when reversed with matching content (boolean true)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}there{{/world}} moon',
      content: 'hello there moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: true});
    });
  });

  // DEV: This is the first instance of where we need to backtrack
  describe('when reversed with matching content (boolean false)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}} there{{/world}}moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: false});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}there{{/world}} moon',
      content: 'hello there'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with a variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{place}}',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({place: 'moon'});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{place}}',
      content: 'hello'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an inline variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello there moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({where: 'there '});
    });
  });

  describe('when reversed with matching content (empty string var)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({where: ''});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello mooon'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an escaped variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{operator}} moon',
      content: 'hello > moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({operator: '>'});
    });
  });
});

describe('A mustache template with an unescaped variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{{operator}}} moon',
      content: 'hello > moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({operator: '>'});
    });
  });
});
<commit_msg>Created test for array loops<commit_after>// Load in library
var expect = require('chai').expect;
var mustache = require('mustache');
var reverseMustache = require('../');

// Define helper utilities
var reverseMustacheUtils = {
  save: function (params) {
    before(function callReverseMustache () {
      this.result = reverseMustache(params);

      // For behavioral sanity, verify `result + template` matches `content`
      if (this.result) {
        var actualOutput = mustache.render(params.template, this.result.tokensByName);
        expect(actualOutput).to.equal(params.content);
      }
    });
    after(function cleanupResult () {
      delete this.result;
    });
  }
};

// TODO: Embrace the same flat file mantra as mustache.js
// TODO: We should be able to reverse the mustache test suite

describe('A mustache template without any mustache tokens', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result).to.be.an('object');
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello world',
      content: 'hello moon'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with a terminating conditional token', function () {
  describe('when reversed with matching content (boolean true)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}moon{{/world}}',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: true});
    });
  });

  describe('when reversed with matching content (boolean false)', function () {
    reverseMustacheUtils.save({
      template: 'hello{{#world}} moon{{/world}}',
      content: 'hello'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: false});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}moon{{/world}}',
      content: 'hello there'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an inline conditional token', function () {
  describe('when reversed with matching content (boolean true)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}there{{/world}} moon',
      content: 'hello there moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: true});
    });
  });

  // DEV: This is the first instance of where we need to backtrack
  describe('when reversed with matching content (boolean false)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}} there{{/world}}moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({world: false});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#world}}there{{/world}} moon',
      content: 'hello there'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with a variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{place}}',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({place: 'moon'});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{place}}',
      content: 'hello'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an inline variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello there moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({where: 'there '});
    });
  });

  describe('when reversed with matching content (empty string var)', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({where: ''});
    });
  });

  describe('when reversed with non-matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{where}}moon',
      content: 'hello mooon'
    });

    it('returns `null`', function () {
      expect(this.result).to.equal(null);
    });
  });
});

describe('A mustache template with an escaped variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{operator}} moon',
      content: 'hello > moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({operator: '>'});
    });
  });
});

describe('A mustache template with an unescaped variable', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{{operator}}} moon',
      content: 'hello > moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.deep.equal({operator: '>'});
    });
  });
});

describe('A mustache template with an array loop', function () {
  describe('when reversed with matching content', function () {
    reverseMustacheUtils.save({
      template: 'hello {{#places}}moon{{/places}}',
      content: 'hello moon moon'
    });

    it('returns meta information', function () {
      expect(this.result).to.not.equal(null);
      expect(this.result.tokensByName).to.have.property('place');
      expect(this.result.tokensByName.place).to.have.property('length', 2);
    });
  });
});
<|endoftext|>


<commit_before>const settings = require('./../../settings.json');
const moment = require('moment');
/**
 * This method should return the response directly to the channel
 * @param {*string array} params
 * @param {*message} message
 */
const access_log = require('./../commands').access_log;
async function command(params, message) {
    if (settings.owners.includes(message.author.id)) {
        let user = message.mentions.users.first();
        if (!user || user == null) {
            await message.reply('Invalid Arguments')
            return false;
        }
        let logs_for_user = access_log.filter(ac => ac.author == user.id);
        if (logs_for_user.length < 1) {
            await message.reply("He hasn't used any bot commands since " + moment(access_log[0].time).fromNow());
        }
        else {
            let res = `${user.toString()} has used ${logs_for_user.length} since ${moment(access_log[0].time).fromNow()}`;
            logs_for_user.map((l) => {
                res = res + "\n" + moment(l.time) + " - ~" + l.command + " " + ((l.sucess) ? "Sucess" : "Failed") + ((l.error) ? " because of error" : "") + ((l.channel) ? ` - <#${l.channel}>` : "")
            })
            await message.channel.send(res, { split: true, code: 'xl' });
        }
        return true;
    }
    else {
        await message.reply('https://media.tenor.com/images/c472d1ee8c75a50f700bd028cc1b10b9/tenor.gif')
        return false;
    }
}
/**
 * description of the command
 */
const description = "Shows access log for a user";
/**
 * Define Exports
 */
module.exports = {
    execute: command,
    description: description,
    owner: true
};
<commit_msg>Update access_log.js<commit_after>const settings = require('./../../settings.json');
const moment = require('moment');
/**
 * This method should return the response directly to the channel
 * @param {*string array} params
 * @param {*message} message
 */
const access_log = require('./../commands').access_log;
async function command(params, message) {
    if (settings.owners.includes(message.author.id)) {
        let user = message.mentions.users.first();
        if (!user || user == null) {
            await message.reply('Invalid Arguments')
            return false;
        }
        if(!access_log)
        {
            await message.reply('access_Log is undefined')
            return false;
        }
        let logs_for_user = access_log.filter(ac => ac.author == user.id);
        if (logs_for_user.length < 1) {
            await message.reply("He hasn't used any bot commands since " + moment(access_log[0].time).fromNow());
        }
        else {
            let res = `${user.toString()} has used ${logs_for_user.length} since ${moment(access_log[0].time).fromNow()}`;
            logs_for_user.map((l) => {
                res = res + "\n" + moment(l.time) + " - ~" + l.command + " " + ((l.sucess) ? "Sucess" : "Failed") + ((l.error) ? " because of error" : "") + ((l.channel) ? ` - <#${l.channel}>` : "")
            })
            await message.channel.send(res, { split: true, code: 'xl' });
        }
        return true;
    }
    else {
        await message.reply('https://media.tenor.com/images/c472d1ee8c75a50f700bd028cc1b10b9/tenor.gif')
        return false;
    }
}
/**
 * description of the command
 */
const description = "Shows access log for a user";
/**
 * Define Exports
 */
module.exports = {
    execute: command,
    description: description,
    owner: true
};
<|endoftext|>


<commit_before>const webpack = require('webpack');

const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const WebpackNotifierPlugin = require('webpack-notifier');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin');
const GenerateJsonPlugin = require('generate-json-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const csp = require('./content-security-policy');
const PacktrackerPlugin = require('@packtracker/webpack-plugin');

const Visualizer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const NotifyPlugin = require('notify-webpack-plugin');

const ASSET_NAME_PATTERN = 'static/[name]-[hash:6].[ext]';

const packageJson = require('../package.json');

const splash = require('../icons/splash.json');

module.exports = (env) => {
  if (process.env.WEBPACK_SERVE) {
    env.name = 'dev';
    if (!fs.existsSync('key.pem') || !fs.existsSync('cert.pem')) {
      console.log('Generating certificate');
      execSync('mkcert create-ca --validity 3650');
      execSync('mkcert create-cert --validity 3650 --key key.pem --cert cert.pem');
    }
  }

  ['release', 'beta', 'dev'].forEach((e) => {
    // set booleans based on env.name
    env[e] = e == env.name;
  });

  let version = packageJson.version.toString();
  if (env.beta && process.env.TRAVIS_BUILD_NUMBER) {
    version += `.${process.env.TRAVIS_BUILD_NUMBER}`;
  }

  const config = {
    mode: env.dev ? 'development' : 'production',

    entry: {
      main: './src/Index.tsx',
      browsercheck: './src/browsercheck.js',
      authReturn: './src/authReturn.ts',
      gdriveReturn: './src/gdriveReturn.ts'
    },

    output: {
      path: path.resolve('./dist'),
      publicPath: '/',
      filename: env.dev ? '[name]-[hash].js' : '[name]-[contenthash:6].js',
      chunkFilename: env.dev ? '[name]-[hash].js' : '[name]-[contenthash:6].js',
      futureEmitAssets: true
    },

    // Dev server
    serve: process.env.WEBPACK_SERVE
      ? {
          host: process.env.DOCKER ? '0.0.0.0' : 'localhost',
          devMiddleware: {
            stats: 'errors-only'
          },
          https: {
            key: fs.readFileSync('key.pem'), // Private keys in PEM format.
            cert: fs.readFileSync('cert.pem') // Cert chains in PEM format.
          }
        }
      : {},

    // Bail and fail hard on first error
    bail: !env.dev,

    stats: env.dev ? 'minimal' : 'normal',

    devtool: 'source-map',

    performance: {
      // Don't warn about too-large chunks
      hints: false
    },

    optimization: {
      // We always want the chunk name, otherwise it's just numbers
      namedChunks: true,
      // Extract the runtime into a separate chunk
      runtimeChunk: 'single',
      splitChunks: {
        chunks: 'all',
        automaticNameDelimiter: '-'
      },
      minimizer: [
        new TerserPlugin({
          cache: true,
          parallel: true,
          terserOptions: {
            ecma: 8,
            module: true,
            compress: { warnings: false, passes: 3, toplevel: true },
            mangle: { safari10: true, toplevel: true },
            output: { safari10: true }
          },
          sourceMap: true
        })
      ]
    },

    module: {
      strictExportPresence: true,

      rules: [
        {
          test: /\.js$/,
          exclude: [/node_modules/],
          loader: 'babel-loader',
          options: {
            cacheDirectory: true
          }
        },
        {
          test: /\.html$/,
          exclude: /index\.html/,
          loader: 'html-loader',
          options: {
            exportAsEs6Default: true,
            minimize: true
          }
        },
        {
          test: /\.(jpg|gif|png|eot|svg|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/,
          loader: 'url-loader',
          options: {
            limit: 5 * 1024, // only inline if less than 5kb
            name: ASSET_NAME_PATTERN
          }
        },
        // *.m.scss will have CSS Modules support
        {
          test: /\.m\.scss$/,
          use: [
            env.dev ? 'style-loader' : MiniCssExtractPlugin.loader,
            {
              loader: 'css-modules-typescript-loader',
              options: {
                mode: process.env.CI ? 'verify' : 'emit'
              }
            },
            {
              loader: 'css-loader',
              options: {
                modules: {
                  localIdentName: env.dev ? '[name]_[local]-[hash:base64:5]' : '[hash:base64:5]'
                },
                localsConvention: 'camelCaseOnly',
                sourceMap: true
              }
            },
            'postcss-loader',
            'sass-loader'
          ]
        },
        // Regular *.scss are global
        {
          test: /\.scss$/,
          exclude: /\.m\.scss$/,
          use: [
            env.dev ? 'style-loader' : MiniCssExtractPlugin.loader,
            {
              loader: 'css-loader',
              options: {
                sourceMap: true
              }
            },
            'postcss-loader',
            'sass-loader'
          ]
        },
        {
          test: /\.css$/,
          use: [env.dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader']
        },
        // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
        {
          test: /\.tsx?$/,
          use: [
            {
              loader: 'babel-loader',
              options: {
                cacheDirectory: true
              }
            },
            {
              loader: 'ts-loader'
            }
          ]
        },
        // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
        {
          enforce: 'pre',
          test: /\.jsx?$/,
          loader: 'source-map-loader'
        },
        {
          type: 'javascript/auto',
          test: /\.json/,
          include: /src(\/|\\)locale/,
          use: [
            {
              loader: 'file-loader',
              options: { name: '[name]-[hash:6].[ext]' }
            }
          ]
        },
        {
          type: 'javascript/auto',
          test: /\.wasm/
        },
        {
          test: /CHANGELOG\.md$/,
          loader: path.resolve('./config/changelog-loader')
        }
      ],

      noParse: function(path) {
        return false;
      }
    },

    resolve: {
      extensions: ['.js', '.json', '.ts', '.tsx', '.jsx'],

      alias: {
        app: path.resolve('./src/app/'),
        data: path.resolve('./src/data/'),
        images: path.resolve('./src/images/'),
        'destiny-icons': path.resolve('./destiny-icons/'),
        'idb-keyval': path.resolve('./src/app/storage/idb-keyval.ts')
      }
    },

    plugins: [
      new CaseSensitivePathsPlugin(),

      new webpack.IgnorePlugin(/caniuse-lite\/data\/regions/),

      new NotifyPlugin('DIM', !env.dev),

      new MiniCssExtractPlugin({
        filename: env.dev ? '[name]-[hash].css' : '[name]-[contenthash:6].css',
        chunkFilename: env.dev ? '[name]-[hash].css' : '[id]-[contenthash:6].css'
      }),

      // Fix some chunks not showing up in Webpack 4
      new HtmlWebpackIncludeSiblingChunksPlugin(),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'index.html',
        template: 'src/index.html',
        chunks: ['main', 'browsercheck'],
        templateParameters: {
          splash
        }
      }),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'return.html',
        template: '!html-loader!src/return.html',
        chunks: ['authReturn']
      }),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'gdrive-return.html',
        template: '!html-loader!src/gdrive-return.html',
        chunks: ['gdriveReturn']
      }),

      // Generate the .htaccess file (kind of an abuse of HtmlWebpack plugin just for templating)
      new HtmlWebpackPlugin({
        filename: '.htaccess',
        template: 'src/htaccess',
        inject: false,
        templateParameters: {
          csp: csp(env.name)
        }
      }),

      // Generate a version info JSON file we can poll. We could theoretically add more info here too.
      new GenerateJsonPlugin('./version.json', {
        version
      }),

      new CopyWebpackPlugin([
        { from: './src/manifest-webapp-6-2018.json' },
        // Only copy the manifests out of the data folder. Everything else we import directly into the bundle.
        { from: './src/data/d1/manifests', to: 'data/d1/manifests' },
        { from: `./icons/${env.name}/` },
        { from: `./icons/splash`, to: 'splash/' },
        { from: './src/safari-pinned-tab.svg' }
      ]),

      new webpack.DefinePlugin({
        $DIM_VERSION: JSON.stringify(version),
        $DIM_FLAVOR: JSON.stringify(env.name),
        $DIM_BUILD_DATE: JSON.stringify(Date.now()),
        // These are set from the Travis repo settings instead of .travis.yml
        $DIM_WEB_API_KEY: JSON.stringify(process.env.WEB_API_KEY),
        $DIM_WEB_CLIENT_ID: JSON.stringify(process.env.WEB_OAUTH_CLIENT_ID),
        $DIM_WEB_CLIENT_SECRET: JSON.stringify(process.env.WEB_OAUTH_CLIENT_SECRET),

        $GOOGLE_DRIVE_CLIENT_ID: JSON.stringify(
          '22022180893-raop2mu1d7gih97t5da9vj26quqva9dc.apps.googleusercontent.com'
        ),

        $BROWSERS: JSON.stringify(packageJson.browserslist),

        // Feature flags!

        // Print debug info to console about item moves
        '$featureFlags.debugMoves': JSON.stringify(!env.release),
        '$featureFlags.reviewsEnabled': JSON.stringify(true),
        // Sync data over gdrive
        '$featureFlags.gdrive': JSON.stringify(true),
        '$featureFlags.debugSync': JSON.stringify(!env.release),
        // Enable color-blind a11y
        '$featureFlags.colorA11y': JSON.stringify(true),
        // Whether to log page views for router events
        '$featureFlags.googleAnalyticsForRouter': JSON.stringify(true),
        // Debug ui-router
        '$featureFlags.debugRouter': JSON.stringify(false),
        // Debug Service Worker
        '$featureFlags.debugSW': JSON.stringify(!env.release),
        // Send exception reports to Sentry.io on beta only
        '$featureFlags.sentry': JSON.stringify(env.beta),
        // Respect the "do not track" header
        '$featureFlags.respectDNT': JSON.stringify(!env.release),
        // Forsaken Item Tiles
        '$featureFlags.forsakenTiles': JSON.stringify(!env.release),
        // Community-curated wish lists
        '$featureFlags.wishLists': JSON.stringify(true)
      }),

      new LodashModuleReplacementPlugin({
        collections: true,
        memoizing: true,
        shorthands: true,
        flattening: true
      }),

      new webpack.WatchIgnorePlugin([/scss\.d\.ts$/])
    ],

    node: {
      fs: 'empty',
      net: 'empty',
      tls: 'empty'
    }
  };

  // Enable if you want to debug the size of the chunks
  if (env.WEBPACK_VISUALIZE) {
    config.plugins.push(new Visualizer());
  }

  if (env.dev) {
    config.plugins.push(
      new WebpackNotifierPlugin({
        title: 'DIM',
        alwaysNotify: true,
        contentImage: path.join(__dirname, '../icons/release/favicon-96x96.png')
      })
    );

    config.module.rules.push({
      test: /\.jsx?$/,
      include: /node_modules/,
      use: ['react-hot-loader/webpack']
    });
  } else {
    config.plugins.push(
      new CleanWebpackPlugin({
        cleanOnceBeforeBuildPatterns: ['.awcache', 'node_modules/.cache']
      }),

      // Tell React we're in Production mode
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production'),
        'process.env': JSON.stringify({ NODE_ENV: 'production' })
      }),

      new CopyWebpackPlugin([
        { from: './src/android-config.json', to: '.well-known/assetlinks.json' }
      ]),

      // Generate a service worker
      new InjectManifest({
        maximumFileSizeToCacheInBytes: 5000000,
        include: [/\.(html|js|css|woff2|json|wasm)$/, /static\/.*\.(png|gif|jpg|svg)$/],
        exclude: [
          /version\.json/,
          /extension-dist/,
          /\.map$/,
          // Ignore both the webapp manifest and the d1-manifest files
          /data\/d1\/manifests/,
          /manifest-webapp/
        ],
        swSrc: './src/service-worker.js',
        swDest: 'service-worker.js',
        importWorkboxFrom: 'local',
        dontCacheBustUrlsMatching: /-[a-f0-9]{6}\./
      })
    );

    if (process.env.PT_PROJECT_TOKEN) {
      const packOptions = {
        upload: true,
        fail_build: true
      };

      if (process.env.TRAVIS === 'true') {
        Object.assign(packOptions, {
          branch: process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH,
          commit: process.env.TRAVIS_PULL_REQUEST_SHA || process.env.TRAVIS_COMMIT
        });
      }

      config.plugins.push(new PacktrackerPlugin(packOptions));
    }
  }

  return config;
};
<commit_msg>code block for release only<commit_after>const webpack = require('webpack');

const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const WebpackNotifierPlugin = require('webpack-notifier');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin');
const GenerateJsonPlugin = require('generate-json-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const csp = require('./content-security-policy');
const PacktrackerPlugin = require('@packtracker/webpack-plugin');

const Visualizer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const NotifyPlugin = require('notify-webpack-plugin');

const ASSET_NAME_PATTERN = 'static/[name]-[hash:6].[ext]';

const packageJson = require('../package.json');

const splash = require('../icons/splash.json');

module.exports = (env) => {
  if (process.env.WEBPACK_SERVE) {
    env.name = 'dev';
    if (!fs.existsSync('key.pem') || !fs.existsSync('cert.pem')) {
      console.log('Generating certificate');
      execSync('mkcert create-ca --validity 3650');
      execSync('mkcert create-cert --validity 3650 --key key.pem --cert cert.pem');
    }
  }

  ['release', 'beta', 'dev'].forEach((e) => {
    // set booleans based on env.name
    env[e] = e == env.name;
  });

  let version = packageJson.version.toString();
  if (env.beta && process.env.TRAVIS_BUILD_NUMBER) {
    version += `.${process.env.TRAVIS_BUILD_NUMBER}`;
  }

  const config = {
    mode: env.dev ? 'development' : 'production',

    entry: {
      main: './src/Index.tsx',
      browsercheck: './src/browsercheck.js',
      authReturn: './src/authReturn.ts',
      gdriveReturn: './src/gdriveReturn.ts'
    },

    output: {
      path: path.resolve('./dist'),
      publicPath: '/',
      filename: env.dev ? '[name]-[hash].js' : '[name]-[contenthash:6].js',
      chunkFilename: env.dev ? '[name]-[hash].js' : '[name]-[contenthash:6].js',
      futureEmitAssets: true
    },

    // Dev server
    serve: process.env.WEBPACK_SERVE
      ? {
          host: process.env.DOCKER ? '0.0.0.0' : 'localhost',
          devMiddleware: {
            stats: 'errors-only'
          },
          https: {
            key: fs.readFileSync('key.pem'), // Private keys in PEM format.
            cert: fs.readFileSync('cert.pem') // Cert chains in PEM format.
          }
        }
      : {},

    // Bail and fail hard on first error
    bail: !env.dev,

    stats: env.dev ? 'minimal' : 'normal',

    devtool: 'source-map',

    performance: {
      // Don't warn about too-large chunks
      hints: false
    },

    optimization: {
      // We always want the chunk name, otherwise it's just numbers
      namedChunks: true,
      // Extract the runtime into a separate chunk
      runtimeChunk: 'single',
      splitChunks: {
        chunks: 'all',
        automaticNameDelimiter: '-'
      },
      minimizer: [
        new TerserPlugin({
          cache: true,
          parallel: true,
          terserOptions: {
            ecma: 8,
            module: true,
            compress: { warnings: false, passes: 3, toplevel: true },
            mangle: { safari10: true, toplevel: true },
            output: { safari10: true }
          },
          sourceMap: true
        })
      ]
    },

    module: {
      strictExportPresence: true,

      rules: [
        {
          test: /\.js$/,
          exclude: [/node_modules/],
          loader: 'babel-loader',
          options: {
            cacheDirectory: true
          }
        },
        {
          test: /\.html$/,
          exclude: /index\.html/,
          loader: 'html-loader',
          options: {
            exportAsEs6Default: true,
            minimize: true
          }
        },
        {
          test: /\.(jpg|gif|png|eot|svg|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/,
          loader: 'url-loader',
          options: {
            limit: 5 * 1024, // only inline if less than 5kb
            name: ASSET_NAME_PATTERN
          }
        },
        // *.m.scss will have CSS Modules support
        {
          test: /\.m\.scss$/,
          use: [
            env.dev ? 'style-loader' : MiniCssExtractPlugin.loader,
            {
              loader: 'css-modules-typescript-loader',
              options: {
                mode: process.env.CI ? 'verify' : 'emit'
              }
            },
            {
              loader: 'css-loader',
              options: {
                modules: {
                  localIdentName: env.dev ? '[name]_[local]-[hash:base64:5]' : '[hash:base64:5]'
                },
                localsConvention: 'camelCaseOnly',
                sourceMap: true
              }
            },
            'postcss-loader',
            'sass-loader'
          ]
        },
        // Regular *.scss are global
        {
          test: /\.scss$/,
          exclude: /\.m\.scss$/,
          use: [
            env.dev ? 'style-loader' : MiniCssExtractPlugin.loader,
            {
              loader: 'css-loader',
              options: {
                sourceMap: true
              }
            },
            'postcss-loader',
            'sass-loader'
          ]
        },
        {
          test: /\.css$/,
          use: [env.dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader']
        },
        // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
        {
          test: /\.tsx?$/,
          use: [
            {
              loader: 'babel-loader',
              options: {
                cacheDirectory: true
              }
            },
            {
              loader: 'ts-loader'
            }
          ]
        },
        // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
        {
          enforce: 'pre',
          test: /\.jsx?$/,
          loader: 'source-map-loader'
        },
        {
          type: 'javascript/auto',
          test: /\.json/,
          include: /src(\/|\\)locale/,
          use: [
            {
              loader: 'file-loader',
              options: { name: '[name]-[hash:6].[ext]' }
            }
          ]
        },
        {
          type: 'javascript/auto',
          test: /\.wasm/
        },
        {
          test: /CHANGELOG\.md$/,
          loader: path.resolve('./config/changelog-loader')
        }
      ],

      noParse: function(path) {
        return false;
      }
    },

    resolve: {
      extensions: ['.js', '.json', '.ts', '.tsx', '.jsx'],

      alias: {
        app: path.resolve('./src/app/'),
        data: path.resolve('./src/data/'),
        images: path.resolve('./src/images/'),
        'destiny-icons': path.resolve('./destiny-icons/'),
        'idb-keyval': path.resolve('./src/app/storage/idb-keyval.ts')
      }
    },

    plugins: [
      new CaseSensitivePathsPlugin(),

      new webpack.IgnorePlugin(/caniuse-lite\/data\/regions/),

      new NotifyPlugin('DIM', !env.dev),

      new MiniCssExtractPlugin({
        filename: env.dev ? '[name]-[hash].css' : '[name]-[contenthash:6].css',
        chunkFilename: env.dev ? '[name]-[hash].css' : '[id]-[contenthash:6].css'
      }),

      // Fix some chunks not showing up in Webpack 4
      new HtmlWebpackIncludeSiblingChunksPlugin(),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'index.html',
        template: 'src/index.html',
        chunks: ['main', 'browsercheck'],
        templateParameters: {
          splash
        }
      }),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'return.html',
        template: '!html-loader!src/return.html',
        chunks: ['authReturn']
      }),

      new HtmlWebpackPlugin({
        inject: true,
        filename: 'gdrive-return.html',
        template: '!html-loader!src/gdrive-return.html',
        chunks: ['gdriveReturn']
      }),

      // Generate the .htaccess file (kind of an abuse of HtmlWebpack plugin just for templating)
      new HtmlWebpackPlugin({
        filename: '.htaccess',
        template: 'src/htaccess',
        inject: false,
        templateParameters: {
          csp: csp(env.name)
        }
      }),

      // Generate a version info JSON file we can poll. We could theoretically add more info here too.
      new GenerateJsonPlugin('./version.json', {
        version
      }),

      new CopyWebpackPlugin([
        { from: './src/manifest-webapp-6-2018.json' },
        // Only copy the manifests out of the data folder. Everything else we import directly into the bundle.
        { from: './src/data/d1/manifests', to: 'data/d1/manifests' },
        { from: `./icons/${env.name}/` },
        { from: `./icons/splash`, to: 'splash/' },
        { from: './src/safari-pinned-tab.svg' }
      ]),

      new webpack.DefinePlugin({
        $DIM_VERSION: JSON.stringify(version),
        $DIM_FLAVOR: JSON.stringify(env.name),
        $DIM_BUILD_DATE: JSON.stringify(Date.now()),
        // These are set from the Travis repo settings instead of .travis.yml
        $DIM_WEB_API_KEY: JSON.stringify(process.env.WEB_API_KEY),
        $DIM_WEB_CLIENT_ID: JSON.stringify(process.env.WEB_OAUTH_CLIENT_ID),
        $DIM_WEB_CLIENT_SECRET: JSON.stringify(process.env.WEB_OAUTH_CLIENT_SECRET),

        $GOOGLE_DRIVE_CLIENT_ID: JSON.stringify(
          '22022180893-raop2mu1d7gih97t5da9vj26quqva9dc.apps.googleusercontent.com'
        ),

        $BROWSERS: JSON.stringify(packageJson.browserslist),

        // Feature flags!

        // Print debug info to console about item moves
        '$featureFlags.debugMoves': JSON.stringify(!env.release),
        '$featureFlags.reviewsEnabled': JSON.stringify(true),
        // Sync data over gdrive
        '$featureFlags.gdrive': JSON.stringify(true),
        '$featureFlags.debugSync': JSON.stringify(!env.release),
        // Enable color-blind a11y
        '$featureFlags.colorA11y': JSON.stringify(true),
        // Whether to log page views for router events
        '$featureFlags.googleAnalyticsForRouter': JSON.stringify(true),
        // Debug ui-router
        '$featureFlags.debugRouter': JSON.stringify(false),
        // Debug Service Worker
        '$featureFlags.debugSW': JSON.stringify(!env.release),
        // Send exception reports to Sentry.io on beta only
        '$featureFlags.sentry': JSON.stringify(env.beta),
        // Respect the "do not track" header
        '$featureFlags.respectDNT': JSON.stringify(!env.release),
        // Forsaken Item Tiles
        '$featureFlags.forsakenTiles': JSON.stringify(!env.release),
        // Community-curated wish lists
        '$featureFlags.wishLists': JSON.stringify(true)
      }),

      new LodashModuleReplacementPlugin({
        collections: true,
        memoizing: true,
        shorthands: true,
        flattening: true
      }),

      new webpack.WatchIgnorePlugin([/scss\.d\.ts$/])
    ],

    node: {
      fs: 'empty',
      net: 'empty',
      tls: 'empty'
    }
  };

  // Enable if you want to debug the size of the chunks
  if (env.WEBPACK_VISUALIZE) {
    config.plugins.push(new Visualizer());
  }

  if (env.release) {
    new CopyWebpackPlugin([
      { from: './src/android-config.json', to: '.well-known/assetlinks.json' }
    ]);
  }

  if (env.dev) {
    config.plugins.push(
      new WebpackNotifierPlugin({
        title: 'DIM',
        alwaysNotify: true,
        contentImage: path.join(__dirname, '../icons/release/favicon-96x96.png')
      })
    );

    config.module.rules.push({
      test: /\.jsx?$/,
      include: /node_modules/,
      use: ['react-hot-loader/webpack']
    });
  } else {
    config.plugins.push(
      new CleanWebpackPlugin({
        cleanOnceBeforeBuildPatterns: ['.awcache', 'node_modules/.cache']
      }),

      // Tell React we're in Production mode
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production'),
        'process.env': JSON.stringify({ NODE_ENV: 'production' })
      }),

      // Generate a service worker
      new InjectManifest({
        maximumFileSizeToCacheInBytes: 5000000,
        include: [/\.(html|js|css|woff2|json|wasm)$/, /static\/.*\.(png|gif|jpg|svg)$/],
        exclude: [
          /version\.json/,
          /extension-dist/,
          /\.map$/,
          // Ignore both the webapp manifest and the d1-manifest files
          /data\/d1\/manifests/,
          /manifest-webapp/
        ],
        swSrc: './src/service-worker.js',
        swDest: 'service-worker.js',
        importWorkboxFrom: 'local',
        dontCacheBustUrlsMatching: /-[a-f0-9]{6}\./
      })
    );

    if (process.env.PT_PROJECT_TOKEN) {
      const packOptions = {
        upload: true,
        fail_build: true
      };

      if (process.env.TRAVIS === 'true') {
        Object.assign(packOptions, {
          branch: process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH,
          commit: process.env.TRAVIS_PULL_REQUEST_SHA || process.env.TRAVIS_COMMIT
        });
      }

      config.plugins.push(new PacktrackerPlugin(packOptions));
    }
  }

  return config;
};
<|endoftext|>