<commit_before>$(function () {
  'use strict';

  module('scrollspy plugin')

  test('should be defined on jquery object', function () {
    ok($(document.body).scrollspy, 'scrollspy method is defined')
  })

  module('scrollspy', {
    setup: function () {
      // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode
      $.fn.bootstrapScrollspy = $.fn.scrollspy.noConflict()
    },
    teardown: function () {
      $.fn.scrollspy = $.fn.bootstrapScrollspy
      delete $.fn.bootstrapScrollspy
    }
  })

  test('should provide no conflict', function () {
    strictEqual($.fn.scrollspy, undefined, 'scrollspy was set back to undefined (org value)')
  })

  test('should return jquery collection containing the element', function () {
    var $el = $('
')
    var $scrollspy = $el.bootstrapScrollspy()
    ok($scrollspy instanceof $, 'returns jquery collection')
    strictEqual($scrollspy[0], $el[0], 'collection contains element')
  })

  // FIXME
  test('should switch "active" class on scroll', function () {
    var topbarHTML = '
' +
        '
' +
        '
' +
        '
Bootstrap
' +
        '
Overview
' +
        '
' +
        '
' +
        '
' +
        '
'
    var $topbar = $(topbarHTML).bootstrapScrollspy()

    ok($topbar.find('.active', true))
  })

  test('should only switch "active" class on current target', function () {
    stop()

    var sectionHTML = '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
Overview
' +
        '
Detail
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
Overview
' +
        '
' +
        'Ad leggings keytar, brunch id art party dolor labore.' +
        '
' +
        '
' +
        '
' +
        '
Detail
' +
        '
' +
        'Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard.' +
        '
' +
        '
' +
        '
' +
        '
'
    var $section = $(sectionHTML).appendTo('#qunit-fixture')

    var $scrollspy = $section
        .show()
        .find('#scrollspy-example')
        .bootstrapScrollspy({ target: '#ss-target' })

    $scrollspy.on('scroll.bs.scrollspy', function () {
      ok($section.hasClass('active'), '"active" class still on root node')
      start()
    })

    $scrollspy.scrollTop(350)
  })

  test('middle navigation option correctly selected when large offset is used', function () {
    stop()

    var sectionHTML = '

' +
        '' +
        '
' +
        '
One
' +
        '
Two
' +
        '
Three
' +
        '
' +
        '' +
        '
' +
        '

' +
        '

' +
        '

' +
        '
'
    var $section = $(sectionHTML).appendTo('#qunit-fixture')
    var $scrollspy = $section
        .show()
        .filter('#content')

    $scrollspy.bootstrapScrollspy({ target: '#navigation', offset: $scrollspy.position().top })

    $scrollspy.on('scroll.bs.scrollspy', function () {
      ok(!$section.find('#one-link').parent().hasClass('active'), '"active" class removed from first section')
      ok($section.find('#two-link').parent().hasClass('active'), '"active" class on middle section')
      ok(!$section.find('#three-link').parent().hasClass('active'), '"active" class not on last section')
      start()
    })

    $scrollspy.scrollTop(550)
  })
})
<commit_msg>Mention #13500 in comment for ScrollSpy test<commit_after>$(function () {
  'use strict';

  module('scrollspy plugin')

  test('should be defined on jquery object', function () {
    ok($(document.body).scrollspy, 'scrollspy method is defined')
  })

  module('scrollspy', {
    setup: function () {
      // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode
      $.fn.bootstrapScrollspy = $.fn.scrollspy.noConflict()
    },
    teardown: function () {
      $.fn.scrollspy = $.fn.bootstrapScrollspy
      delete $.fn.bootstrapScrollspy
    }
  })

  test('should provide no conflict', function () {
    strictEqual($.fn.scrollspy, undefined, 'scrollspy was set back to undefined (org value)')
  })

  test('should return jquery collection containing the element', function () {
    var $el = $('
')
    var $scrollspy = $el.bootstrapScrollspy()
    ok($scrollspy instanceof $, 'returns jquery collection')
    strictEqual($scrollspy[0], $el[0], 'collection contains element')
  })

  // Does not work properly ATM, #13500 will fix this
  test('should switch "active" class on scroll', function () {
    var topbarHTML = '
' +
        '
' +
        '
' +
        '
Bootstrap
' +
        '
Overview
' +
        '
' +
        '
' +
        '
' +
        '
'
    var $topbar = $(topbarHTML).bootstrapScrollspy()

    ok($topbar.find('.active', true))
  })

  test('should only switch "active" class on current target', function () {
    stop()

    var sectionHTML = '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
Overview
' +
        '
Detail
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
Overview
' +
        '
' +
        'Ad leggings keytar, brunch id art party dolor labore.' +
        '
' +
        '
' +
        '
' +
        '
Detail
' +
        '
' +
        'Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard.' +
        '
' +
        '
' +
        '
' +
        '
'
    var $section = $(sectionHTML).appendTo('#qunit-fixture')

    var $scrollspy = $section
        .show()
        .find('#scrollspy-example')
        .bootstrapScrollspy({ target: '#ss-target' })

    $scrollspy.on('scroll.bs.scrollspy', function () {
      ok($section.hasClass('active'), '"active" class still on root node')
      start()
    })

    $scrollspy.scrollTop(350)
  })

  test('middle navigation option correctly selected when large offset is used', function () {
    stop()

    var sectionHTML = '

' +
        '' +
        '
' +
        '
One
' +
        '
Two
' +
        '
Three
' +
        '
' +
        '' +
        '
' +
        '

' +
        '

' +
        '

' +
        '
'
    var $section = $(sectionHTML).appendTo('#qunit-fixture')
    var $scrollspy = $section
        .show()
        .filter('#content')

    $scrollspy.bootstrapScrollspy({ target: '#navigation', offset: $scrollspy.position().top })

    $scrollspy.on('scroll.bs.scrollspy', function () {
      ok(!$section.find('#one-link').parent().hasClass('active'), '"active" class removed from first section')
      ok($section.find('#two-link').parent().hasClass('active'), '"active" class on middle section')
      ok(!$section.find('#three-link').parent().hasClass('active'), '"active" class not on last section')
      start()
    })

    $scrollspy.scrollTop(550)
  })
})
<|endoftext|>


<commit_before>require('LiveScript');
<commit_msg>Lowercase LiveScript require in specs<commit_after>require('livescript');
<|endoftext|>


<commit_before>/* Biblia League plugin
 * A fun script that holds all
 * of the league ranks in a JSON (now 2 JSON files)
 * file to be accessed at our leisure
 * By: panpawn
 */

var fs = require('fs');
var serialize = require('node-serialize');
var leagueRanks = {};
var leagueFactions = {};
var leagueName = "Biblia";
var leagueRanksToHave = ["e4", "champ", "gl", "purgatory", "prof"];
var leagueFactionsToHave = ["hell", "heaven"];

function loadFaction() {
	try {
		leagueFactions = serialize.unserialize(fs.readFileSync('config/biblia-league-factions.json', 'utf8'));
		Object.merge(Core.bibliafaction, leagueFactions);
	} catch (e) {};
}
setTimeout(function(){loadFaction();},1000);

function saveFaction() {
	try {
		fs.writeFileSync('config/biblia-league-factions.json',serialize.serialize(leagueFactions));
		Object.merge(Core.bibliafaction, leagueFactions);
	} catch (e) {};
}

function loadLeague() {
	try {
		leagueRanks = serialize.unserialize(fs.readFileSync('config/biblia-league-ranks.json', 'utf8'));
		Object.merge(Core.biblia, leagueRanks);
	} catch (e) {};
}
setTimeout(function(){loadLeague();},1000);

function saveLeague() {
	try {
		fs.writeFileSync('config/biblia-league-ranks.json',serialize.serialize(leagueRanks));
		Object.merge(Core.biblia, leagueRanks);
	} catch (e) {};
}

exports.commands = {
	biblia: function (target, room, user) {
		if (!target) target = 'help';
		if (room.id !== 'thebiblialeague') return false;
		var parts = target.split(',');
		for (var u in parts) parts[u] = parts[u].trim();
		try {
			switch (toId(parts[0])) {
				case 'giverank':
					if (!parts[1] || !parts[2]) return this.sendReply("ERROR!  Usage: /biblia giverank, [user], [rank] - Gives a user a league rank.");
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can give a " + leagueName + " rank!");
					var targetUser = toId(parts[1]);
					if (Core.biblia[targetUser]) return this.sendReply("ERROR! The user " + targetUser + " already has a league rank!");
					if (toId(parts[2]) !== 'e4' && toId(parts[2]) !== 'prof' && toId(parts[2]) !== 'champ' && toId(parts[2]) !== 'gl' && toId(parts[2]) !== 'purgatory') return this.sendReply("Ahhhh!  You didn't enter a valid league rank! (" + leagueRanksToHave + ")");
					leagueRanks[targetUser] = Core.biblia[targetUser] = toId(parts[2]); //shouldn't have to take the id here, this is for safety precautions
					saveLeague();
					this.sendReply(targetUser + " was given the league rank of " + parts[2]);
					this.logModCommand(targetUser + " was given the league rank of " + parts[2]);
					var rankLEBEL = "";
					switch (toId(parts[2])) {
						case 'e4':
							rankLEBEL = "Elite Four";
							break;
						case 'champ':
							rankLEBEL = "Champion";
							break;
						case 'gl':
							rankLEBEL = "Gym Leader";
							break;
						case 'prof':
							rankLEBEL = "Professor";
							break;
						case 'purgatory':
							rankLEBEL = "Purgatory";
							break;
					}
					room.add(targetUser + " was given the league rank of " + rankLEBEL + " by " + user.name + ".");
					break;
				case 'takerank':
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can take a " + leagueName + " rank!");
					if (!parts[1]) return this.sendReply("Usage: /biblia takerank, [user] - Removes a users rank.");
					var targetUser = toId(parts[1]);
					if (!Core.biblia[targetUser]) return this.sendReply("ERROR!  The user " + targetUser + " does not have an existing rank to remove!");
					delete Core.biblia[targetUser];
					delete leagueRanks[targetUser];
					saveLeague();
					this.sendReply(targetUser + "'s league rank was removed.");
					this.logModCommand(targetUser + "'s league rank was removed by " + user.name);
					room.add(targetUser + "'s league rank was removed by " + user.name);
					break;
				case 'givefaction':
					if (!parts[1] || !parts[2]) return this.sendReply("ERROR!  Usage: /biblia givefaction, [user], [faction] - Gives a user a league faction.");
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can give a " + leagueName + " faction!");
					var targetUser = toId(parts[1]);
					if (Core.bibliafaction[targetUser]) return this.sendReply("ERROR! The user " + targetUser + " already has a league faction!");
					if (toId(parts[2]) !== 'hell' || toId(parts[2]) == 'heaven') return this.sendReply("Ahhhh!  You didn't enter a valid league faction! (" + leagueFactionsToHave + ")");
					leagueFactions[targetUser] = Core.bibliafaction[targetUser] = toId(parts[2]); //shouldn't have to take the id here, this is for safety precautions
					saveFaction();
					this.sendReply(targetUser + " was given the league faction of " + parts[2]);
					this.logModCommand(targetUser + " was given the league faction of " + parts[2]);
					room.add(targetUser + " was given the league faction of " + parts[2] + " by " + user.name + ".");
					break;
				case 'takefaction':
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can take a " + leagueName + " faction!");
					if (!parts[1]) return this.sendReply("Usage: /biblia takefaction, [user] - Removes a users faction.");
					var targetUser = toId(parts[1]);
					if (!Core.bibliafaction[targetUser]) return this.sendReply("ERROR!  The user " + targetUser + " does not have an existing faction to remove!");
					delete Core.bibliafaction[targetUser];
					delete leagueFactions[targetUser];
					saveFaction();
					this.sendReply(targetUser + "'s league faction was removed.");
					this.logModCommand(targetUser + "'s league faction was removed by " + user.name);
					room.add(targetUser + "'s league faction was removed by " + user.name + ".");
					break;
				//Development commands
				case 'rankobject':
					if (!this.canBroadcast()) return;
					if (this.broadcasting) return this.sendReply("ERROR: this command is too spammy to broadcast.  Use / instead of ! to see it for yourself.");
					return this.sendReplyBox("Core.biblia = " + fs.readFileSync('config/biblia-league-ranks.json', 'utf8'));
					break;
				case 'factionobject':
					if (!this.canBroadcast()) return;
					if (this.broadcasting) return this.sendReply("ERROR: this command is too spammy to broadcast.  Use / instead of ! to see it for yourself.");
					return this.sendReplyBox("Core.bibliafaction = " + fs.readFileSync('config/biblia-league-factions.json', 'utf8'));
					break;
				case 'view':
				case 'show':
					if (!this.canBroadcast()) return;
					if (!parts[1]) parts[1] = user.name;
					var rank = Core.biblia[toId(parts[1])];
					if (!rank) rank = "None.";
					if (!Core.bibliafaction[toId(parts[1])]) fuckingFaction = "None.";
					var img = "";
					var rankLabel = "";
					var fuckingFaction = Core.bibliafaction[toId(parts[1])];
					if (!fuckingFaction) fuckingFaction = "None.";
					switch (rank) {
						case 'e4':
							rankLabel = "Elite Four";
							img = '';
							break;
						case 'champ':
							rankLabel = "Champion";
							img = '';
							break;
						case 'gl':
							rankLabel = "Gym Leader";
							img = '';
							break;
						case 'purgatory':
							rankLabel = "Purgatory";
							img = '';
							break;
						case 'prof':
							rankLabel = "Professor"
							img = '';
							break;
						default:
							img = '';
							rankLabel = "None."
					}
					return this.sendReplyBox(
						"User: " + parts[1] + "
" +
						"League Faction: " + fuckingFaction.substring(0,1).toUpperCase() + fuckingFaction.substring(1,fuckingFaction.length) + "
" +
						"League Rank: " + rankLabel.substring(0,1).toUpperCase() + rankLabel.substring(1,rankLabel.length) + " " + img
					);
					break;
				case 'help':
				default:
					if (!this.canBroadcast()) return;
					return this.sendReplyBox(
						"Biblia commands:
" +
						"/biblia giverank, [user], [rank] - Gives someone a league rank. Requires # and up.
" +
						"/biblia takerank, [user] - Removes that user's league rank.  Requires # and up.
" +
						"/biblia givefaction, [user], [faction] - Gives a user a league faction. Requires # and up.
" +
						"/biblia takefaction, [user] - Removes that user's league faction. Requires # and up.
" +
						"/biblia show, [user] - Shows that user's league rank and faction according to the biblia script."
					);
			}
		} catch (e) {
			console.log("AH!  THE BIBLIA SCRIPT HAS SELF DESTRUCTED!");
		}
	}
};
<commit_msg>General updates for biblia<commit_after>/* Biblia League plugin
 * A fun script that holds all
 * of the league ranks in a JSON (now 2 JSON files)
 * file to be accessed at our leisure (Beta)
 * By: panpawn
 */

