Remove underscore

This commit is contained in:
Fedor Indutny 2022-11-29 16:53:39 -08:00 committed by GitHub
parent 704107a256
commit 9d8ad21819
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 111 additions and 226 deletions

View file

@ -3187,32 +3187,6 @@ Signal Desktop makes use of the following open source projects.
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
## underscore
Copyright (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative
Reporters & Editors
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
## uuid
License: MIT

View file

@ -178,7 +178,6 @@
"split2": "4.0.0",
"testcheck": "1.0.0-rc.2",
"typeface-inter": "3.18.1",
"underscore": "1.12.1",
"uuid": "3.3.2",
"websocket": "1.0.28",
"zod": "3.5.1"
@ -254,7 +253,6 @@
"@types/sinon": "10.0.2",
"@types/split2": "3.2.1",
"@types/terser-webpack-plugin": "5.0.3",
"@types/underscore": "1.10.3",
"@types/uuid": "3.4.4",
"@types/webpack-dev-server": "3.11.3",
"@types/websocket": "1.0.0",
@ -454,6 +452,7 @@
"sticker-creator/preload.js",
"sticker-creator/window/*.js",
"sticker-creator/dist/**",
"!node_modules/underscore/**",
"!node_modules/emoji-datasource/emoji_pretty.json",
"!node_modules/emoji-datasource/**/*.png",
"!node_modules/emoji-datasource-apple/emoji_pretty.json",

View file

@ -0,0 +1,32 @@
diff --git a/node_modules/backbone/backbone.js b/node_modules/backbone/backbone.js
index 3e09d0d..c650ed6 100644
--- a/node_modules/backbone/backbone.js
+++ b/node_modules/backbone/backbone.js
@@ -12,24 +12,9 @@
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global;
- // Set up Backbone appropriately for the environment. Start with AMD.
- if (typeof define === 'function' && define.amd) {
- define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
- // Export global even in AMD case in case this script is loaded with
- // others that may still expect a global Backbone.
- root.Backbone = factory(root, exports, _, $);
- });
-
- // Next for Node.js or CommonJS. jQuery may not be needed as a module.
- } else if (typeof exports !== 'undefined') {
- var _ = require('underscore'), $;
- try { $ = require('jquery'); } catch (e) {}
- factory(root, exports, _, $);
-
- // Finally, as a browser global.
- } else {
- root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
- }
+ var _ = require('lodash'), $;
+ try { $ = require('jquery'); } catch (e) {}
+ factory(root, exports, _, $);
})(function(root, Backbone, _, $) {

View file

@ -42,7 +42,6 @@ const bundleDefaults = {
'websocket',
// Things that don't bundle well
'backbone',
'got',
'jquery',
'node-fetch',

View file

@ -39,6 +39,6 @@ before(async () => {
});
window.Whisper = window.Whisper || {};
window.Whisper.events = _.clone(Backbone.Events);
window.Whisper.events = { ...Backbone.Events };
window.textsecure.storage.protocol = new window.SignalProtocolStore();

View file

@ -1,32 +0,0 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/*
* Defines a default definition for render() which allows sub-classes
* to simply specify a template property and renderAttributes which are plugged
* into Mustache.render
*/
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
window.Whisper.View = Backbone.View.extend({
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
constructor(...params: Array<any>) {
window.Backbone.View.call(this, ...params);
// Checks for syntax errors
window.Mustache.parse(_.result(this, 'template'));
},
render_attributes() {
return _.result(this.model, 'attributes', {});
},
render() {
const attrs = window._.result(this, 'render_attributes', {});
const template = window._.result(this, 'template', '');
this.$el.html(window.Mustache.render(template, attrs));
return this;
},
});
})();

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { webFrame } from 'electron';
import { isNumber } from 'lodash';
import { isNumber, clone, debounce } from 'lodash';
import { bindActionCreators } from 'redux';
import { render } from 'react-dom';
import { batch as batchDispatch } from 'react-redux';
@ -197,7 +197,7 @@ export async function startApp(): Promise<void> {
await KeyboardLayout.initialize();
window.Whisper.events = window._.clone(window.Backbone.Events);
window.Whisper.events = clone(window.Backbone.Events);
window.Signal.Util.MessageController.install();
window.Signal.conversationControllerStart();
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
@ -1753,7 +1753,7 @@ export async function startApp(): Promise<void> {
window.Whisper.events.on(
'mightBeUnlinked',
window._.debounce(enqueueReconnectToWebSocket, 1000, { maxWait: 5000 })
debounce(enqueueReconnectToWebSocket, 1000, { maxWait: 5000 })
);
window.Whisper.events.on('unlinkAndDisconnect', () => {
@ -3137,10 +3137,8 @@ export async function startApp(): Promise<void> {
let unidentifiedDeliveries: Array<string> = [];
if (unidentifiedStatus.length) {
const unidentified = window._.filter(data.unidentifiedStatus, item =>
Boolean(item.unidentified)
);
unidentifiedDeliveries = unidentified
unidentifiedDeliveries = unidentifiedStatus
.filter(item => Boolean(item.unidentified))
.map(item => item.destinationUuid || item.destination)
.filter(isNotNil);
}

4
ts/model-types.d.ts vendored
View file

@ -435,10 +435,6 @@ export type GroupV2PendingAdminApprovalType = {
timestamp: number;
};
export type VerificationOptions = {
key?: null | Uint8Array;
};
export type ShallowChallengeError = CustomError & {
readonly retryAfter: number;
readonly data: SendMessageChallengeData;

View file

@ -1,7 +1,15 @@
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { compact, has, isNumber, throttle, debounce } from 'lodash';
import {
compact,
has,
isNumber,
throttle,
debounce,
head,
sortBy,
} from 'lodash';
import { batch as batchDispatch } from 'react-redux';
import { v4 as generateGuid } from 'uuid';
import PQueue from 'p-queue';
@ -13,7 +21,6 @@ import type {
MessageAttributesType,
QuotedMessageType,
SenderKeyInfoType,
VerificationOptions,
} from '../model-types.d';
import { getInitials } from '../util/getInitials';
import { normalizeUuid } from '../util/normalizeUuid';
@ -468,7 +475,7 @@ export class ConversationModel extends window.Backbone
return false;
}
return window._.any(membersV2, item => item.uuid === uuid.toString());
return membersV2.some(item => item.uuid === uuid.toString());
}
async updateExpirationTimerInGroupV2(
@ -1792,10 +1799,8 @@ export class ConversationModel extends window.Backbone
}
}
const typingValues = window._.values(this.contactTypingTimers || {});
const typingMostRecent = window._.first(
window._.sortBy(typingValues, 'timestamp')
);
const typingValues = Object.values(this.contactTypingTimers || {});
const typingMostRecent = head(sortBy(typingValues, 'timestamp'));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const timestamp = this.get('timestamp')!;
@ -2675,36 +2680,24 @@ export class ConversationModel extends window.Backbone
);
}
setVerifiedDefault(options?: VerificationOptions): Promise<boolean> {
setVerifiedDefault(): Promise<boolean> {
const { DEFAULT } = this.verifiedEnum;
return this.queueJob('setVerifiedDefault', () =>
this._setVerified(DEFAULT, options)
this._setVerified(DEFAULT)
);
}
setVerified(options?: VerificationOptions): Promise<boolean> {
setVerified(): Promise<boolean> {
const { VERIFIED } = this.verifiedEnum;
return this.queueJob('setVerified', () =>
this._setVerified(VERIFIED, options)
);
return this.queueJob('setVerified', () => this._setVerified(VERIFIED));
}
setUnverified(options: VerificationOptions): Promise<boolean> {
setUnverified(): Promise<boolean> {
const { UNVERIFIED } = this.verifiedEnum;
return this.queueJob('setUnverified', () =>
this._setVerified(UNVERIFIED, options)
);
return this.queueJob('setUnverified', () => this._setVerified(UNVERIFIED));
}
private async _setVerified(
verified: number,
providedOptions?: VerificationOptions
): Promise<boolean> {
const options = providedOptions || {};
window._.defaults(options, {
key: null,
});
private async _setVerified(verified: number): Promise<boolean> {
const { VERIFIED, DEFAULT } = this.verifiedEnum;
if (!isDirectConversation(this.attributes)) {
@ -2886,13 +2879,13 @@ export class ConversationModel extends window.Backbone
if (isDirectConversation(this.attributes)) {
return this.safeIsUntrusted(timestampThreshold);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.contactCollection!.length) {
const { contactCollection } = this;
if (!contactCollection?.length) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.contactCollection!.any(contact => {
return contactCollection.some(contact => {
if (isMe(contact.attributes)) {
return false;
}
@ -3166,7 +3159,7 @@ export class ConversationModel extends window.Backbone
if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => {
window._.forEach(groups, group => {
groups.forEach(group => {
group.addVerifiedChange(this.id, verified, options);
});
}
@ -3303,7 +3296,7 @@ export class ConversationModel extends window.Backbone
if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => {
window._.forEach(groups, group => {
groups.forEach(group => {
group.addProfileChange(profileChange, this.id);
});
}

View file

@ -2,14 +2,21 @@
// SPDX-License-Identifier: AGPL-3.0-only
import {
groupBy,
difference,
isEmpty,
isEqual,
isNumber,
isObject,
mapValues,
maxBy,
noop,
omit,
partition,
pick,
reject,
union,
without,
} from 'lodash';
import type {
CustomError,
@ -262,8 +269,6 @@ type PropsForMessageDetail = Pick<
| 'expirationTimestamp'
>;
declare const _: typeof window._;
window.Whisper = window.Whisper || {};
const { Message: TypedMessage } = window.Signal.Types;
@ -309,7 +314,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
cachedOutgoingStickerData?: StickerWithHydratedData;
override initialize(attributes: unknown): void {
if (_.isObject(attributes)) {
if (isObject(attributes)) {
this.set(
TypedMessage.initializeSchemaVersion({
message: attributes as MessageAttributesType,
@ -516,10 +521,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// If an error has a specific number it's associated with, we'll show it next to
// that contact. Otherwise, it will be a standalone entry.
const errors = _.reject(allErrors, error =>
const errors = reject(allErrors, error =>
Boolean(error.identifier || error.number)
);
const errorsGroupedById = _.groupBy(allErrors, error => {
const errorsGroupedById = groupBy(allErrors, error => {
const identifier = error.identifier || error.number;
if (!identifier) {
return null;
@ -747,7 +752,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const joinedContacts = _.map(groupUpdate.joined, item =>
const joinedContacts = groupUpdate.joined.map(item =>
window.ConversationController.getOrCreate(item, 'private')
);
const joinedWithoutMe = joinedContacts.filter(
@ -757,7 +762,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (joinedContacts.length > 1) {
messages.push(
window.i18n('multipleJoinedTheGroup', [
_.map(joinedWithoutMe, contact => contact.getTitle()).join(', '),
joinedWithoutMe.map(contact => contact.getTitle()).join(', '),
])
);
@ -1026,7 +1031,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
override validate(attributes: Record<string, unknown>): void {
const required = ['conversationId', 'received_at', 'sent_at'];
const missing = _.filter(required, attr => !attributes[attr]);
const missing = required.filter(attr => !attributes[attr]);
if (missing.length) {
log.warn(`Message missing attributes: ${missing}`);
}
@ -1366,7 +1371,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// We check instanceof second because typescript believes that anything that comes
// through here must be an instance of Error, so e is 'never' after that check.
if ((e.message && e.stack) || e instanceof Error) {
return _.pick(
return pick(
e,
'name',
'message',
@ -1523,7 +1528,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
removeOutgoingErrors(incomingIdentifier: string): CustomError {
const incomingConversationId =
window.ConversationController.getConversationId(incomingIdentifier);
const errors = _.partition(
const errors = partition(
this.get('errors'),
e =>
window.ConversationController.getConversationId(
@ -2386,12 +2391,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
const existingRevision = conversation.get('revision');
const isFirstUpdate = !_.isNumber(existingRevision);
const isFirstUpdate = !isNumber(existingRevision);
// Standard GroupV2 modification codepath
const isV2GroupUpdate =
initialMessage.groupV2 &&
_.isNumber(initialMessage.groupV2.revision) &&
isNumber(initialMessage.groupV2.revision) &&
(isFirstUpdate ||
initialMessage.groupV2.revision > existingRevision);
@ -2708,7 +2713,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
attributes = {
...attributes,
name: initialMessage.group.name,
members: _.union(members, conversation.get('members')),
members: union(members, conversation.get('members')),
};
if (initialMessage.group.name !== conversation.get('name')) {
@ -2776,14 +2781,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
const difference = _.difference(
const differentMembers = difference(
members,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
conversation.get('members')!
);
if (difference.length > 0) {
if (differentMembers.length > 0) {
// Because GroupV1 groups are based on e164 only
const maybeE164s = map(difference, id =>
const maybeE164s = map(differentMembers, id =>
window.ConversationController.get(id)?.get('e164')
);
const e164s = filter(maybeE164s, isNotNil);
@ -2813,7 +2818,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} else {
pendingGroupUpdate.left = sender.get('id');
}
attributes.members = _.without(
attributes.members = without(
conversation.get('members'),
sender.get('id')
);

View file

@ -14,7 +14,6 @@ const CONCAT_TARGET = join(__dirname, '../../js/components.js');
const CONCAT_SOURCES = [
join(BASE_NODE, 'jquery/dist/jquery.js'),
join(BASE_NODE, 'mustache/mustache.js'),
join(BASE_NODE, 'underscore/underscore.js'),
join(BASE_BOWER, 'webaudiorecorder/lib/WebAudioRecorder.js'),
];

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { pick } from 'underscore';
import { pick } from 'lodash';
import { MessageAudio } from '../../components/conversation/MessageAudio';
import type { OwnProps as MessageAudioOwnProps } from '../../components/conversation/MessageAudio';

View file

@ -5,6 +5,7 @@
import chai, { assert } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { clone } from 'lodash';
import {
Direction,
IdentityKeyPair,
@ -563,7 +564,7 @@ describe('SignalProtocolStore', () => {
describe('with invalid attributes', () => {
let attributes: IdentityKeyType;
beforeEach(() => {
attributes = window._.clone(validAttributes);
attributes = clone(validAttributes);
});
async function testInvalidAttributes() {

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { pick } from 'lodash';
import { isOverHourIntoPast, cleanupSessionResets } from '../background';
@ -38,7 +39,7 @@ describe('#cleanupSessionResets', () => {
cleanupSessionResets();
const actual = window.storage.get('sessionResets');
const expected = window._.pick(startValue, ['one', 'two']);
const expected = pick(startValue, ['one', 'two']);
assert.deepEqual(actual, expected);
});
it('filters out falsey items', () => {
@ -50,7 +51,7 @@ describe('#cleanupSessionResets', () => {
cleanupSessionResets();
const actual = window.storage.get('sessionResets');
const expected = window._.pick(startValue, ['two']);
const expected = pick(startValue, ['two']);
assert.deepEqual(actual, expected);
assert.deepEqual(Object.keys(startValue), ['two']);

View file

@ -1,41 +0,0 @@
// Copyright 2015-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
describe('Whisper.View', () => {
it('renders a template with render_attributes', () => {
const ViewClass = window.Whisper.View.extend({
template: '<div>{{ variable }}</div>',
render_attributes: {
variable: 'value',
},
});
const view = new ViewClass();
view.render();
assert.strictEqual(view.$el.html(), '<div>value</div>');
});
it('renders a template with no render_attributes', () => {
const ViewClass = window.Whisper.View.extend({
template: '<div>static text</div>',
});
const view = new ViewClass();
view.render();
assert.strictEqual(view.$el.html(), '<div>static text</div>');
});
it('renders a template function with render_attributes function', () => {
const ViewClass = window.Whisper.View.extend({
template() {
return '<div>{{ variable }}</div>';
},
render_attributes() {
return { variable: 'value' };
},
});
const view = new ViewClass();
view.render();
assert.strictEqual(view.$el.html(), '<div>value</div>');
});
});

View file

@ -399,13 +399,24 @@ export function createIPCEvents(
},
addDarkOverlay: () => {
if ($('.dark-overlay').length) {
const elems = document.querySelectorAll('.dark-overlay');
if (elems.length) {
return;
}
$(document.body).prepend('<div class="dark-overlay"></div>');
$('.dark-overlay').on('click', () => $('.dark-overlay').remove());
const newOverlay = document.createElement('div');
newOverlay.className = 'dark-overlay';
newOverlay.addEventListener('click', () => {
newOverlay.remove();
});
document.body.prepend(newOverlay);
},
removeDarkOverlay: () => {
const elems = document.querySelectorAll('.dark-overlay');
for (const elem of elems) {
elem.remove();
}
},
removeDarkOverlay: () => $('.dark-overlay').remove(),
showKeyboardShortcuts: () => window.showKeyboardShortcuts(),
deleteAllData: async () => {

View file

@ -8522,14 +8522,6 @@
"reasonCategory": "falseMatch",
"updated": "2020-07-21T18:34:59.251Z"
},
{
"rule": "jQuery-html(",
"path": "ts/backbone/views/whisper_view.ts",
"line": " this.$el.html(window.Mustache.render(template, attrs));",
"reasonCategory": "usageTrusted",
"updated": "2021-02-26T18:44:56.450Z",
"reasonDetail": "Rendered template has been sanitized"
},
{
"rule": "React-useRef",
"path": "ts/calling/useGetCallingFrameBuffer.ts",
@ -9390,45 +9382,12 @@
"reasonCategory": "usageTrusted",
"updated": "2021-08-03T21:17:38.615Z"
},
{
"rule": "jQuery-$(",
"path": "ts/util/createIPCEvents.tsx",
"line": " if ($('.dark-overlay').length) {",
"reasonCategory": "usageTrusted",
"updated": "2021-08-18T18:22:55.307Z",
"reasonDetail": "Legacy code"
},
{
"rule": "jQuery-$(",
"path": "ts/util/createIPCEvents.tsx",
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
"reasonCategory": "usageTrusted",
"updated": "2021-08-18T18:22:55.307Z",
"reasonDetail": "Legacy code"
},
{
"rule": "jQuery-$(",
"path": "ts/util/createIPCEvents.tsx",
"line": " $('.dark-overlay').on('click', () => $('.dark-overlay').remove());",
"reasonCategory": "usageTrusted",
"updated": "2021-08-18T18:22:55.307Z",
"reasonDetail": "Legacy code"
},
{
"rule": "jQuery-$(",
"path": "ts/util/createIPCEvents.tsx",
"line": " removeDarkOverlay: () => $('.dark-overlay').remove(),",
"reasonCategory": "usageTrusted",
"updated": "2021-08-18T18:22:55.307Z",
"reasonDetail": "Legacy code"
},
{
"rule": "jQuery-prepend(",
"path": "ts/util/createIPCEvents.tsx",
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
"reasonCategory": "usageTrusted",
"updated": "2021-08-18T18:22:55.307Z",
"reasonDetail": "Legacy code"
"line": " document.body.prepend(newOverlay);",
"reasonCategory": "falseMatch",
"updated": "2022-11-30T00:14:31.394Z"
},
{
"rule": "jQuery-load(",

3
ts/window.d.ts vendored
View file

@ -5,7 +5,6 @@
import type { Store } from 'redux';
import type * as Backbone from 'backbone';
import type * as Underscore from 'underscore';
import type PQueue from 'p-queue/dist';
import type { assert } from 'chai';
import type * as Mustache from 'mustache';
@ -229,7 +228,6 @@ declare global {
) => Promise<void>;
FontFace: typeof FontFace;
_: typeof Underscore;
$: typeof jQuery;
imageToBlurHash: typeof imageToBlurHash;
@ -393,5 +391,4 @@ export type WhisperType = {
// 'extend View' syntax. Thus, we'll need to typescriptify most of it at once.
InboxView: typeof Backbone.View;
View: typeof Backbone.View;
};

View file

@ -6,7 +6,6 @@
import '../../models/messages';
import '../../models/conversations';
import '../../backbone/views/whisper_view';
import '../../views/conversation_view';
import '../../SignalProtocolStore';
import '../../background';

View file

@ -3836,11 +3836,6 @@
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0"
integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ==
"@types/underscore@1.10.3":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.3.tgz#927ff2b2e516444587fc80fb7845bb5c1806aad2"
integrity sha512-WgNbx0H2QO4ccIk2R1aWkteETuPxSa9OYKXoYujBgc0R4u+d2PWsb9MPpP77H+xqwbCXT+wuEBQ/6fl6s4C0OA==
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
@ -17195,7 +17190,7 @@ unc-path-regex@^0.1.2:
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
underscore@1.12.1, underscore@>=1.8.3:
underscore@>=1.8.3:
version "1.12.1"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e"
integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==