From 5cde48c2c526bdae7a52472294b83185531c1f78 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 3 Mar 2019 18:34:10 -0800 Subject: [PATCH] test: add IndexedDB tests (#1075) * test: add IndexedDB tests Adds unit tests using fake-indexeddb. * remove wtfnode dep --- package.json | 1 + src/routes/_database/cache.js | 6 + src/routes/_database/cleanup.js | 8 +- src/routes/_database/constants.js | 5 +- src/routes/_database/databaseLifecycle.js | 16 +- src/routes/_database/helpers.js | 6 +- tests/indexedDBShims.js | 5 + tests/unit/test-database.js | 192 ++++++++++++++++++++++ yarn.lock | 81 ++++++++- 9 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 tests/indexedDBShims.js create mode 100644 tests/unit/test-database.js diff --git a/package.json b/package.json index ad21672e..dfecb024 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "devDependencies": { "assert": "^1.4.1", "eslint-plugin-html": "^5.0.3", + "fake-indexeddb": "^2.0.5", "mocha": "^6.0.2", "now": "^14.0.1", "standard": "^12.0.1", diff --git a/src/routes/_database/cache.js b/src/routes/_database/cache.js index fb51cfb8..edf9abf7 100644 --- a/src/routes/_database/cache.js +++ b/src/routes/_database/cache.js @@ -42,6 +42,12 @@ function getOrCreateInstanceCache (cache, instanceName) { export function clearCache (cache, instanceName) { delete cache.caches[instanceName] } +export function clearAllCaches (instanceName) { + let allCaches = [statusesCache, accountsCache, relationshipsCache, metaCache, notificationsCache] + for (let cache of allCaches) { + clearCache(cache, instanceName) + } +} export function setInCache (cache, instanceName, key, value) { let instanceCache = getOrCreateInstanceCache(cache, instanceName) return instanceCache.set(key, value) diff --git a/src/routes/_database/cleanup.js b/src/routes/_database/cleanup.js index c07eea06..cad4af22 100644 --- a/src/routes/_database/cleanup.js +++ b/src/routes/_database/cleanup.js @@ -15,9 +15,10 @@ import { mark, stop } from '../_utils/marks' import { deleteAll } from './utils' import { createPinnedStatusKeyRange, createThreadKeyRange } from './keys' import { getKnownInstances } from './knownInstances' +import noop from 'lodash-es/noop' const BATCH_SIZE = 20 -const TIME_AGO = 5 * 24 * 60 * 60 * 1000 // five days ago +export const TIME_AGO = 5 * 24 * 60 * 60 * 1000 // five days ago const DELAY = 5 * 60 * 1000 // five minutes function batchedGetAll (callGetAll, callback) { @@ -97,7 +98,7 @@ function cleanupRelationships (relationshipsStore, cutoff) { ) } -async function cleanup (instanceName) { +export async function cleanup (instanceName) { console.log('cleanup', instanceName) mark(`cleanup:${instanceName}`) let db = await getDatabase(instanceName) @@ -146,4 +147,5 @@ async function scheduledCleanup () { } } -export const scheduleCleanup = debounce(scheduledCleanup, DELAY) +// we have unit tests that test indexedDB; we don't want this thing to run forever +export const scheduleCleanup = process.browser ? debounce(scheduledCleanup, DELAY) : noop diff --git a/src/routes/_database/constants.js b/src/routes/_database/constants.js index 07a34348..56883e9f 100644 --- a/src/routes/_database/constants.js +++ b/src/routes/_database/constants.js @@ -17,4 +17,7 @@ export const USERNAME_LOWERCASE = '__pinafore_acct_lc' export const DB_VERSION_INITIAL = 9 export const DB_VERSION_SEARCH_ACCOUNTS = 10 export const DB_VERSION_SNOWFLAKE_IDS = 11 -export const DB_VERSION_CURRENT = 11 + +// Using an object for these so that unit tests can change them +export const DB_VERSION_CURRENT = { version: 11 } +export const CURRENT_TIME = { now: () => Date.now() } diff --git a/src/routes/_database/databaseLifecycle.js b/src/routes/_database/databaseLifecycle.js index 93ced061..9762b87c 100644 --- a/src/routes/_database/databaseLifecycle.js +++ b/src/routes/_database/databaseLifecycle.js @@ -1,13 +1,14 @@ import { DB_VERSION_CURRENT } from './constants' import { addKnownInstance, deleteKnownInstance } from './knownInstances' import { migrations } from './migrations' +import { clearAllCaches } from './cache' const openReqs = {} const databaseCache = {} function createDatabase (instanceName) { return new Promise((resolve, reject) => { - let req = indexedDB.open(instanceName, DB_VERSION_CURRENT) + let req = indexedDB.open(instanceName, DB_VERSION_CURRENT.version) openReqs[instanceName] = req req.onerror = reject req.onblocked = () => { @@ -73,4 +74,17 @@ export function deleteDatabase (instanceName) { req.onerror = () => reject(req.error) req.onblocked = () => console.error(`database ${instanceName} blocked`) }).then(() => deleteKnownInstance(instanceName)) + .then(() => clearAllCaches(instanceName)) +} + +// this should probably only be used in unit tests +export function closeDatabase (instanceName) { + // close any open requests + let openReq = openReqs[instanceName] + if (openReq && openReq.result) { + openReq.result.close() + } + delete openReqs[instanceName] + delete databaseCache[instanceName] + clearAllCaches(instanceName) } diff --git a/src/routes/_database/helpers.js b/src/routes/_database/helpers.js index 7b815581..c0117a70 100644 --- a/src/routes/_database/helpers.js +++ b/src/routes/_database/helpers.js @@ -1,6 +1,8 @@ import { dbPromise, getDatabase } from './databaseLifecycle' import { getInCache, hasInCache, setInCache } from './cache' -import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE } from './constants' +import { + ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE, CURRENT_TIME +} from './constants' export async function getGenericEntityWithId (store, cache, instanceName, id) { if (hasInCache(cache, instanceName, id)) { @@ -50,6 +52,6 @@ export function cloneForStorage (obj) { break } } - res[TIMESTAMP] = Date.now() + res[TIMESTAMP] = CURRENT_TIME.now() return res } diff --git a/tests/indexedDBShims.js b/tests/indexedDBShims.js new file mode 100644 index 00000000..c8b3c7e2 --- /dev/null +++ b/tests/indexedDBShims.js @@ -0,0 +1,5 @@ +import indexedDB from 'fake-indexeddb' +import IDBKeyRange from 'fake-indexeddb/build/FDBKeyRange' + +global.indexedDB = indexedDB +global.IDBKeyRange = IDBKeyRange diff --git a/tests/unit/test-database.js b/tests/unit/test-database.js new file mode 100644 index 00000000..f2176395 --- /dev/null +++ b/tests/unit/test-database.js @@ -0,0 +1,192 @@ +/* global it describe beforeEach afterEach */ + +import '../indexedDBShims' +import assert from 'assert' +import { closeDatabase, deleteDatabase, getDatabase } from '../../src/routes/_database/databaseLifecycle' +import * as dbApi from '../../src/routes/_database/databaseApis' +import times from 'lodash-es/times' +import cloneDeep from 'lodash-es/cloneDeep' +import { + TIMESTAMP, ACCOUNT_ID, STATUS_ID, REBLOG_ID, USERNAME_LOWERCASE, + CURRENT_TIME, DB_VERSION_CURRENT, DB_VERSION_SEARCH_ACCOUNTS, DB_VERSION_SNOWFLAKE_IDS +} from '../../src/routes/_database/constants' +import { cleanup, TIME_AGO } from '../../src/routes/_database/cleanup' + +const INSTANCE_NAME = 'localhost:3000' + +const INSTANCE_INFO = { + 'uri': 'localhost:3000', + 'title': 'lolcathost', + 'description': 'blah', + 'foo': { + 'bar': true + } +} + +const createStatus = i => ({ + id: i.toString(), + created_at: new Date().toISOString(), + content: `Status #4{id}`, + account: { + id: '1' + } +}) + +const stripDBFields = item => { + let res = cloneDeep(item) + delete res[TIMESTAMP] + delete res[ACCOUNT_ID] + delete res[STATUS_ID] + delete res[REBLOG_ID] + delete res[USERNAME_LOWERCASE] + if (res.account) { + delete res.account[TIMESTAMP] + } + return res +} + +describe('test-database.js', function () { + this.timeout(60000) + + describe('db-basic', () => { + beforeEach(async () => { + await getDatabase(INSTANCE_NAME) + }) + + afterEach(async () => { + await deleteDatabase(INSTANCE_NAME) + }) + + it('basic indexeddb test', async () => { + let info = await dbApi.getInstanceInfo(INSTANCE_NAME) + assert(!info) + await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO) + info = await dbApi.getInstanceInfo(INSTANCE_NAME) + assert.deepStrictEqual(info, INSTANCE_INFO) + }) + + it('basic indexeddb test 2', async () => { + // sanity check to make sure that we have a clean DB between each test + let info = await dbApi.getInstanceInfo(INSTANCE_NAME) + assert(!info) + await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO) + info = await dbApi.getInstanceInfo(INSTANCE_NAME) + assert.deepStrictEqual(info, INSTANCE_INFO) + }) + + it('stores and sorts some statuses', async () => { + let allStatuses = times(40, createStatus) + await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses) + let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20) + let expected = allStatuses.slice().reverse().slice(0, 20) + assert.deepStrictEqual(statuses.map(stripDBFields), expected) + + statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', statuses[statuses.length - 1].id, 20) + expected = allStatuses.slice().reverse().slice(20, 40) + assert.deepStrictEqual(statuses.map(stripDBFields), expected) + }) + + it('cleans up old statuses', async () => { + // Pretend we are inserting a status from a long time ago. Note that we + // set a timestamp based on the *current* date when the status is inserted, + // not the date that the status was composed. + + let longAgo = Date.now() - (TIME_AGO * 2) + + let oldStatus = { + id: '1', + created_at: new Date(longAgo).toISOString(), + content: 'This is old', + account: { + id: '1' + } + } + + let previousNow = CURRENT_TIME.now + CURRENT_TIME.now = () => longAgo + + await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [oldStatus]) + + CURRENT_TIME.now = previousNow + + let newStatus = { + id: '2', + created_at: new Date().toISOString(), + content: 'This is new', + account: { + id: '2' + } + } + + await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [newStatus]) + let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20) + assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus, oldStatus]) + + let status1 = await dbApi.getStatus(INSTANCE_NAME, '1') + let status2 = await dbApi.getStatus(INSTANCE_NAME, '2') + + assert.deepStrictEqual(stripDBFields(status1), oldStatus) + assert.deepStrictEqual(stripDBFields(status2), newStatus) + + await cleanup(INSTANCE_NAME) + + statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20) + assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus]) + + status1 = await dbApi.getStatus(INSTANCE_NAME, '1') + status2 = await dbApi.getStatus(INSTANCE_NAME, '2') + + assert(!!status1) + assert.deepStrictEqual(stripDBFields(status2), newStatus) + }) + }) + + describe('db-migrations', () => { + let oldCurrentVersion + + beforeEach(async () => { + oldCurrentVersion = DB_VERSION_CURRENT.version + }) + + afterEach(async () => { + DB_VERSION_CURRENT.version = oldCurrentVersion + await deleteDatabase(INSTANCE_NAME) + }) + + it('migrates from v10 to v11', async () => { + // open the db using the old version + DB_VERSION_CURRENT.version = DB_VERSION_SEARCH_ACCOUNTS + await getDatabase(INSTANCE_NAME) + + // insert some statuses + let allStatuses = times(40, createStatus) + await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses) + + let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 40) + let expected = allStatuses.slice().reverse() + assert.deepStrictEqual(statuses.map(stripDBFields), expected) + + // close the database + closeDatabase(INSTANCE_NAME) + + // do a version upgrade + DB_VERSION_CURRENT.version = DB_VERSION_SNOWFLAKE_IDS + await getDatabase(INSTANCE_NAME) + + // check that the old statuses are correct + statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 40) + expected = allStatuses.slice().reverse() + assert.deepStrictEqual(statuses.map(stripDBFields), expected) + + // insert some more statuses for good measure + let moreStatuses = times(20, i => 40 + i).map(createStatus) + + await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', moreStatuses) + + statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 60) + expected = moreStatuses.slice().reverse().concat(allStatuses.reverse()) + + assert.deepStrictEqual(statuses.map(stripDBFields), expected) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 65bccc22..5afcb19a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1237,6 +1237,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer-es6@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.4.2.tgz#b567d364065843113589b6c1436bd9492701c7fe" + integrity sha512-HaJx92u12By863ZXVHZs4Bp1nkKaLpbs3Ec9SI1OKzq60Hz+Ks6z7UvdD8pIx61Ck3e8F9MH/IPEu5T0xKSbkQ== + base64-js@^1.0.2, base64-js@^1.1.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -2001,6 +2006,11 @@ core-js@^2.4.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49" integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ== +core-js@^2.4.1, core-js@^2.5.3: + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2447,6 +2457,13 @@ domelementtype@~1.1.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -3023,6 +3040,15 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fake-indexeddb@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-2.0.5.tgz#829685232f79bcb9d182b8dd33934e9e5657ed18" + integrity sha512-C68kh3Ec3L6JZaTpRm6+TjY5AOs4bwtEOXazzb6733UL0F0jLR7j939e+TdlUmJdxumFQmXIzFhyLu5ZifQc5w== + dependencies: + core-js "^2.4.1" + realistic-structured-clone "^2.0.1" + setimmediate "^1.0.5" + fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -4542,6 +4568,11 @@ lodash.mergewith@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + lodash@4.17.11, "lodash@4.6.1 || ^4.16.1", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -6126,6 +6157,16 @@ readdirp@^2.0.0, readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +realistic-structured-clone@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959" + integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg== + dependencies: + core-js "^2.5.3" + domexception "^1.0.1" + typeson "^5.8.2" + typeson-registry "^1.0.0-alpha.20" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -6575,7 +6616,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -7455,6 +7496,13 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + tree-kill@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" @@ -7551,6 +7599,21 @@ typescript@^3.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== +typeson-registry@^1.0.0-alpha.20: + version "1.0.0-alpha.26" + resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.26.tgz#d1f337584196c5d5d112ad981e0dbbd2ced30c30" + integrity sha512-R0wwXIYSiJMh+1XfvyUsCnEGVERoJcNrMl9e/ka30dJ+gQyh4/0NU9WHaqUm8oHtZzZYCz+A5fDRCiXYIq7H1Q== + dependencies: + base64-arraybuffer-es6 "0.4.2" + typeson "5.11.0" + uuid "3.3.2" + whatwg-url "7.0.0" + +typeson@5.11.0, typeson@^5.8.2: + version "5.11.0" + resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.11.0.tgz#a8273f00050be9eeef974aaa04a0c95a394f821a" + integrity sha512-S5KtLzcU4dr4BXh8VuJDYugsRGsDQYlumCbrmwuAX1a1GNpbVYK4p9wluCIfTVPFvVyV6wRfExXX6Q1+YDItEQ== + uglify-js@3.4.x: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -7701,7 +7764,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.3.2: +uuid@3.3.2, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -7749,6 +7812,11 @@ webauth@^1.1.0: resolved "https://registry.yarnpkg.com/webauth/-/webauth-1.1.0.tgz#64704f6b8026986605bc3ca629952e6e26fdd100" integrity sha1-ZHBPa4AmmGYFvDymKZUubib90QA= +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + webpack-bundle-analyzer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.4.tgz#095638487a664162f19e3b2fb7e621b7002af4b8" @@ -7805,6 +7873,15 @@ webpack@^4.29.6: watchpack "^1.5.0" webpack-sources "^1.3.0" +whatwg-url@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"