var fs = require('fs');
var serialize = require('node-serialize');
var leagueRanks = {};
var leagueFactions = {};
var leagueName = "Biblia";
var leagueRanksToHave = ["e4", "champ", "gl", "purgatory", "prof"];
var leagueFactionsToHave = ["hell", "heaven", "earth", "space"];

if (typeof Gold === 'undefined') global.Gold = {};
Gold.biblia = {
	bibliafaction: {},
	biblia: {}
};

function loadFaction() {
	try {
		leagueFactions = serialize.unserialize(fs.readFileSync('config/biblia-league-factions.json', 'utf8'));
		Object.merge(Gold.biblia.bibliafaction, leagueFactions);
	} catch (e) {};
}
setTimeout(function(){loadFaction();},1000);

function saveFaction() {
	try {
		fs.writeFileSync('config/biblia-league-factions.json',serialize.serialize(leagueFactions));
		Object.merge(Gold.biblia.bibliafaction, leagueFactions);
	} catch (e) {};
}

function loadLeague() {
	try {
		leagueRanks = serialize.unserialize(fs.readFileSync('config/biblia-league-ranks.json', 'utf8'));
		Object.merge(Gold.biblia.biblia, leagueRanks);
	} catch (e) {};
}
setTimeout(function(){loadLeague();},1000);

function saveLeague() {
	try {
		fs.writeFileSync('config/biblia-league-ranks.json',serialize.serialize(leagueRanks));
		Object.merge(Gold.biblia.biblia, leagueRanks);
	} catch (e) {};
}

exports.commands = {
	biblia: function (target, room, user) {
		if (!target) target = 'help';
		if (room.id !== 'thebiblialeague') return false;
		var parts = target.split(',');
		for (var u in parts) parts[u] = parts[u].trim();
		try {
			switch (toId(parts[0])) {
				case 'giverank':
					if (!parts[1] || !parts[2]) return this.sendReply("ERROR!  Usage: /biblia giverank, [user], [rank] - Gives a user a league rank.");
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can give a " + leagueName + " rank!");
					var targetUser = toId(parts[1]);
					if (Gold.biblia.biblia[targetUser]) return this.sendReply("ERROR! The user " + targetUser + " already has a league rank!");
					if (toId(parts[2]) !== 'e4' && toId(parts[2]) !== 'prof' && toId(parts[2]) !== 'champ' && toId(parts[2]) !== 'gl' && toId(parts[2]) !== 'purgatory') return this.sendReply("Ahhhh!  You didn't enter a valid league rank! (" + leagueRanksToHave + ")");
					leagueRanks[targetUser] = Gold.biblia.biblia[targetUser] = toId(parts[2]); //shouldn't have to take the id here, this is for safety precautions
					saveLeague();
					this.sendReply(targetUser + " was given the league rank of " + parts[2]);
					this.logModCommand(targetUser + " was given the league rank of " + parts[2]);
					var rankLEBEL = "";
					switch (toId(parts[2])) {
						case 'e4':
							rankLEBEL = "Elite Four";
							break;
						case 'champ':
							rankLEBEL = "Champion";
							break;
						case 'gl':
							rankLEBEL = "Gym Leader";
							break;
						case 'prof':
							rankLEBEL = "Professor";
							break;
						case 'purgatory':
							rankLEBEL = "Purgatory";
							break;
					}
					room.add(targetUser + " was given the league rank of " + rankLEBEL + " by " + user.name + ".");
					break;
				case 'takerank':
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can take a " + leagueName + " rank!");
					if (!parts[1]) return this.sendReply("Usage: /biblia takerank, [user] - Removes a users rank.");
					var targetUser = toId(parts[1]);
					if (!Gold.biblia.biblia[targetUser]) return this.sendReply("ERROR!  The user " + targetUser + " does not have an existing rank to remove!");
					delete Gold.biblia.biblia[targetUser];
					delete leagueRanks[targetUser];
					saveLeague();
					this.sendReply(targetUser + "'s league rank was removed.");
					this.logModCommand(targetUser + "'s league rank was removed by " + user.name);
					room.add(targetUser + "'s league rank was removed by " + user.name);
					break;
				case 'givefaction':
					if (!parts[1] || !parts[2]) return this.sendReply("ERROR!  Usage: /biblia givefaction, [user], [faction] - Gives a user a league faction.");
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can give a " + leagueName + " faction!");
					var targetUser = toId(parts[1]);
					if (Gpld.biblia.bibliafaction[targetUser]) return this.sendReply("ERROR! The user " + targetUser + " already has a league faction!");
					if (toId(parts[2]) !== 'hell' || toId(parts[2]) == 'heaven') return this.sendReply("Ahhhh!  You didn't enter a valid league faction! (" + leagueFactionsToHave + ")");
					leagueFactions[targetUser] = Gold.biblia.bibliafaction[targetUser] = toId(parts[2]); //shouldn't have to take the id here, this is for safety precautions
					saveFaction();
					this.sendReply(targetUser + " was given the league faction of " + parts[2]);
					this.logModCommand(targetUser + " was given the league faction of " + parts[2]);
					room.add(targetUser + " was given the league faction of " + parts[2] + " by " + user.name + ".");
					break;
				case 'takefaction':
					if (!this.can('declare', null, room)) return this.sendReply("Only room owners and up can take a " + leagueName + " faction!");
					if (!parts[1]) return this.sendReply("Usage: /biblia takefaction, [user] - Removes a users faction.");
					var targetUser = toId(parts[1]);
					if (!Gold.biblia.bibliafaction[targetUser]) return this.sendReply("ERROR!  The user " + targetUser + " does not have an existing faction to remove!");
					delete Gold.biblia.bibliafaction[targetUser];
					delete leagueFactions[targetUser];
					saveFaction();
					this.sendReply(targetUser + "'s league faction was removed.");
					this.logModCommand(targetUser + "'s league faction was removed by " + user.name);
					room.add(targetUser + "'s league faction was removed by " + user.name + ".");
					break;
				//Development commands
				case 'rankobject':
					if (!this.canBroadcast()) return;
					if (this.broadcasting) return this.sendReply("ERROR: this command is too spammy to broadcast.  Use / instead of ! to see it for yourself.");
					return this.sendReplyBox("Gold.biblia.biblia = " + fs.readFileSync('config/biblia-league-ranks.json', 'utf8'));
					break;
				case 'factionobject':
					if (!this.canBroadcast()) return;
					if (this.broadcasting) return this.sendReply("ERROR: this command is too spammy to broadcast.  Use / instead of ! to see it for yourself.");
					return this.sendReplyBox("Gold.biblia.bibliafaction = " + fs.readFileSync('config/biblia-league-factions.json', 'utf8'));
					break;
				case 'profile':
				case 'view':
				case 'show':
					if (!this.canBroadcast()) return;
					if (!parts[1]) parts[1] = user.name;
					var rank = Gold.biblia.biblia[toId(parts[1])];
					if (!rank) rank = "None.";
					if (!Gold.biblia.bibliafaction[toId(parts[1])]) fuckingFaction = "None.";
					var img = "";
					var rankLabel = "";
					var fuckingFaction = Gold.biblia.bibliafaction[toId(parts[1])];
					if (!fuckingFaction) fuckingFaction = "None.";
					switch (rank) {
						case 'e4':
							rankLabel = "Elite Four";
							img = '';
							break;
						case 'champ':
							rankLabel = "Champion";
							img = '';
							break;
						case 'gl':
							rankLabel = "Gym Leader";
							img = '';
							break;
						case 'purgatory':
							rankLabel = "Purgatory";
							img = '';
							break;
						case 'prof':
							rankLabel = "Professor"
							img = '';
							break;
						default:
							img = '';
							rankLabel = "None."
					}
					return this.sendReplyBox(
						"User: " + parts[1] + "
" +
						"League Faction: " + fuckingFaction.substring(0,1).toUpperCase() + fuckingFaction.substring(1,fuckingFaction.length) + "
" +
						"League Rank: " + rankLabel.substring(0,1).toUpperCase() + rankLabel.substring(1,rankLabel.length) + " " + img
					);
					break;
				case 'help':
				default:
					if (!this.canBroadcast()) return;
					return this.sendReplyBox(
						"Biblia commands:
" +
						"/biblia giverank, [user], [rank] - Gives someone a league rank. Requires # and up.
" +
						"/biblia takerank, [user] - Removes that user's league rank.  Requires # and up.
" +
						"/biblia givefaction, [user], [faction] - Gives a user a league faction. Requires # and up.
" +
						"/biblia takefaction, [user] - Removes that user's league faction. Requires # and up.
" +
						"/biblia profile, [user] - Shows that user's league rank and faction according to the biblia script."
					);
			}
		} catch (e) {
			console.log("AH!  THE BIBLIA SCRIPT HAS SELF DESTRUCTED!\n" + e.stack);
			this.sendReply("Something broke. PM panpawn. Do not PM Lights. He doesn't care.");
		}
	}
};
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayObservable
 * @flow
 * @format
 */

'use strict';

const isPromise = require('isPromise');

import type {Disposable} from 'RelayCombinedEnvironmentTypes';
import type {LegacyObserver} from 'RelayNetworkTypes';

export type Subscription = {
  unsubscribe: () => void,
  +closed: boolean,
};

export type Observer<T> = {
  start?: ?(Subscription) => mixed,
  next?: ?(T) => mixed,
  error?: ?(Error) => mixed,
  complete?: ?() => mixed,
  unsubscribe?: ?(Subscription) => mixed,
};

type Sink<T> = {|
  +next: T => void,
  +error: Error => void,
  +complete: () => void,
  +closed: boolean,
|};

type Source_<T, SinkOfT: Sink<T>> = SinkOfT =>
  | void
  | Subscription
  | (() => mixed);
type Source<T> = Source_<T, *>;

export interface Subscribable<T> {
  subscribe(observer: Observer<T>): Subscription,
}

// Note: This should accept Subscribable<T> instead of RelayObservable<T>,
// however Flow cannot yet distinguish it from T.
export type ObservableFromValue<T> = RelayObservable<T> | Promise<T> | T;

let hostReportError;

/**
 * Limited implementation of ESObservable, providing the limited set of behavior
 * Relay networking requires.
 *
 * Observables retain the benefit of callbacks which can be called
 * synchronously, avoiding any UI jitter, while providing a compositional API,
 * which simplifies logic and prevents mishandling of errors compared to
 * the direct use of callback functions.
 *
 * ESObservable: https://github.com/tc39/proposal-observable
 */
class RelayObservable<T> implements Subscribable<T> {
  _source: Source<T>;

  constructor(source: Source<T>): void {
    if (__DEV__) {
      // Early runtime errors for ill-formed sources.
      if (!source || typeof source !== 'function') {
        throw new Error('Source must be a Function: ' + String(source));
      }
    }
    this._source = source;
  }

  /**
   * When an unhandled error is detected, it is reported to the host environment
   * (the ESObservable spec refers to this method as "HostReportErrors()").
   *
   * The default implementation in development builds re-throws errors in a
   * separate frame, and from production builds does nothing (swallowing
   * uncaught errors).
   *
   * Called during application initialization, this method allows
   * application-specific handling of uncaught errors. Allowing, for example,
   * integration with error logging or developer tools.
   */
  static onUnhandledError(callback: Error => mixed): void {
    hostReportError = callback;
  }

  /**
   * Accepts various kinds of data sources, and always returns a RelayObservable
   * useful for accepting the result of a user-provided FetchFunction.
   */
  static from<V>(obj: ObservableFromValue<V>): RelayObservable<V> {
    return isObservable(obj)
      ? fromObservable(obj)
      : isPromise(obj) ? fromPromise(obj) : fromValue(obj);
  }

  /**
   * Creates a RelayObservable, given a function which expects a legacy
   * Relay Observer as the last argument and which returns a Disposable.
   *
   * To support migration to Observable, the function may ignore the
   * legacy Relay observer and directly return an Observable instead.
   */
  static fromLegacy<V>(
    callback: (LegacyObserver<V>) => Disposable | RelayObservable<V>,
  ): RelayObservable<V> {
    return new RelayObservable(sink => {
      const result = callback({
        onNext: sink.next,
        onError: sink.error,
        onCompleted: sink.complete,
      });
      return isObservable(result)
        ? result.subscribe(sink)
        : () => result.dispose();
    });
  }

  /**
   * Similar to promise.catch(), observable.catch() handles error events, and
   * provides an alternative observable to use in it's place.
   *
   * If the catch handler throws a new error, it will appear as an error event
   * on the resulting Observable.
   */
  catch<U>(fn: Error => RelayObservable<U>): RelayObservable<T | U> {
    return new RelayObservable(sink => {
      let subscription;
      this.subscribe({
        start: sub => {
          subscription = sub;
        },
        next: sink.next,
        complete: sink.complete,
        error(error) {
          try {
            fn(error).subscribe({
              start: sub => {
                subscription = sub;
              },
              next: sink.next,
              complete: sink.complete,
              error: sink.error,
            });
          } catch (error2) {
            sink.error(error2);
          }
        },
      });
      return () => subscription.unsubscribe();
    });
  }

  /**
   * Returns a new Observable which returns the same values as this one, but
   * modified so that the provided Observer is called to perform a side-effects
   * for all events emitted by the source.
   *
   * Any errors that are thrown in the side-effect Observer are unhandled, and
   * do not affect the source Observable or its Observer.
   *
   * This is useful for when debugging your Observables or performing other
   * side-effects such as logging or performance monitoring.
   */
  do(observer: Observer<T>): RelayObservable<T> {
    return new RelayObservable(sink => {
      const both = action =>
        function() {
          try {
            observer[action] && observer[action].apply(observer, arguments);
          } catch (error) {
            handleError(error);
          }
          sink[action] && sink[action].apply(sink, arguments);
        };
      return this.subscribe({
        start: both('start'),
        next: both('next'),
        error: both('error'),
        complete: both('complete'),
        unsubscribe: both('unsubscribe'),
      });
    });
  }

  /**
   * Returns a new Observable which returns the same values as this one, but
   * modified so that the finally callback is performed after completion,
   * whether normal or due to error or unsubscription.
   *
   * This is useful for cleanup such as resource finalization.
   */
  finally(fn: () => mixed): RelayObservable<T> {
    return new RelayObservable(sink => {
      const subscription = this.subscribe(sink);
      return () => {
        subscription.unsubscribe();
        fn();
      };
    });
  }

  /**
   * Returns a new Observable which is identical to this one, unless this
   * Observable completes before yielding any values, in which case the new
   * Observable will yield the values from the alternate Observable.
   *
   * If this Observable does yield values, the alternate is never subscribed to.
   *
   * This is useful for scenarios where values may come from multiple sources
   * which should be tried in order, i.e. from a cache before a network.
   */
  ifEmpty<U>(alternate: RelayObservable<U>): RelayObservable<T | U> {
    return new RelayObservable(sink => {
      let hasValue = false;
      let current = this.subscribe({
        next(value) {
          hasValue = true;
          sink.next(value);
        },
        error: sink.error,
        complete() {
          if (hasValue) {
            sink.complete();
          } else {
            current = alternate.subscribe(sink);
          }
        },
      });
      return () => {
        current.unsubscribe();
      };
    });
  }

  /**
   * Observable's primary API: returns an unsubscribable Subscription to the
   * source of this Observable.
   */
  subscribe(observer: Observer<T>): Subscription {
    if (__DEV__) {
      // Early runtime errors for ill-formed observers.
      if (!observer || typeof observer !== 'object') {
        throw new Error(
          'Observer must be an Object with callbacks: ' + String(observer),
        );
      }
    }
    return subscribe(this._source, observer);
  }

  /**
   * Supports subscription of a legacy Relay Observer, returning a Disposable.
   */
  subscribeLegacy(legacyObserver: LegacyObserver<T>): Disposable {
    const subscription = this.subscribe({
      next: legacyObserver.onNext,
      error: legacyObserver.onError,
      complete: legacyObserver.onCompleted,
    });
    return {
      dispose: subscription.unsubscribe,
    };
  }

  /**
   * Returns a new Observerable where each value has been transformed by
   * the mapping function.
   */
  map<U>(fn: T => U): RelayObservable<U> {
    return this.concatMap(value => fromValue(fn(value)));
  }

  /**
   * Returns a new Observable where each value is replaced with a new Observable
   * by the mapping function, the results of which returned as a single
   * concattenated Observable.
   */
  concatMap<U>(fn: T => ObservableFromValue<U>): RelayObservable<U> {
    return new RelayObservable(sink => {
      let hasCompleted = false;
      let outer;
      let inner;
      const buffer = [];

      function next(value) {
        if (inner) {
          buffer.push(value);
        } else {
          try {
            RelayObservable.from(fn(value)).subscribe({
              start: sub => {
                inner = sub;
              },
              next: sink.next,
              error: sink.error,
              complete() {
                inner = undefined;
                if (buffer.length !== 0) {
                  next(buffer.shift());
                } else if (hasCompleted) {
                  sink.complete();
                }
              },
            });
          } catch (error) {
            sink.error(error);
          }
        }
      }

      this.subscribe({
        start: sub => {
          outer = sub;
        },
        next,
        error: sink.error,
        complete() {
          hasCompleted = true;
          if (!inner) {
            sink.complete();
          }
        },
      });

      return () => {
        if (inner) {
          inner.unsubscribe();
          inner = undefined;
        }
        outer.unsubscribe();
        buffer.length = 0;
      };
    });
  }

  /**
   * Returns a new Observable which first mirrors this Observable, then when it
   * completes, waits for `pollInterval` milliseconds before re-subscribing to
   * this Observable again, looping in this manner until unsubscribed.
   *
   * The returned Observable never completes.
   */
  poll(pollInterval: number): RelayObservable<T> {
    if (__DEV__) {
      if (typeof pollInterval !== 'number' || pollInterval <= 0) {
        throw new Error(
          'RelayObservable: Expected pollInterval to be positive, got: ' +
            pollInterval,
        );
      }
    }
    return new RelayObservable(sink => {
      let subscription;
      let timeout;
      const poll = () => {
        subscription = this.subscribe({
          next: sink.next,
          error: sink.error,
          complete() {
            timeout = setTimeout(poll, pollInterval);
          },
        });
      };
      poll();
      return () => {
        clearTimeout(timeout);
        subscription.unsubscribe();
      };
    });
  }

  /**
   * Returns a Promise which resolves when this Observable yields a first value
   * or when it completes with no value.
   */
  toPromise(): Promise<T | void> {
    return new Promise((resolve, reject) => {
      let subscription;
      this.subscribe({
        start(sub) {
          subscription = sub;
        },
        next(val) {
          resolve(val);
          subscription.unsubscribe();
        },
        error: reject,
        complete: resolve,
      });
    });
  }
}

// Use declarations to teach Flow how to check isObservable.
declare function isObservable(p: mixed): boolean %checks(p instanceof
  RelayObservable);

// prettier-ignore
// eslint-disable-next-line no-redeclare
function isObservable(obj) {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.subscribe === 'function'
  );
}

function fromObservable<T>(obj: Subscribable<T>): RelayObservable<T> {
  return obj instanceof RelayObservable
    ? obj
    : new RelayObservable(sink => obj.subscribe(sink));
}

function fromPromise<T>(promise: Promise<T>): RelayObservable<T> {
  return new RelayObservable(sink => {
    // Since sink methods do not throw, the resulting Promise can be ignored.
    promise.then(value => {
      sink.next(value);
      sink.complete();
    }, sink.error);
  });
}

function fromValue<T>(value: T): RelayObservable<T> {
  return new RelayObservable(sink => {
    sink.next(value);
    sink.complete();
  });
}

function handleError(error: Error): void {
  hostReportError && hostReportError(error);
}

function subscribe<T>(source: Source<T>, observer: Observer<T>): Subscription {
  let closed = false;
  let cleanup;

  function doCleanup() {
    if (cleanup) {
      if (cleanup.unsubscribe) {
        cleanup.unsubscribe();
      } else {
        try {
          cleanup();
        } catch (error) {
          handleError(error);
        }
      }
      cleanup = undefined;
    }
  }

  // Create a Subscription.
  const subscription: Subscription = {
    unsubscribe() {
      if (!closed) {
        closed = true;

        // Tell Observer that unsubscribe was called.
        try {
          observer.unsubscribe && observer.unsubscribe(subscription);
        } catch (error) {
          handleError(error);
        } finally {
          doCleanup();
        }
      }
    },
    get closed() {
      return closed;
    },
  };

  // Tell Observer that observation is about to begin.
  try {
    observer.start && observer.start(subscription);
  } catch (error) {
    handleError(error);
  }

  // If closed already, don't bother creating a Sink.
  if (closed) {
    return subscription;
  }

  // Create a Sink respecting subscription state and cleanup.
  const sink: Sink<T> = {
    next(value) {
      if (!closed && observer.next) {
        try {
          observer.next(value);
        } catch (error) {
          handleError(error);
        }
      }
    },
    error(error) {
      try {
        if (closed) {
          throw error;
        }
        closed = true;
        if (!observer.error) {
          throw error;
        }
        observer.error(error);
      } catch (error2) {
        handleError(error2);
      } finally {
        doCleanup();
      }
    },
    complete() {
      if (!closed) {
        closed = true;
        try {
          observer.complete && observer.complete();
        } catch (error) {
          handleError(error);
        } finally {
          doCleanup();
        }
      }
    },
    get closed() {
      return closed;
    },
  };

  // If anything goes wrong during observing the source, handle the error.
  try {
    cleanup = source(sink);
  } catch (error) {
    sink.error(error);
  }

  if (__DEV__) {
    // Early runtime errors for ill-formed returned cleanup.
    if (
      cleanup !== undefined &&
      typeof cleanup !== 'function' &&
      (!cleanup || typeof cleanup.unsubscribe !== 'function')
    ) {
      throw new Error(
        'Returned cleanup function which cannot be called: ' + String(cleanup),
      );
    }
  }

  // If closed before the source function existed, cleanup now.
  if (closed) {
    doCleanup();
  }

  return subscription;
}

if (__DEV__) {
  // Default implementation of HostReportErrors() in development builds.
  // Can be replaced by the host application environment.
  RelayObservable.onUnhandledError(error => {
    declare function fail(string): void;
    if (typeof fail === 'function') {
      // In test environments (Jest), fail() immediately fails the current test.
      fail(String(error));
    } else {
      // Otherwise, rethrow on the next frame to avoid breaking current logic.
      setTimeout(() => {
        throw error;
      });
    }
  });
}

module.exports = RelayObservable;
<commit_msg>Fix Obserable sink.closed for ES3<commit_after>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayObservable
 * @flow
 * @format
 */

'use strict';

const isPromise = require('isPromise');

import type {Disposable} from 'RelayCombinedEnvironmentTypes';
import type {LegacyObserver} from 'RelayNetworkTypes';

export type Subscription = {
  unsubscribe: () => void,
  +closed: boolean,
};

export type Observer<T> = {
  start?: ?(Subscription) => mixed,
  next?: ?(T) => mixed,
  error?: ?(Error) => mixed,
  complete?: ?() => mixed,
  unsubscribe?: ?(Subscription) => mixed,
};

type Sink<T> = {|
  +next: T => void,
  +error: Error => void,
  +complete: () => void,
  +closed: boolean,
|};

type Source_<T, SinkOfT: Sink<T>> = SinkOfT =>
  | void
  | Subscription
  | (() => mixed);
type Source<T> = Source_<T, *>;

export interface Subscribable<T> {
  subscribe(observer: Observer<T>): Subscription,
}

// Note: This should accept Subscribable<T> instead of RelayObservable<T>,
// however Flow cannot yet distinguish it from T.
export type ObservableFromValue<T> = RelayObservable<T> | Promise<T> | T;

let hostReportError;

/**
 * Limited implementation of ESObservable, providing the limited set of behavior
 * Relay networking requires.
 *
 * Observables retain the benefit of callbacks which can be called
 * synchronously, avoiding any UI jitter, while providing a compositional API,
 * which simplifies logic and prevents mishandling of errors compared to
 * the direct use of callback functions.
 *
 * ESObservable: https://github.com/tc39/proposal-observable
 */
class RelayObservable<T> implements Subscribable<T> {
  _source: Source<T>;

  constructor(source: Source<T>): void {
    if (__DEV__) {
      // Early runtime errors for ill-formed sources.
      if (!source || typeof source !== 'function') {
        throw new Error('Source must be a Function: ' + String(source));
      }
    }
    this._source = source;
  }

  /**
   * When an unhandled error is detected, it is reported to the host environment
   * (the ESObservable spec refers to this method as "HostReportErrors()").
   *
   * The default implementation in development builds re-throws errors in a
   * separate frame, and from production builds does nothing (swallowing
   * uncaught errors).
   *
   * Called during application initialization, this method allows
   * application-specific handling of uncaught errors. Allowing, for example,
   * integration with error logging or developer tools.
   */
  static onUnhandledError(callback: Error => mixed): void {
    hostReportError = callback;
  }

  /**
   * Accepts various kinds of data sources, and always returns a RelayObservable
   * useful for accepting the result of a user-provided FetchFunction.
   */
  static from<V>(obj: ObservableFromValue<V>): RelayObservable<V> {
    return isObservable(obj)
      ? fromObservable(obj)
      : isPromise(obj) ? fromPromise(obj) : fromValue(obj);
  }

  /**
   * Creates a RelayObservable, given a function which expects a legacy
   * Relay Observer as the last argument and which returns a Disposable.
   *
   * To support migration to Observable, the function may ignore the
   * legacy Relay observer and directly return an Observable instead.
   */
  static fromLegacy<V>(
    callback: (LegacyObserver<V>) => Disposable | RelayObservable<V>,
  ): RelayObservable<V> {
    return new RelayObservable(sink => {
      const result = callback({
        onNext: sink.next,
        onError: sink.error,
        onCompleted: sink.complete,
      });
      return isObservable(result)
        ? result.subscribe(sink)
        : () => result.dispose();
    });
  }

  /**
   * Similar to promise.catch(), observable.catch() handles error events, and
   * provides an alternative observable to use in it's place.
   *
   * If the catch handler throws a new error, it will appear as an error event
   * on the resulting Observable.
   */
  catch<U>(fn: Error => RelayObservable<U>): RelayObservable<T | U> {
    return new RelayObservable(sink => {
      let subscription;
      this.subscribe({
        start: sub => {
          subscription = sub;
        },
        next: sink.next,
        complete: sink.complete,
        error(error) {
          try {
            fn(error).subscribe({
              start: sub => {
                subscription = sub;
              },
              next: sink.next,
              complete: sink.complete,
              error: sink.error,
            });
          } catch (error2) {
            sink.error(error2);
          }
        },
      });
      return () => subscription.unsubscribe();
    });
  }

  /**
   * Returns a new Observable which returns the same values as this one, but
   * modified so that the provided Observer is called to perform a side-effects
   * for all events emitted by the source.
   *
   * Any errors that are thrown in the side-effect Observer are unhandled, and
   * do not affect the source Observable or its Observer.
   *
   * This is useful for when debugging your Observables or performing other
   * side-effects such as logging or performance monitoring.
   */
  do(observer: Observer<T>): RelayObservable<T> {
    return new RelayObservable(sink => {
      const both = action =>
        function() {
          try {
            observer[action] && observer[action].apply(observer, arguments);
          } catch (error) {
            handleError(error);
          }
          sink[action] && sink[action].apply(sink, arguments);
        };
      return this.subscribe({
        start: both('start'),
        next: both('next'),
        error: both('error'),
        complete: both('complete'),
        unsubscribe: both('unsubscribe'),
      });
    });
  }

  /**
   * Returns a new Observable which returns the same values as this one, but
   * modified so that the finally callback is performed after completion,
   * whether normal or due to error or unsubscription.
   *
   * This is useful for cleanup such as resource finalization.
   */
  finally(fn: () => mixed): RelayObservable<T> {
    return new RelayObservable(sink => {
      const subscription = this.subscribe(sink);
      return () => {
        subscription.unsubscribe();
        fn();
      };
    });
  }

  /**
   * Returns a new Observable which is identical to this one, unless this
   * Observable completes before yielding any values, in which case the new
   * Observable will yield the values from the alternate Observable.
   *
   * If this Observable does yield values, the alternate is never subscribed to.
   *
   * This is useful for scenarios where values may come from multiple sources
   * which should be tried in order, i.e. from a cache before a network.
   */
  ifEmpty<U>(alternate: RelayObservable<U>): RelayObservable<T | U> {
    return new RelayObservable(sink => {
      let hasValue = false;
      let current = this.subscribe({
        next(value) {
          hasValue = true;
          sink.next(value);
        },
        error: sink.error,
        complete() {
          if (hasValue) {
            sink.complete();
          } else {
            current = alternate.subscribe(sink);
          }
        },
      });
      return () => {
        current.unsubscribe();
      };
    });
  }

  /**
   * Observable's primary API: returns an unsubscribable Subscription to the
   * source of this Observable.
   */
  subscribe(observer: Observer<T>): Subscription {
    if (__DEV__) {
      // Early runtime errors for ill-formed observers.
      if (!observer || typeof observer !== 'object') {
        throw new Error(
          'Observer must be an Object with callbacks: ' + String(observer),
        );
      }
    }
    return subscribe(this._source, observer);
  }

  /**
   * Supports subscription of a legacy Relay Observer, returning a Disposable.
   */
  subscribeLegacy(legacyObserver: LegacyObserver<T>): Disposable {
    const subscription = this.subscribe({
      next: legacyObserver.onNext,
      error: legacyObserver.onError,
      complete: legacyObserver.onCompleted,
    });
    return {
      dispose: subscription.unsubscribe,
    };
  }

  /**
   * Returns a new Observerable where each value has been transformed by
   * the mapping function.
   */
  map<U>(fn: T => U): RelayObservable<U> {
    return this.concatMap(value => fromValue(fn(value)));
  }

  /**
   * Returns a new Observable where each value is replaced with a new Observable
   * by the mapping function, the results of which returned as a single
   * concattenated Observable.
   */
  concatMap<U>(fn: T => ObservableFromValue<U>): RelayObservable<U> {
    return new RelayObservable(sink => {
      let hasCompleted = false;
      let outer;
      let inner;
      const buffer = [];

      function next(value) {
        if (inner) {
          buffer.push(value);
        } else {
          try {
            RelayObservable.from(fn(value)).subscribe({
              start: sub => {
                inner = sub;
              },
              next: sink.next,
              error: sink.error,
              complete() {
                inner = undefined;
                if (buffer.length !== 0) {
                  next(buffer.shift());
                } else if (hasCompleted) {
                  sink.complete();
                }
              },
            });
          } catch (error) {
            sink.error(error);
          }
        }
      }

      this.subscribe({
        start: sub => {
          outer = sub;
        },
        next,
        error: sink.error,
        complete() {
          hasCompleted = true;
          if (!inner) {
            sink.complete();
          }
        },
      });

      return () => {
        if (inner) {
          inner.unsubscribe();
          inner = undefined;
        }
        outer.unsubscribe();
        buffer.length = 0;
      };
    });
  }

  /**
   * Returns a new Observable which first mirrors this Observable, then when it
   * completes, waits for `pollInterval` milliseconds before re-subscribing to
   * this Observable again, looping in this manner until unsubscribed.
   *
   * The returned Observable never completes.
   */
  poll(pollInterval: number): RelayObservable<T> {
    if (__DEV__) {
      if (typeof pollInterval !== 'number' || pollInterval <= 0) {
        throw new Error(
          'RelayObservable: Expected pollInterval to be positive, got: ' +
            pollInterval,
        );
      }
    }
    return new RelayObservable(sink => {
      let subscription;
      let timeout;
      const poll = () => {
        subscription = this.subscribe({
          next: sink.next,
          error: sink.error,
          complete() {
            timeout = setTimeout(poll, pollInterval);
          },
        });
      };
      poll();
      return () => {
        clearTimeout(timeout);
        subscription.unsubscribe();
      };
    });
  }

  /**
   * Returns a Promise which resolves when this Observable yields a first value
   * or when it completes with no value.
   */
  toPromise(): Promise<T | void> {
    return new Promise((resolve, reject) => {
      let subscription;
      this.subscribe({
        start(sub) {
          subscription = sub;
        },
        next(val) {
          resolve(val);
          subscription.unsubscribe();
        },
        error: reject,
        complete: resolve,
      });
    });
  }
}

// Use declarations to teach Flow how to check isObservable.
declare function isObservable(p: mixed): boolean %checks(p instanceof
  RelayObservable);

// prettier-ignore
// eslint-disable-next-line no-redeclare
function isObservable(obj) {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.subscribe === 'function'
  );
}

function fromObservable<T>(obj: Subscribable<T>): RelayObservable<T> {
  return obj instanceof RelayObservable
    ? obj
    : new RelayObservable(sink => obj.subscribe(sink));
}

function fromPromise<T>(promise: Promise<T>): RelayObservable<T> {
  return new RelayObservable(sink => {
    // Since sink methods do not throw, the resulting Promise can be ignored.
    promise.then(value => {
      sink.next(value);
      sink.complete();
    }, sink.error);
  });
}

function fromValue<T>(value: T): RelayObservable<T> {
  return new RelayObservable(sink => {
    sink.next(value);
    sink.complete();
  });
}

function handleError(error: Error): void {
  hostReportError && hostReportError(error);
}

function subscribe<T>(source: Source<T>, observer: Observer<T>): Subscription {
  let closed = false;
  let cleanup;

  // Ideally we would simply describe a `get closed()` method on the Sink and
  // Subscription objects below, however not all flow environments we expect
  // Relay to be used within will support property getters, and many minifier
  // tools still do not support ES5 syntax. Instead, we can use defineProperty.
  const withClosed: <O>(obj: O) => O & {+closed: boolean} = obj =>
    Object.defineProperty((obj: any), 'closed', ({get: () => closed}: any));

  function doCleanup() {
    if (cleanup) {
      if (cleanup.unsubscribe) {
        cleanup.unsubscribe();
      } else {
        try {
          cleanup();
        } catch (error) {
          handleError(error);
        }
      }
      cleanup = undefined;
    }
  }

  // Create a Subscription.
  const subscription: Subscription = withClosed({
    unsubscribe() {
      if (!closed) {
        closed = true;

        // Tell Observer that unsubscribe was called.
        try {
          observer.unsubscribe && observer.unsubscribe(subscription);
        } catch (error) {
          handleError(error);
        } finally {
          doCleanup();
        }
      }
    },
  });

  // Tell Observer that observation is about to begin.
  try {
    observer.start && observer.start(subscription);
  } catch (error) {
    handleError(error);
  }

  // If closed already, don't bother creating a Sink.
  if (closed) {
    return subscription;
  }

  // Create a Sink respecting subscription state and cleanup.
  const sink: Sink<T> = withClosed({
    next(value) {
      if (!closed && observer.next) {
        try {
          observer.next(value);
        } catch (error) {
          handleError(error);
        }
      }
    },
    error(error) {
      try {
        if (closed) {
          throw error;
        }
        closed = true;
        if (!observer.error) {
          throw error;
        }
        observer.error(error);
      } catch (error2) {
        handleError(error2);
      } finally {
        doCleanup();
      }
    },
    complete() {
      if (!closed) {
        closed = true;
        try {
          observer.complete && observer.complete();
        } catch (error) {
          handleError(error);
        } finally {
          doCleanup();
        }
      }
    },
  });

  // If anything goes wrong during observing the source, handle the error.
  try {
    cleanup = source(sink);
  } catch (error) {
    sink.error(error);
  }

  if (__DEV__) {
    // Early runtime errors for ill-formed returned cleanup.
    if (
      cleanup !== undefined &&
      typeof cleanup !== 'function' &&
      (!cleanup || typeof cleanup.unsubscribe !== 'function')
    ) {
      throw new Error(
        'Returned cleanup function which cannot be called: ' + String(cleanup),
      );
    }
  }

  // If closed before the source function existed, cleanup now.
  if (closed) {
    doCleanup();
  }

  return subscription;
}

if (__DEV__) {
  // Default implementation of HostReportErrors() in development builds.
  // Can be replaced by the host application environment.
  RelayObservable.onUnhandledError(error => {
    declare function fail(string): void;
    if (typeof fail === 'function') {
      // In test environments (Jest), fail() immediately fails the current test.
      fail(String(error));
    } else {
      // Otherwise, rethrow on the next frame to avoid breaking current logic.
      setTimeout(() => {
        throw error;
      });
    }
  });
}

module.exports = RelayObservable;
<|endoftext|>


<commit_before>/* global ngDefine: false, angular: false */
ngDefine('camunda.common.pages', ['jquery'], function(module, $) {
  'use strict';
  var ResponseErrorHandlerInitializer = [
    '$rootScope', '$location', 'Notifications', 'Authentication',
    function($rootScope, $location, Notifications, Authentication) {

    function addError(error) {
      error.http = true;
      error.exclusive = [ 'http' ];

      Notifications.addError(error);
    }

    /**
     * A handler function that handles HTTP error responses,
     * i.e. 4XX and 5XX responses by redirecting / notifying the user.
     */
    function handleHttpError(event, error) {

      var status = error.status,
          data = error.data;

      switch (status) {
      case 500:
        if (data && data.message) {
          addError({
            status: 'Server Error',
            message: data.message,
            exceptionType: data.exceptionType
          });
        } else {
          addError({
            status: 'Server Error',
            message: 'The server reported an internal error. Try to refresh the page or login and out of the application.'
          });
        }
        break;

      case 0:
        addError({ status: 'Request Timeout', message:  'Your request timed out. Try to refresh the page.' });
        break;

      case 401:
        Authentication.clear();

        if ($location.absUrl().indexOf('/setup/#') == -1) {
          addError({ type: 'warning', status: 'Session ended', message: 'Your session timed out or was ended from another browser window. Please signin again.' });

          $location.path('/login');
        } else {
          $location.path('/setup');
        }
        break;

      case 403:
        if (data.type == 'AuthorizationException') {
          addError({
            status: 'Access Denied',
            message: 'You are unauthorized to ' +
            data.permissionName.toLowerCase() + ' ' +
            data.resourceName.toLowerCase() +
            (data.resourceId ? ' ' + data.resourceId : 's') +
            '.' });
        } else {
          addError({
            status: 'Access Denied',
            message: 'Executing an action has been denied by the server. Try to refresh the page.'
          });
        }
        break;

      case 404:
        addError({ status: 'Not found', message: 'A resource you requested could not be found.' });
        break;
      default:
        addError({
          status: 'Communication Error',
          message: 'The application received an unexpected ' + status + ' response from the server. Try to refresh the page or login and out of the application.'
        });
      }
    }

    // triggered by httpStatusInterceptor
    $rootScope.$on('httpError', handleHttpError);
  }];

  var ProcessEngineSelectionController = [
    '$scope', '$http', '$location', '$window', 'Uri', 'Notifications',
    function($scope, $http, $location, $window, Uri, Notifications) {

    var current = Uri.appUri(':engine');
    var enginesByName = {};

    $http.get(Uri.appUri('engine://engine/')).then(function(response) {
      $scope.engines = response.data;

      angular.forEach($scope.engines , function(engine) {
        enginesByName[engine.name] = engine;
      });

      $scope.currentEngine = enginesByName[current];

      if (!$scope.currentEngine) {
        Notifications.addError({ status: 'Not found', message: 'The process engine you are trying to access does not exist' });
        $location.path('/');
      }
    });

    $scope.$watch('currentEngine', function(engine) {
      if (engine && current !== engine.name) {
        $window.location.href = Uri.appUri('app://../' + engine.name + '/');
      }
    });
  }];

  var NavigationController = [ '$scope', '$location', function($scope, $location) {

      $scope.activeClass = function(link) {
        var path = $location.absUrl();
        return path.indexOf(link) != -1 ? 'active' : '';
      };
  }];

  var AuthenticationController = [
    '$scope', '$window', '$cacheFactory', 'Notifications', 'AuthenticationService', 'Uri',
    function($scope, $window, $cacheFactory, Notifications, AuthenticationService, Uri) {
    // '$scope', '$window', '$cacheFactory', 'Notifications', 'AuthenticationService', 'Uri', 'page',
    // function($scope, $window, $cacheFactory, Notifications, AuthenticationService, Uri, page) {
      $scope.logout = function() {
        AuthenticationService.logout().then(function() {
          $cacheFactory.get('$http').removeAll();
          // page.titleSet('camunda Cockpit');
          $('head title').text('camunda Cockpit');
          $window.location.href = Uri.appUri('app://#/login');
        });
      };
  }];

  module
    .run(ResponseErrorHandlerInitializer)
    .controller('ProcessEngineSelectionController', ProcessEngineSelectionController)
    .controller('AuthenticationController', AuthenticationController)
    .controller('NavigationController', NavigationController);
});
<commit_msg>fix(cockpit/page title): fix page title when logged out<commit_after>/* global ngDefine: false, angular: false */
ngDefine('camunda.common.pages', ['jquery'], function(module, $) {
  'use strict';
  var ResponseErrorHandlerInitializer = [
    '$rootScope', '$location', 'Notifications', 'Authentication',
    function($rootScope, $location, Notifications, Authentication) {

    function addError(error) {
      error.http = true;
      error.exclusive = [ 'http' ];

      Notifications.addError(error);
    }

    /**
     * A handler function that handles HTTP error responses,
     * i.e. 4XX and 5XX responses by redirecting / notifying the user.
     */
    function handleHttpError(event, error) {

      var status = error.status,
          data = error.data;

      switch (status) {
      case 500:
        if (data && data.message) {
          addError({
            status: 'Server Error',
            message: data.message,
            exceptionType: data.exceptionType
          });
        } else {
          addError({
            status: 'Server Error',
            message: 'The server reported an internal error. Try to refresh the page or login and out of the application.'
          });
        }
        break;

      case 0:
        addError({ status: 'Request Timeout', message:  'Your request timed out. Try to refresh the page.' });
        break;

      case 401:
        Authentication.clear();

        if ($location.absUrl().indexOf('/setup/#') == -1) {
          addError({ type: 'warning', status: 'Session ended', message: 'Your session timed out or was ended from another browser window. Please signin again.' });

          $('head title').text('camunda Login');

          $location.path('/login');
        } else {
          $location.path('/setup');
        }
        break;

      case 403:
        if (data.type == 'AuthorizationException') {
          addError({
            status: 'Access Denied',
            message: 'You are unauthorized to ' +
            data.permissionName.toLowerCase() + ' ' +
            data.resourceName.toLowerCase() +
            (data.resourceId ? ' ' + data.resourceId : 's') +
            '.' });
        } else {
          addError({
            status: 'Access Denied',
            message: 'Executing an action has been denied by the server. Try to refresh the page.'
          });
        }
        break;

      case 404:
        addError({ status: 'Not found', message: 'A resource you requested could not be found.' });
        break;
      default:
        addError({
          status: 'Communication Error',
          message: 'The application received an unexpected ' + status + ' response from the server. Try to refresh the page or login and out of the application.'
        });
      }
    }

    // triggered by httpStatusInterceptor
    $rootScope.$on('httpError', handleHttpError);
  }];

  var ProcessEngineSelectionController = [
    '$scope', '$http', '$location', '$window', 'Uri', 'Notifications',
    function($scope, $http, $location, $window, Uri, Notifications) {

    var current = Uri.appUri(':engine');
    var enginesByName = {};

    $http.get(Uri.appUri('engine://engine/')).then(function(response) {
      $scope.engines = response.data;

      angular.forEach($scope.engines , function(engine) {
        enginesByName[engine.name] = engine;
      });

      $scope.currentEngine = enginesByName[current];

      if (!$scope.currentEngine) {
        Notifications.addError({ status: 'Not found', message: 'The process engine you are trying to access does not exist' });
        $location.path('/');
      }
    });

    $scope.$watch('currentEngine', function(engine) {
      if (engine && current !== engine.name) {
        $window.location.href = Uri.appUri('app://../' + engine.name + '/');
      }
    });
  }];

  var NavigationController = [ '$scope', '$location', function($scope, $location) {

      $scope.activeClass = function(link) {
        var path = $location.absUrl();
        return path.indexOf(link) != -1 ? 'active' : '';
      };
  }];

  var AuthenticationController = [
    '$scope', '$window', '$cacheFactory', 'Notifications', 'AuthenticationService', 'Uri',
    function($scope, $window, $cacheFactory, Notifications, AuthenticationService, Uri) {
    // '$scope', '$window', '$cacheFactory', 'Notifications', 'AuthenticationService', 'Uri', 'page',
    // function($scope, $window, $cacheFactory, Notifications, AuthenticationService, Uri, page) {
      $scope.logout = function() {
        AuthenticationService.logout().then(function() {
          $cacheFactory.get('$http').removeAll();
          // page.titleSet('camunda Cockpit');
          $('head title').text('camunda Login');
          $window.location.href = Uri.appUri('app://#/login');
        });
      };
  }];

  module
    .run(ResponseErrorHandlerInitializer)
    .controller('ProcessEngineSelectionController', ProcessEngineSelectionController)
    .controller('AuthenticationController', AuthenticationController)
    .controller('NavigationController', NavigationController);
});
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

(function() {
	OC.Update = {
		_started : false,

		/**
		 * Start the upgrade process.
		 *
		 * @param $el progress list element
		 */
		start: function($el, options) {
			if (this._started) {
				return;
			}

			var hasWarnings = false;

			this.$el = $el;

			this._started = true;

			$(window).on('beforeunload.inprogress', function () {
				return t('core', 'The upgrade is in progress, leaving this page might interrupt the process in some environments.');
			});

			this.addMessage(t(
				'core',
				'Updating {productName} to version {version}, this may take a while.', {
					productName: options.productName || 'ownCloud',
					version: options.version
				}),
				'bold'
			).append('
'); // FIXME: these should be ul/li with CSS paddings!

			var updateEventSource = new OC.EventSource(OC.webroot+'/core/ajax/update.php');
			updateEventSource.listen('success', function(message) {
				$('').append(message).append('
').appendTo($el);
			});
			updateEventSource.listen('notice', function(message) {
				$('').addClass('error').append(message).append('
').appendTo($el);
				hasWarnings = true;
			});
			updateEventSource.listen('error', function(message) {
				message = message || t('core', 'An error occurred.');
				$(window).off('beforeunload.inprogress');
				$('').addClass('error').append(message).append('
').appendTo($el);
				message = t('core', 'Please reload the page.');
				$('').addClass('error').append(''+message+'
').appendTo($el);
				updateEventSource.close();
			});
			updateEventSource.listen('failure', function(message) {
				$(window).off('beforeunload.inprogress');
				$('').addClass('error').append(message).append('
').appendTo($el);
				$('')
				.addClass('bold')
				.append(t('core', 'The update was unsuccessful. ' +
					'Please report this issue to the ' +
					'ownCloud community.'))
				.appendTo($el);
			});
			updateEventSource.listen('done', function() {
				$(window).off('beforeunload.inprogress');

				if (hasWarnings) {
					$('').addClass('bold')
						.append('
')
						.append(t('core', 'The update was successful. There were warnings.'))
						.appendTo($el);
					var message = t('core', 'Please reload the page.');
					$('').append('
').append(message).append('
').appendTo($el);
				} else {
					// FIXME: use product name
					$('').addClass('bold')
						.append('
')
						.append(t('core', 'The update was successful. Redirecting you to ownCloud now.'))
						.appendTo($el);
					setTimeout(function () {
						OC.redirect(OC.webroot + '/');
					}, 3000);
				}
			});
		},

		addMessage: function(message, className) {
			var $span = $('');
			$span.addClass(className).append(message).append('
').appendTo(this.$el);
			return $span;
		}
	};

})();

$(document).ready(function() {
	$('.updateButton').on('click', function() {
		var $updateEl = $('.update');
		var $progressEl = $('.updateProgress');
		$progressEl.removeClass('hidden');
		$('.updateOverview').addClass('hidden');
		OC.Update.start($progressEl, {
			productName: $updateEl.attr('data-productname'),
			version: $updateEl.attr('data-version'),
		});
		return false;
	});
});
<commit_msg>Don't show "report this to the community" when the upgrade is unsupported<commit_after>/*
 * Copyright (c) 2014
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

(function() {
	OC.Update = {
		_started : false,

		/**
		 * Start the upgrade process.
		 *
		 * @param $el progress list element
		 */
		start: function($el, options) {
			if (this._started) {
				return;
			}

			var hasWarnings = false;

			this.$el = $el;

			this._started = true;

			$(window).on('beforeunload.inprogress', function () {
				return t('core', 'The upgrade is in progress, leaving this page might interrupt the process in some environments.');
			});

			this.addMessage(t(
				'core',
				'Updating {productName} to version {version}, this may take a while.', {
					productName: options.productName || 'ownCloud',
					version: options.version
				}),
				'bold'
			).append('
'); // FIXME: these should be ul/li with CSS paddings!

			var updateEventSource = new OC.EventSource(OC.webroot+'/core/ajax/update.php');
			updateEventSource.listen('success', function(message) {
				$('').append(message).append('
').appendTo($el);
			});
			updateEventSource.listen('notice', function(message) {
				$('').addClass('error').append(message).append('
').appendTo($el);
				hasWarnings = true;
			});
			updateEventSource.listen('error', function(message) {
				message = message || t('core', 'An error occurred.');
				$(window).off('beforeunload.inprogress');
				$('').addClass('error').append(message).append('
').appendTo($el);
				message = t('core', 'Please reload the page.');
				$('').addClass('error').append(''+message+'
').appendTo($el);
				updateEventSource.close();
			});
			updateEventSource.listen('failure', function(message) {
				$(window).off('beforeunload.inprogress');
				$('').addClass('error').append(message).append('
').appendTo($el);
				var span = $('')
					.addClass('bold');
				if(message === 'Exception: Updates between multiple major versions and downgrades are unsupported.') {
					span.append(t('core', 'The update was unsuccessful.'));
				} else {
					span.append(t('core', 'The update was unsuccessful. ' +
						'Please report this issue to the ' +
						'ownCloud community.'));
				}
				span.appendTo($el);
			});
			updateEventSource.listen('done', function() {
				$(window).off('beforeunload.inprogress');

				if (hasWarnings) {
					$('').addClass('bold')
						.append('
')
						.append(t('core', 'The update was successful. There were warnings.'))
						.appendTo($el);
					var message = t('core', 'Please reload the page.');
					$('').append('
').append(message).append('
').appendTo($el);
				} else {
					// FIXME: use product name
					$('').addClass('bold')
						.append('
')
						.append(t('core', 'The update was successful. Redirecting you to ownCloud now.'))
						.appendTo($el);
					setTimeout(function () {
						OC.redirect(OC.webroot + '/');
					}, 3000);
				}
			});
		},

		addMessage: function(message, className) {
			var $span = $('');
			$span.addClass(className).append(message).append('
').appendTo(this.$el);
			return $span;
		}
	};

})();

$(document).ready(function() {
	$('.updateButton').on('click', function() {
		var $updateEl = $('.update');
		var $progressEl = $('.updateProgress');
		$progressEl.removeClass('hidden');
		$('.updateOverview').addClass('hidden');
		OC.Update.start($progressEl, {
			productName: $updateEl.attr('data-productname'),
			version: $updateEl.attr('data-version'),
		});
		return false;
	});
});
<|endoftext|>


<commit_before>import {Experiment, ExperimentStep, toUIStep} from './experiment.model';

angular.module('materialscommons').component('mcExperiment', {
    templateUrl: 'app/project/experiments/experiment/mc-experiment.html',
    controller: MCExperimentComponentController
});

/*@ngInject*/
function MCExperimentComponentController($scope, $stateParams, moveStep, currentStep, experimentsService) {
    let ctrl = this;

    ctrl.$onInit = function() {
        ctrl.currentNode = null;
        ctrl.showSidebar = false;
        ctrl.experiment = null;

        experimentsService.getForProject($stateParams.project_id, $stateParams.experiment_id).then(
            (e) => {
                ctrl.experiment = e;
                if (!ctrl.experiment.steps.length) {
                    let s = {
                        name: '',
                        description: ''
                    };
                    experimentsService.createStep($stateParams.project_id, $stateParams.experiment_id, s).then(
                        (step) => {
                            toUIStep(step);
                            ctrl.experiment.steps.push(step);
                            currentStep.set(step);
                            ctrl.currentStep = currentStep.get();
                        },
                        (error) => console.log('error', error)
                    );
                } else {
                    ctrl.experiment.steps.forEach((step) => toUIStep(step));
                    currentStep.set(ctrl.experiment.steps[0]);
                    ctrl.currentStep = currentStep.get();
                }
            },
            (error) => console.log('error', error)
        );

        ctrl.editorOptions = {
            height: '20vh',
            width: '27vw'
        };

        ctrl.editorOptionsMaximized = {
            height: '55vh',
            width: '93vw'
        };
    };

    ctrl.moveLeft = () => moveStep.left(ctrl.currentNode, currentStep.get(), ctrl.experiment);
    ctrl.moveRight = () => moveStep.right(ctrl.currentNode, currentStep.get());
    ctrl.moveUp = () => moveStep.up(ctrl.currentNode, currentStep.get());
    ctrl.moveDown = () => moveStep.down(ctrl.currentNode, currentStep.get(), ctrl.experiment);
    ctrl.expandAll = () => $scope.$broadcast('angular-ui-tree:expand-all');
    ctrl.collapseAll = () => $scope.$broadcast('angular-ui-tree:collapse-all');
    ctrl.showStepMaximized = () => {
        if (!currentStep.get()) {
            return false;
        }
        return currentStep.get().displayState.maximize;
    };
    ctrl.openAll = openAll;
    ctrl.closeAll = closeAll;
    ctrl.getCurrentStep = () => currentStep.get();

    function openAll() {
        var treeModel = new TreeModel({childrenPropertyName: 'steps'}),
            root = treeModel.parse(ctrl.experiment);
        console.dir(root);
    }

    function closeAll() {

    }
}

<commit_msg>Every experiment has at least one step. There is no need to check if there is a step.<commit_after>import {Experiment, ExperimentStep, toUIStep} from './experiment.model';

angular.module('materialscommons').component('mcExperiment', {
    templateUrl: 'app/project/experiments/experiment/mc-experiment.html',
    controller: MCExperimentComponentController
});

/*@ngInject*/
function MCExperimentComponentController($scope, $stateParams, moveStep, currentStep, experimentsService) {
    let ctrl = this;

    ctrl.$onInit = () => {
        ctrl.currentNode = null;
        ctrl.showSidebar = false;
        ctrl.experiment = null;

        experimentsService.getForProject($stateParams.project_id, $stateParams.experiment_id).then(
            (e) => {
                ctrl.experiment = e;
                ctrl.experiment.steps.forEach((step) => toUIStep(step));
                currentStep.set(ctrl.experiment.steps[0]);
                ctrl.currentStep = currentStep.get();
            },
            (error) => console.log('error', error)
        );

        ctrl.editorOptions = {
            height: '20vh',
            width: '27vw'
        };

        ctrl.editorOptionsMaximized = {
            height: '55vh',
            width: '93vw'
        };
    };

    ctrl.moveLeft = () => moveStep.left(ctrl.currentNode, currentStep.get(), ctrl.experiment);
    ctrl.moveRight = () => moveStep.right(ctrl.currentNode, currentStep.get());
    ctrl.moveUp = () => moveStep.up(ctrl.currentNode, currentStep.get());
    ctrl.moveDown = () => moveStep.down(ctrl.currentNode, currentStep.get(), ctrl.experiment);
    ctrl.expandAll = () => $scope.$broadcast('angular-ui-tree:expand-all');
    ctrl.collapseAll = () => $scope.$broadcast('angular-ui-tree:collapse-all');
    ctrl.showStepMaximized = () => {
        if (!currentStep.get()) {
            return false;
        }
        return currentStep.get().displayState.maximize;
    };
    ctrl.openAll = openAll;
    ctrl.closeAll = closeAll;
    ctrl.getCurrentStep = () => currentStep.get();

    function openAll() {
        var treeModel = new TreeModel({childrenPropertyName: 'steps'}),
            root = treeModel.parse(ctrl.experiment);
        console.dir(root);
    }

    function closeAll() {

    }
}

<|endoftext|>


<commit_before>import { extractModuleSizesTree } from "./stats.js";

Plugin.registerMinifier({
    extensions: ['js'],
    archMatching: 'web',
  },
  () => new MeteorBabelMinifier()
);

class MeteorBabelMinifier {

  processFilesForBundle (files, options) {
    const mode = options.minifyMode;

    // don't minify anything for development
    if (mode === 'development') {
      files.forEach(function (file) {
        file.addJavaScript({
          data: file.getContentsAsBuffer(),
          sourceMap: file.getSourceMap(),
          path: file.getPathInBundle(),
        });
      });
      return;
    }

    // nested function
    // this function tries its best to locate the original source file
    // that the error being reported was located inside of
    function maybeThrowMinifyErrorBySourceFile(error, file) {
      // this regex matches error messages like "Unexpected token (1:12)"
      const minifierErrorRegex = /^(.*?)\s?\((\d+):(\d+)\)$/;
      const parseError = minifierErrorRegex.exec(error.message);

      if (!parseError) {
        // If we were unable to parse it, just let the usual error handling work.
        return;
      }

      const lineErrorMessage = parseError[1];
      const lineErrorLineNumber = parseError[2];

      const parseErrorContentIndex = lineErrorLineNumber - 1;

      // Unlikely, since we have a multi-line fixed header in this file.
      if (parseErrorContentIndex < 0) {
        return;
      }

      /*

      What we're parsing looks like this:

      /////////////////////////////////////////
      //                                     //
      // path/to/file.js                     //
      //                                     //
      /////////////////////////////////////////
                                            // 1
        var illegalECMAScript = true;       // 2
                                            // 3
      /////////////////////////////////////////

      Btw, the above code is intentionally not newer ECMAScript so
      we don't break ourselves.

      */

      const contents = file.getContentsAsString().split(/\n/);
      const lineContent = contents[parseErrorContentIndex];

      // Try to grab the line number, which sometimes doesn't exist on
      // line, abnormally-long lines in a larger block.
      const lineSrcLineParts = /^(.*?)(?:\s*\/\/ (\d+))?$/.exec(lineContent);

      // The line didn't match at all?  Let's just not try.
      if (!lineSrcLineParts) {
        return;
      }

      const lineSrcLineContent = lineSrcLineParts[1];
      const lineSrcLineNumber = lineSrcLineParts[2];

      // Count backward from the failed line to find the filename.
      for (let c = parseErrorContentIndex - 1; c >= 0; c--) {
        let sourceLine = contents[c];

        // If the line is a boatload of slashes, we're in the right place.
        if (/^\/\/\/{6,}$/.test(sourceLine)) {

          // If 4 lines back is the same exact line, we've found the framing.
          if (contents[c - 4] === sourceLine) {

            // So in that case, 2 lines back is the file path.
            let parseErrorPath = contents[c - 2]
              .substring(3)
              .replace(/\s+\/\//, "");

            let minError = new Error(
              "babel-minify minification error " +
              "within " + file.getPathInBundle() + ":\n" +
              parseErrorPath +
              (lineSrcLineNumber ? ", line " + lineSrcLineNumber : "") + "\n" +
              "\n" +
              lineErrorMessage + ":\n" +
              "\n" +
              lineSrcLineContent + "\n"
            );

            throw minError;
          }
        }
      }
    }

    // this object will collect all the minified code in the
    // data field and then post-minfiication file sizes in
    // stats field
    const toBeAdded = {
      data: "",
      stats: Object.create(null)
    };

    files.forEach(file => {
      // Don't reminify *.min.js.
      if (/\.min\.js$/.test(file.getPathInBundle())) {
        toBeAdded.data += file.getContentsAsString();
      }
      else {
        let minified;
        try {
          minified = meteorJsMinify(file.getContentsAsString());
        }
        catch (err) {
          const filePath = file.getPathInBundle();

          maybeThrowMinifyErrorBySourceFile(err, file);

          err.message += " while minifying " + filePath;
          throw err;
        }

        const ast = extractModuleSizesTree(minified.code);

        if (ast) {
          toBeAdded.stats[file.getPathInBundle()] = [Buffer.byteLength(minified.code), ast];
        } else {
          toBeAdded.stats[file.getPathInBundle()] = Buffer.byteLength(minified.code);
        }
        // append the minified code to the "running sum"
        // of code being minified
        toBeAdded.data += minified.code;
      }

      toBeAdded.data += '\n\n';

      Plugin.nudge();
    });

    // this is where the minified code gets added to one
    // JS file that is delivered to the client
    if (files.length) {
      files[0].addJavaScript(toBeAdded);
    }
  }
}<commit_msg>update the error handling to match what terser returns in the error it throws<commit_after>import { extractModuleSizesTree } from "./stats.js";

Plugin.registerMinifier({
    extensions: ['js'],
    archMatching: 'web',
  },
  () => new MeteorBabelMinifier()
);

class MeteorBabelMinifier {

  processFilesForBundle (files, options) {
    const mode = options.minifyMode;

    // don't minify anything for development
    if (mode === 'development') {
      files.forEach(function (file) {
        file.addJavaScript({
          data: file.getContentsAsBuffer(),
          sourceMap: file.getSourceMap(),
          path: file.getPathInBundle(),
        });
      });
      return;
    }

      // nested function
    // this function tries its best to locate the original source file
    // that the error being reported was located inside of
    function maybeThrowMinifyErrorBySourceFile(error, file) {

      const lines = file.getContentsAsString().split(/\n/);
      const lineContent = lines[error.line - 1];

      // Count backward from the failed line to find the filename.
      for (let i = error.line - 1; i >= 0; i--) {
        let currentLine = lines[i];

        // If the line is a boatload of slashes, we're in the right place.
        if (/^\/\/\/{6,}$/.test(currentLine)) {

          // If 4 lines back is the same exact line, we've found the framing.
          if (contents[i - 4] === currentLine) {

            // So in that case, 2 lines back is the file path.
            let originalFilePath = contents[c - 2].substring(3).replace(/\s+\/\//, "");

            throw new Error(
              "terser minification error within" +
              file.getPathInBundle() + ":\n" +
              originalFilePath +
              ", line " + error.line + "\n\n" +
              error.message + ":\n\n" +
              lineContent + "\n"
            );
          }
        }
      }
    }

    // this object will collect all the minified code in the
    // data field and then post-minfiication file sizes in
    // stats field
    const toBeAdded = {
      data: "",
      stats: Object.create(null)
    };

    files.forEach(file => {
      // Don't reminify *.min.js.
      if (/\.min\.js$/.test(file.getPathInBundle())) {
        toBeAdded.data += file.getContentsAsString();
      }
      else {
        let minified;
        try {
          minified = meteorJsMinify(file.getContentsAsString());
        }
        catch (err) {
          const filePath = file.getPathInBundle();

          maybeThrowMinifyErrorBySourceFile(err, file);

          err.message += " while minifying " + filePath;
          throw err;
        }

        const ast = extractModuleSizesTree(minified.code);

        if (ast) {
          toBeAdded.stats[file.getPathInBundle()] = [Buffer.byteLength(minified.code), ast];
        } else {
          toBeAdded.stats[file.getPathInBundle()] = Buffer.byteLength(minified.code);
        }
        // append the minified code to the "running sum"
        // of code being minified
        toBeAdded.data += minified.code;
      }

      toBeAdded.data += '\n\n';

      Plugin.nudge();
    });

    // this is where the minified code gets added to one
    // JS file that is delivered to the client
    if (files.length) {
      files[0].addJavaScript(toBeAdded);
    }
  }
}<|endoftext|>


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

const {
  getMainEntry,
  getPlatformCompiler
} = require('@dcloudio/uni-cli-shared')

const {
  isUnaryTag,
  getPartialIdentifier
} = require('../util')

// const {
//   createTemplateCacheLoader
// } = require('../cache-loader')

function getProvides () {
  return {
    '__f__': [path.resolve(__dirname, '../format-log.js'), 'default'],
    'Behavior': ['@dcloudio/uni-mp-weixin/dist/mp.js', 'Behavior'],
    'Component': ['@dcloudio/uni-mp-weixin/dist/mp.js', 'Component']
  }
}

const v3 = {
  vueConfig: {
    parallel: false
  },
  webpackConfig (webpackConfig, vueOptions, api) {
    const isAppService = !!vueOptions.pluginOptions['uni-app-plus']['service']
    const isAppView = !!vueOptions.pluginOptions['uni-app-plus']['view']

    const statCode = process.env.UNI_USING_STAT ? `import '@dcloudio/uni-stat';` : ''

    const beforeCode = `import 'uni-pages';`

    if (!webpackConfig.optimization) {
      webpackConfig.optimization = {}
    }
    // disable noEmitOnErrors
    webpackConfig.optimization.noEmitOnErrors = false

    if (isAppService) {
      webpackConfig.optimization.runtimeChunk = {
        name: 'app-config'
      }
    } else if (isAppView) {
      webpackConfig.optimization.runtimeChunk = false
    }

    webpackConfig.optimization.splitChunks = false

    let devtool = false

    const rules = []

    if (isAppView) {
      rules.push({ // 解析组件，css 等
        resourceQuery: /vue&type=script/,
        use: [{
          loader: path.resolve(__dirname,
            '../../packages/webpack-uni-app-loader/view/script')
        }]
      })
      rules.push({
        resourceQuery: [/lang=wxs/, /blockType=wxs/],
        use: [{
          loader: path.resolve(__dirname, '../../packages/webpack-uni-filter-loader')
        }]
      })
    }

    const entry = {}
    if (isAppService) {
      entry['app-service'] = path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
    } else if (isAppView) {
      entry['app-view'] = path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
    }

    return {
      devtool,
      mode: process.env.NODE_ENV,
      externals: {
        vue: 'Vue'
      },
      entry () {
        return entry
      },
      output: {
        filename: '[name].js',
        chunkFilename: '[id].js',
        globalObject: 'this'
      },
      performance: {
        hints: false
      },
      resolve: {
        extensions: ['.nvue']
      },
      resolveLoader: {
        alias: {
          'vue-style-loader': path.resolve(__dirname, '../../packages/app-vue-style-loader')
        }
      },
      module: {
        rules: [{
          test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
          use: [{
            loader: isAppService ? 'wrap-loader' : path.resolve(__dirname,
              '../../packages/webpack-uni-app-loader/view/main.js'),
            options: {
              compiler: getPlatformCompiler(),
              before: [
                beforeCode + statCode
              ]
            }
          }]
        },
        {
          resourceQuery: /vue&type=template/,
          use: [{
            loader: path.resolve(__dirname,
              '../../packages/webpack-uni-app-loader/filter-modules-template.js')
          }]
        },
        ...rules
          // v3 暂不支持 cache
          // createTemplateCacheLoader(api,
          //   isAppService
          //     ? 'uni-template-compiler-service'
          //     : 'uni-template-compiler-view'
          // )
        ]
      },
      plugins: [
        new webpack.ProvidePlugin(getProvides())
      ]
    }
  },
  chainWebpack (webpackConfig, vueOptions, api) {
    webpackConfig.entryPoints.delete('app')

    const isAppService = !!vueOptions.pluginOptions['uni-app-plus']['service']
    const isAppView = !!vueOptions.pluginOptions['uni-app-plus']['view']

    // 处理静态资源
    webpackConfig.module
      .rule('images')
      .use('url-loader')
      .loader('url-loader')
      .tap(options => Object.assign(options, {
        limit: 1,
        fallback: {
          loader: 'file-loader',
          options: {
            emitFile: isAppView,
            name: '[name].[ext]',
            useRelativePath: true,
            context: process.env.UNI_INPUT_DIR
          }
        }
      }))

    const cacheConfig = {
      cacheDirectory: false,
      cacheIdentifier: false
    }

    if (process.env.UNI_USING_CACHE) {
      Object.assign(cacheConfig, api.genCacheConfig(
        'vue-template-compiler/' + process.env.UNI_PLATFORM,
        getPartialIdentifier()
      ))
    }

    const compilerOptions = {
      isUnaryTag,
      preserveWhitespace: false,
      service: isAppService,
      view: isAppView
    }

    // disable vue cache-loader
    webpackConfig.module
      .rule('vue')
      .test([/\.vue$/, /\.nvue$/])
      .use('vue-loader') //  service 层移除 style 节点，view 层返回固定 script
      .loader(path.resolve(__dirname, '../../packages/vue-loader/lib'))
      .tap(options => Object.assign(options, {
        isAppService,
        isAppView,
        compiler: getPlatformCompiler(),
        compilerOptions
      }, cacheConfig))
      .end()
      .use('uniapp-custom-block-loader')
      .loader(require.resolve('@dcloudio/vue-cli-plugin-uni/packages/webpack-custom-block-loader'))
      .options({
        compiler: getPlatformCompiler()
      })

    // 是否启用 cache
    if (process.env.UNI_USING_CACHE) {
      webpackConfig.module
        .rule('vue')
        .use('cache-loader')
        .tap(options => Object.assign(options, api.genCacheConfig(
          'vue-loader/' + process.env.UNI_PLATFORM,
          getPartialIdentifier()
        )))
    } else {
      webpackConfig.module
        .rule('vue')
        .uses
        .delete('cache-loader')
    }

    if (isAppView) {
      if (process.env.NODE_ENV === 'production') {
        require('../h5/cssnano-options')(webpackConfig)
      }
    }

    webpackConfig.plugins.delete('hmr')
    webpackConfig.plugins.delete('html')
    webpackConfig.plugins.delete('copy')
    webpackConfig.plugins.delete('preload')
    webpackConfig.plugins.delete('prefetch')
  }
}
if (process.env.UNI_USING_V3) {
  module.exports = v3
} else {
  module.exports = require('../mp')
}
<commit_msg>feat(v3): change svg,midia limit<commit_after>const path = require('path')
const webpack = require('webpack')

const {
  getMainEntry,
  getPlatformCompiler
} = require('@dcloudio/uni-cli-shared')

const {
  isUnaryTag,
  getPartialIdentifier
} = require('../util')

// const {
//   createTemplateCacheLoader
// } = require('../cache-loader')

function getProvides () {
  return {
    '__f__': [path.resolve(__dirname, '../format-log.js'), 'default'],
    'Behavior': ['@dcloudio/uni-mp-weixin/dist/mp.js', 'Behavior'],
    'Component': ['@dcloudio/uni-mp-weixin/dist/mp.js', 'Component']
  }
}

const v3 = {
  vueConfig: {
    parallel: false
  },
  webpackConfig (webpackConfig, vueOptions, api) {
    const isAppService = !!vueOptions.pluginOptions['uni-app-plus']['service']
    const isAppView = !!vueOptions.pluginOptions['uni-app-plus']['view']

    const statCode = process.env.UNI_USING_STAT ? `import '@dcloudio/uni-stat';` : ''

    const beforeCode = `import 'uni-pages';`

    if (!webpackConfig.optimization) {
      webpackConfig.optimization = {}
    }
    // disable noEmitOnErrors
    webpackConfig.optimization.noEmitOnErrors = false

    if (isAppService) {
      webpackConfig.optimization.runtimeChunk = {
        name: 'app-config'
      }
    } else if (isAppView) {
      webpackConfig.optimization.runtimeChunk = false
    }

    webpackConfig.optimization.splitChunks = false

    let devtool = false

    const rules = []

    if (isAppView) {
      rules.push({ // 解析组件，css 等
        resourceQuery: /vue&type=script/,
        use: [{
          loader: path.resolve(__dirname,
            '../../packages/webpack-uni-app-loader/view/script')
        }]
      })
      rules.push({
        resourceQuery: [/lang=wxs/, /blockType=wxs/],
        use: [{
          loader: path.resolve(__dirname, '../../packages/webpack-uni-filter-loader')
        }]
      })
    }

    const entry = {}
    if (isAppService) {
      entry['app-service'] = path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
    } else if (isAppView) {
      entry['app-view'] = path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
    }

    return {
      devtool,
      mode: process.env.NODE_ENV,
      externals: {
        vue: 'Vue'
      },
      entry () {
        return entry
      },
      output: {
        filename: '[name].js',
        chunkFilename: '[id].js',
        globalObject: 'this'
      },
      performance: {
        hints: false
      },
      resolve: {
        extensions: ['.nvue']
      },
      resolveLoader: {
        alias: {
          'vue-style-loader': path.resolve(__dirname, '../../packages/app-vue-style-loader')
        }
      },
      module: {
        rules: [{
          test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
          use: [{
            loader: isAppService ? 'wrap-loader' : path.resolve(__dirname,
              '../../packages/webpack-uni-app-loader/view/main.js'),
            options: {
              compiler: getPlatformCompiler(),
              before: [
                beforeCode + statCode
              ]
            }
          }]
        },
        {
          resourceQuery: /vue&type=template/,
          use: [{
            loader: path.resolve(__dirname,
              '../../packages/webpack-uni-app-loader/filter-modules-template.js')
          }]
        },
        ...rules
          // v3 暂不支持 cache
          // createTemplateCacheLoader(api,
          //   isAppService
          //     ? 'uni-template-compiler-service'
          //     : 'uni-template-compiler-view'
          // )
        ]
      },
      plugins: [
        new webpack.ProvidePlugin(getProvides())
      ]
    }
  },
  chainWebpack (webpackConfig, vueOptions, api) {
    webpackConfig.entryPoints.delete('app')

    const isAppService = !!vueOptions.pluginOptions['uni-app-plus']['service']
    const isAppView = !!vueOptions.pluginOptions['uni-app-plus']['view']

    // 处理静态资源
    const staticTypes = ['images', 'svg', 'media', 'fonts']
    staticTypes.forEach(staticType => {
      webpackConfig.module
        .rule(staticType)
        .use('url-loader')
        .loader('url-loader')
        .tap(options => Object.assign(options, {
          limit: 1,
          fallback: {
            loader: 'file-loader',
            options: {
              emitFile: isAppView,
              name: '[name].[ext]',
              useRelativePath: true,
              context: process.env.UNI_INPUT_DIR
            }
          }
        }))
    })

    const cacheConfig = {
      cacheDirectory: false,
      cacheIdentifier: false
    }

    if (process.env.UNI_USING_CACHE) {
      Object.assign(cacheConfig, api.genCacheConfig(
        'vue-template-compiler/' + process.env.UNI_PLATFORM,
        getPartialIdentifier()
      ))
    }

    const compilerOptions = {
      isUnaryTag,
      preserveWhitespace: false,
      service: isAppService,
      view: isAppView
    }

    // disable vue cache-loader
    webpackConfig.module
      .rule('vue')
      .test([/\.vue$/, /\.nvue$/])
      .use('vue-loader') //  service 层移除 style 节点，view 层返回固定 script
      .loader(path.resolve(__dirname, '../../packages/vue-loader/lib'))
      .tap(options => Object.assign(options, {
        isAppService,
        isAppView,
        compiler: getPlatformCompiler(),
        compilerOptions
      }, cacheConfig))
      .end()
      .use('uniapp-custom-block-loader')
      .loader(require.resolve('@dcloudio/vue-cli-plugin-uni/packages/webpack-custom-block-loader'))
      .options({
        compiler: getPlatformCompiler()
      })

    // 是否启用 cache
    if (process.env.UNI_USING_CACHE) {
      webpackConfig.module
        .rule('vue')
        .use('cache-loader')
        .tap(options => Object.assign(options, api.genCacheConfig(
          'vue-loader/' + process.env.UNI_PLATFORM,
          getPartialIdentifier()
        )))
    } else {
      webpackConfig.module
        .rule('vue')
        .uses
        .delete('cache-loader')
    }

    if (isAppView) {
      if (process.env.NODE_ENV === 'production') {
        require('../h5/cssnano-options')(webpackConfig)
      }
    }

    webpackConfig.plugins.delete('hmr')
    webpackConfig.plugins.delete('html')
    webpackConfig.plugins.delete('copy')
    webpackConfig.plugins.delete('preload')
    webpackConfig.plugins.delete('prefetch')
  }
}
if (process.env.UNI_USING_V3) {
  module.exports = v3
} else {
  module.exports = require('../mp')
}
<|endoftext|>