From 42cdb7cdc8d14e55b34870f2497d3bc71bec99e7 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Tue, 22 Jul 2025 22:22:35 -0700 Subject: [PATCH 01/92] Initial commit to fix all known Manifest V3 bugs --- js/background.js | 395 ++++++++++++++++++++++--------------------- js/common.js | 21 +++ js/db.js | 22 +-- js/dbService.js | 5 +- js/popup.js | 73 +++++--- js/spaces.js | 134 +++++++++------ js/spacesRenderer.js | 2 +- js/spacesService.js | 18 +- js/switcher.js | 10 +- js/utils.js | 43 ++++- manifest.json | 19 +-- popup.html | 4 +- spaces.html | 2 +- 13 files changed, 422 insertions(+), 326 deletions(-) create mode 100644 js/common.js diff --git a/js/background.js b/js/background.js index d72364d..6eeb7da 100644 --- a/js/background.js +++ b/js/background.js @@ -6,12 +6,40 @@ * Copyright (C) 2015 Dean Oemcke */ +import { spacesService } from './spacesService.js'; + +/** @typedef {import('./common.js').SessionPresence} SessionPresence */ + // eslint-disable-next-line no-unused-vars, no-var -var spaces = (() => { - let spacesPopupWindowId = false; - let spacesOpenWindowId = false; - const noop = () => {}; - const debug = false; +let spacesPopupWindowId = false; +let spacesOpenWindowId = false; +const noop = () => {}; +const debug = false; + +// runtime extension install listener +chrome.runtime.onInstalled.addListener(details => { + console.log(`Extension installed: ${JSON.stringify(details)}`); + + if (details.reason === 'install') { + // eslint-disable-next-line no-console + console.log('This is a first install!'); + showSpacesOpenWindow(); + } else if (details.reason === 'update') { + const thisVersion = chrome.runtime.getManifest().version; + if (details.previousVersion !== thisVersion) { + // eslint-disable-next-line no-console + console.log( + `Updated from ${details.previousVersion} to ${thisVersion}!` + ); + } + } + + chrome.contextMenus.create({ + id: 'spaces-add-link', + title: 'Add link to space...', + contexts: ['link'], + }); +}); // LISTENERS @@ -101,6 +129,29 @@ var spaces = (() => { // endpoints called by spaces.js switch (request.action) { + case 'requestSessionPresence': + const sessionPresence = requestSessionPresence(request.sessionName); + sendResponse(sessionPresence); + return true; + + case 'requestSpaceFromWindowId': + windowId = _cleanParameter(request.windowId); + if (windowId) { + requestSpaceFromWindowId(windowId, sendResponse); + } + return true; + + case 'requestCurrentSpace': + console.log('requestCurrentSpace event received'); + requestCurrentSpace(sendResponse); + return true; + + case 'generatePopupParams': + generatePopupParams(request.action, request.tabUrl).then(params => { + sendResponse(params); + }); + return true; + case 'loadSession': sessionId = _cleanParameter(request.sessionId); if (sessionId) { @@ -151,6 +202,7 @@ var spaces = (() => { handleSaveNewSession( windowId, request.sessionName, + !!request.deleteOld, sendResponse ); } @@ -163,15 +215,15 @@ var spaces = (() => { return true; // allow async response case 'restoreFromBackup': - if (request.spaces) { - handleRestoreFromBackup(request.spaces, sendResponse); + if (request.space) { + handleRestoreFromBackup(request.space, !!request.deleteOld, sendResponse); } return true; // allow async response case 'deleteSession': sessionId = _cleanParameter(request.sessionId); if (sessionId) { - handleDeleteSession(sessionId, false, sendResponse); + handleDeleteSession(sessionId, sendResponse); } return true; @@ -181,6 +233,7 @@ var spaces = (() => { handleUpdateSessionName( sessionId, request.sessionName, + !!request.deleteOld, sendResponse ); } @@ -209,10 +262,6 @@ var spaces = (() => { }); return true; - case 'requestHotkeys': - requestHotkeys(sendResponse); - return true; - case 'requestTabDetail': tabId = _cleanParameter(request.tabId); if (tabId) { @@ -259,13 +308,16 @@ var spaces = (() => { windowId = _cleanParameter(request.windowId); sessionId = _cleanParameter(request.sessionId); - if (windowId) { - handleLoadWindow(windowId); - } else if (sessionId) { - handleLoadSession(sessionId); - } + (async () => { + if (windowId) { + await handleLoadWindow(windowId); + } else if (sessionId) { + await handleLoadSession(sessionId); + } + sendResponse(true); + })(); - return false; + return true; case 'addLinkToNewSession': tabId = _cleanParameter(request.tabId); @@ -385,13 +437,6 @@ var spaces = (() => { } }); - // add context menu entry - - chrome.contextMenus.create({ - id: 'spaces-add-link', - title: 'Add link to space...', - contexts: ['link'], - }); chrome.contextMenus.onClicked.addListener(info => { // handle showing the move tab popup (tab.html) if (info.menuItemId === 'spaces-add-link') { @@ -399,36 +444,19 @@ var spaces = (() => { } }); - // runtime extension install listener - chrome.runtime.onInstalled.addListener(details => { - if (details.reason === 'install') { - // eslint-disable-next-line no-console - console.log('This is a first install!'); - showSpacesOpenWindow(); - } else if (details.reason === 'update') { - const thisVersion = chrome.runtime.getManifest().version; - if (details.previousVersion !== thisVersion) { - // eslint-disable-next-line no-console - console.log( - `Updated from ${details.previousVersion} to ${thisVersion}!` - ); - } - } - }); - function createShortcutsWindow() { chrome.tabs.create({ url: 'chrome://extensions/configureCommands' }); } - function showSpacesOpenWindow(windowId, editMode) { + async function showSpacesOpenWindow(windowId, editMode) { let url; if (editMode && windowId) { - url = chrome.extension.getURL( + url = chrome.runtime.getURL( `spaces.html#windowId=${windowId}&editMode=true` ); } else { - url = chrome.extension.getURL('spaces.html'); + url = chrome.runtime.getURL('spaces.html'); } // if spaces open window already exists then just give it focus (should be up to date) @@ -448,6 +476,9 @@ var spaces = (() => { // otherwise re-create it } else { + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; chrome.windows.create( { type: 'popup', @@ -499,8 +530,8 @@ var spaces = (() => { } function createOrShowSpacesPopupWindow(action, tabUrl) { - generatePopupParams(action, tabUrl).then(params => { - const popupUrl = `${chrome.extension.getURL( + generatePopupParams(action, tabUrl).then(async (params) => { + const popupUrl = `${chrome.runtime.getURL( 'popup.html' )}#opener=bg&${params}`; // if spaces window already exists @@ -527,6 +558,10 @@ var spaces = (() => { // otherwise create it } else { + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + chrome.windows.create( { type: 'popup', @@ -575,17 +610,33 @@ var spaces = (() => { } } - function updateSpacesWindow(source) { - if (debug) + async function updateSpacesWindow(source) { + if (debug) { // eslint-disable-next-line no-console console.log(`updateSpacesWindow triggered. source: ${source}`); + } + + if (spacesOpenWindowId) { + const spacesOpenWindow = await chrome.windows.get(spacesOpenWindowId); + if (chrome.runtime.lastError || !spacesOpenWindow) { + // eslint-disable-next-line no-console + console.log(`updateSpacesWindow: Error getting spacesOpenWindow: ${chrome.runtime.lastError}`); + spacesOpenWindowId = false; + return; + } - requestAllSpaces(allSpaces => { - chrome.runtime.sendMessage({ - action: 'updateSpaces', - spaces: allSpaces, + requestAllSpaces(allSpaces => { + try { + chrome.runtime.sendMessage({ + action: 'updateSpaces', + spaces: allSpaces, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`updateSpacesWindow: Error updating spaces window: ${err}`); + } }); - }); + } } function checkInternalSpacesWindows(windowId, windowClosed) { @@ -600,47 +651,13 @@ var spaces = (() => { return false; } - function checkSessionOverwrite(session) { - // make sure session being overwritten is not currently open - if (session.windowId) { - alert( - `A session with the name '${session.name}' is currently open an cannot be overwritten` - ); - return false; - - // otherwise prompt to see if user wants to overwrite session - } - return window.confirm(`Replace existing space: ${session.name}?`); - } - - function checkSessionDelete(session) { - return window.confirm( - `Are you sure you want to delete the space: ${session.name}?` - ); - } - - function requestHotkeys(callback) { - chrome.commands.getAll(commands => { - let switchStr; - let moveStr; - let spacesStr; - - commands.forEach(command => { - if (command.name === 'spaces-switch') { - switchStr = command.shortcut; - } else if (command.name === 'spaces-move') { - moveStr = command.shortcut; - } else if (command.name === 'spaces-open') { - spacesStr = command.shortcut; - } - }); - - callback({ - switchCode: switchStr, - moveCode: moveStr, - spacesCode: spacesStr, - }); - }); + /** + * @param {string} sessionName + * @returns {SessionPresence} + */ + function requestSessionPresence(sessionName) { + const session = spacesService.getSessionByName(sessionName); + return { exists: !!session, isOpen: !!session && !!session.windowId }; } function requestTabDetail(tabId, callback) { @@ -733,56 +750,61 @@ var spaces = (() => { return 0; } - function handleLoadSession(sessionId, tabUrl) { + async function handleLoadSession(sessionId, tabUrl) { const session = spacesService.getSessionBySessionId(sessionId); // if space is already open, then give it focus if (session.windowId) { - handleLoadWindow(session.windowId, tabUrl); + await handleLoadWindow(session.windowId, tabUrl); // else load space in new window } else { const urls = session.tabs.map(curTab => { return curTab.url; }); - chrome.windows.create( + + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + + const newWindow = await chrome.windows.create( { url: urls, height: screen.height - 100, width: screen.width - 100, top: 0, left: 0, - }, - newWindow => { - // force match this new window to the session - spacesService.matchSessionToWindow(session, newWindow); - - // after window has loaded try to pin any previously pinned tabs - session.tabs.forEach(curSessionTab => { - if (curSessionTab.pinned) { - let pinnedTabId = false; - newWindow.tabs.some(curNewTab => { - if ( - curNewTab.url === curSessionTab.url || - curNewTab.pendingUrl === curSessionTab.url - ) { - pinnedTabId = curNewTab.id; - return true; - } - return false; - }); - if (pinnedTabId) { - chrome.tabs.update(pinnedTabId, { - pinned: true, - }); - } + }); + + // force match this new window to the session + spacesService.matchSessionToWindow(session, newWindow); + + // after window has loaded try to pin any previously pinned tabs + for (const curSessionTab of session.tabs) { + if (curSessionTab.pinned) { + let pinnedTabId = false; + newWindow.tabs.some(curNewTab => { + if ( + curNewTab.url === curSessionTab.url || + curNewTab.pendingUrl === curSessionTab.url + ) { + pinnedTabId = curNewTab.id; + return true; } + return false; }); - - // if tabUrl is defined, then focus this tab - if (tabUrl) { - focusOrLoadTabInWindow(newWindow, tabUrl); + if (pinnedTabId) { + await chrome.tabs.update(pinnedTabId, { + pinned: true, + }); } + } + } + + // if tabUrl is defined, then focus this tab + if (tabUrl) { + await focusOrLoadTabInWindow(newWindow, tabUrl); + } /* session.tabs.forEach(function (curTab) { chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false}); @@ -791,54 +813,57 @@ var spaces = (() => { chrome.tabs.query({windowId: newWindow.id, index: 0}, function (tabs) { chrome.tabs.remove(tabs[0].id); }); */ - } - ); } } - function handleLoadWindow(windowId, tabUrl) { + + async function handleLoadWindow(windowId, tabUrl) { // assume window is already open, give it focus if (windowId) { - focusWindow(windowId); + await focusWindow(windowId); } // if tabUrl is defined, then focus this tab if (tabUrl) { - chrome.windows.get(windowId, { populate: true }, window => { - focusOrLoadTabInWindow(window, tabUrl); - }); + const theWin = await chrome.windows.get(windowId, { populate: true }); + await focusOrLoadTabInWindow(theWin, tabUrl); } } - function focusWindow(windowId) { - chrome.windows.update(windowId, { focused: true }); + async function focusWindow(windowId) { + await chrome.windows.update(windowId, { focused: true }); } - function focusOrLoadTabInWindow(window, tabUrl) { - const match = window.tabs.some(tab => { + async function focusOrLoadTabInWindow(window, tabUrl) { + let match = false; + for (const tab of window.tabs) { if (tab.url === tabUrl) { - chrome.tabs.update(tab.id, { active: true }); - return true; + await chrome.tabs.update(tab.id, { active: true }); + match = true; + break; } - return false; - }); + } + if (!match) { - chrome.tabs.create({ url: tabUrl }); + await chrome.tabs.create({ url: tabUrl }); } } - function handleSaveNewSession(windowId, sessionName, callback) { + function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { chrome.windows.get(windowId, { populate: true }, curWindow => { const existingSession = spacesService.getSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session if (existingSession) { - if (!checkSessionOverwrite(existingSession)) { + if (!deleteOld) { + console.error( + `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` + ); callback(false); return; // if we choose to overwrite, delete the existing session } - handleDeleteSession(existingSession.id, true, noop); + handleDeleteSession(existingSession.id, noop); } spacesService.saveNewSession( sessionName, @@ -849,43 +874,32 @@ var spaces = (() => { }); } - function handleRestoreFromBackup(_spaces, callback) { - let existingSession; - let performSave; + function handleRestoreFromBackup(space, deleteOld, callback) { - const promises = []; - for (let i = 0; i < _spaces.length; i += 1) { - const space = _spaces[i]; - existingSession = space.name - ? spacesService.getSessionByName(space.name) - : false; - performSave = true; + const existingSession = space.name + ? spacesService.getSessionByName(space.name) + : false; - // if session with same name already exist, then prompt to override the existing session - if (existingSession) { - if (!checkSessionOverwrite(existingSession)) { - performSave = false; + // if session with same name already exist, then prompt to override the existing session + if (existingSession) { + if (!deleteOld) { + console.error( + `handleRestoreFromBackup: Session with name "${space.name}" already exists and deleteOld was not true.` + ); + callback(false); + return; // if we choose to overwrite, delete the existing session - } else { - handleDeleteSession(existingSession.id, true, noop); - } - } - - if (performSave) { - promises.push( - new Promise(resolve => { - spacesService.saveNewSession( - space.name, - space.tabs, - false, - resolve - ); - }) - ); } + handleDeleteSession(existingSession.id, noop); } - Promise.all(promises).then(callback); + + spacesService.saveNewSession( + space.name, + space.tabs, + false, + callback + ); } function handleImportNewSession(urlList, callback) { @@ -906,30 +920,35 @@ var spaces = (() => { spacesService.saveNewSession(tempName, tabList, false, callback); } - function handleUpdateSessionName(sessionId, sessionName, callback) { + function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { // check to make sure session name doesn't already exist const existingSession = spacesService.getSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session - if (existingSession && existingSession.id !== sessionId) { - if (!checkSessionOverwrite(existingSession)) { + if (existingSession) { + if (!deleteOld) { + console.error( + `handleUpdateSessionName: Session with name "${sessionName}" already exists and deleteOld was not true.` + ); callback(false); return; // if we choose to override, then delete the existing session } - handleDeleteSession(existingSession.id, true, noop); + handleDeleteSession(existingSession.id, noop); } spacesService.updateSessionName(sessionId, sessionName, callback); } - function handleDeleteSession(sessionId, force, callback) { + function handleDeleteSession(sessionId, callback) { const session = spacesService.getSessionBySessionId(sessionId); - if (!force && !checkSessionDelete(session)) { + if (!session) { + console.error(`handleDeleteSession: No session found with id ${sessionId}`); callback(false); - } else { - spacesService.deleteSession(sessionId, callback); + return; } + + spacesService.deleteSession(sessionId, callback); } function handleAddLinkToNewSession(url, sessionName, callback) { @@ -1051,13 +1070,5 @@ var spaces = (() => { callback(true); } - return { - requestSpaceFromWindowId, - requestCurrentSpace, - requestHotkeys, - generatePopupParams, - }; -})(); - spacesService.initialiseSpaces(); spacesService.initialiseTabHistory(); diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..9ab5f7a --- /dev/null +++ b/js/common.js @@ -0,0 +1,21 @@ +/* + * common.js + * Licensed under the MIT License + * Copyright (C) 2025 by the Contributors. + */ + +/** Common types shared between background and client code. */ + +// TODO(codedread): Fill out the rest of the properties. +/** + * @typedef Space + * @property {string} id The unique identifier for the space. + * @property {string} name The name of the space. + * @property {string?} windowId The ID of the window associated with the space, if any. + */ + +/** + * @typedef SessionPresence + * @property {boolean} exists A session with this name exists in the database. + * @property {boolean} isOpen The session is currently open in a window. + */ diff --git a/js/db.js b/js/db.js index ebdf5b3..298d326 100644 --- a/js/db.js +++ b/js/db.js @@ -1,9 +1,7 @@ //The MIT License //Copyright (c) 2012 Aaron Powell - -(function(window, undefined) { - 'use strict'; - +//Conversion to ES Module by codedread +const window = self; var indexedDB, IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange, transactionModes = { @@ -550,7 +548,7 @@ var dbCache = {}; - var db = { + export var db = { version: '0.9.2', open: function(options) { var request; @@ -592,17 +590,3 @@ }); }, }; - - if ( - typeof module !== 'undefined' && - typeof module.exports !== 'undefined' - ) { - module.exports = db; - } else if (typeof define === 'function' && define.amd) { - define(function() { - return db; - }); - } else { - window.db = db; - } -})(window); diff --git a/js/dbService.js b/js/dbService.js index cbfb1ed..f2daa5e 100644 --- a/js/dbService.js +++ b/js/dbService.js @@ -1,6 +1,9 @@ /* global db */ + +import { db } from './db.js'; + // eslint-disable-next-line no-var -var dbService = { +export var dbService = { DB_SERVER: 'spaces', DB_VERSION: '1', DB_SESSIONS: 'ttSessions', diff --git a/js/popup.js b/js/popup.js index 8781ddd..afc3cf8 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,5 +1,8 @@ /* global chrome spacesRenderer */ +import { spacesRenderer } from './spacesRenderer.js'; +import { utils } from './utils.js'; + (() => { const UNSAVED_SESSION = '(unnamed window)'; const NO_HOTKEY = 'no hotkey set'; @@ -16,13 +19,10 @@ */ document.addEventListener('DOMContentLoaded', async () => { - const { utils, spaces } = chrome.extension.getBackgroundPage(); const url = utils.getHashVariable('url', window.location.href); globalUrl = url !== '' ? decodeURIComponent(url) : false; - const windowId = utils.getHashVariable( - 'windowId', - window.location.href - ); + const currentWindow = await chrome.windows.getCurrent({ populate: true }); + const windowId = currentWindow.id; globalWindowId = windowId !== '' ? windowId : false; globalTabId = utils.getHashVariable('tabId', window.location.href); const sessionName = utils.getHashVariable( @@ -34,13 +34,8 @@ const action = utils.getHashVariable('action', window.location.href); const requestSpacePromise = globalWindowId - ? new Promise(resolve => - spaces.requestSpaceFromWindowId( - parseInt(globalWindowId, 10), - resolve - ) - ) - : new Promise(resolve => spaces.requestCurrentSpace(resolve)); + ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) + : chrome.runtime.sendMessage({ action: 'requestCurrentSpace' }); requestSpacePromise.then(space => { globalCurrentSpace = space; @@ -115,16 +110,14 @@ * MAIN POPUP VIEW */ - function renderMainCard() { - const { spaces } = chrome.extension.getBackgroundPage(); - spaces.requestHotkeys(hotkeys => { + async function renderMainCard() { + const hotkeys = await requestHotkeys(); document.querySelector( '#switcherLink .hotkey' ).innerHTML = hotkeys.switchCode ? hotkeys.switchCode : NO_HOTKEY; document.querySelector( '#moverLink .hotkey' ).innerHTML = hotkeys.moveCode ? hotkeys.moveCode : NO_HOTKEY; - }); const hotkeyEls = document.querySelectorAll('.hotkey'); for (let i = 0; i < hotkeyEls.length; i += 1) { @@ -146,18 +139,18 @@ }); document .querySelector('#switcherLink .optionText') - .addEventListener('click', () => { - spaces.generatePopupParams('switch').then(params => { + .addEventListener('click', async () => { + chrome.runtime.sendMessage({'action': 'switch',}).then(params => { if (!params) return; window.location.hash = params; window.location.reload(); }); - // renderSwitchCard() + renderSwitchCard(); }); document .querySelector('#moverLink .optionText') .addEventListener('click', () => { - spaces.generatePopupParams('move').then(params => { + chrome.runtime.sendMessage({'action': 'generatePopupParams',}).then(params => { if (!params) return; window.location.hash = params; window.location.reload(); @@ -166,6 +159,31 @@ }); } + async function requestHotkeys() { + console.log('requestHotkeys called'); + const commands = await chrome.commands.getAll(); + console.dir(commands); + let switchStr; + let moveStr; + let spacesStr; + + commands.forEach(command => { + if (command.name === 'spaces-switch') { + switchStr = command.shortcut; + } else if (command.name === 'spaces-move') { + moveStr = command.shortcut; + } else if (command.name === 'spaces-open') { + spacesStr = command.shortcut; + } + }); + + return { + switchCode: switchStr, + moveCode: moveStr, + spacesCode: spacesStr, + }; + } + function handleNameEdit() { const inputEl = document.getElementById('activeSpaceTitle'); inputEl.focus(); @@ -174,7 +192,7 @@ } } - function handleNameSave() { + async function handleNameSave() { const inputEl = document.getElementById('activeSpaceTitle'); const newName = inputEl.value; @@ -185,10 +203,16 @@ return; } + const canOverwrite = await utils.checkSessionOverwrite(newName); + if (!canOverwrite) { + return; + } + if (globalCurrentSpace.sessionId) { chrome.runtime.sendMessage( { action: 'updateSessionName', + deleteOld: true, sessionName: newName, sessionId: globalCurrentSpace.sessionId, }, @@ -198,6 +222,7 @@ chrome.runtime.sendMessage( { action: 'saveNewSession', + deleteOld: true, sessionName: newName, windowId: globalCurrentSpace.windowId, }, @@ -237,12 +262,14 @@ return document.querySelector('.space.selected'); } - function handleSwitchAction(selectedSpaceEl) { - chrome.runtime.sendMessage({ + async function handleSwitchAction(selectedSpaceEl) { + await chrome.runtime.sendMessage({ action: 'switchToSpace', sessionId: selectedSpaceEl.getAttribute('data-sessionId'), windowId: selectedSpaceEl.getAttribute('data-windowId'), }); + // Wait for the response from the background message handler before + // closing the window. window.close(); } diff --git a/js/spaces.js b/js/spaces.js index c6bb221..04083d4 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -1,7 +1,9 @@ /* global chrome */ -(() => { - const UNSAVED_SESSION = 'Unnamed window'; +import { utils } from './utils.js'; + + const UNSAVED_SESSION_NAME = 'Unnamed window'; + const UNSAVED_SESSION = `${UNSAVED_SESSION_NAME}`; const nodes = {}; let globalSelectedSpace; let bannerState; @@ -298,7 +300,7 @@ } } - function handleNameSave() { + async function handleNameSave() { const newName = nodes.nameFormInput.value; const oldName = globalSelectedSpace.name; const { sessionId } = globalSelectedSpace; @@ -311,6 +313,11 @@ return; } + const canOverwrite = await utils.checkSessionOverwrite(newName); + if (!canOverwrite) { + return; + } + // otherwise call the save service if (sessionId) { performSessionUpdate(newName, sessionId, session => { @@ -328,30 +335,42 @@ } } - function handleDelete() { + async function handleDelete() { const { sessionId } = globalSelectedSpace; - if (sessionId) { - performDelete(sessionId, () => { - updateSpacesList(); - reroute(false, false, true); - }); + const session = await fetchSpaceDetail(sessionId, false); + if (!session) { + console.error( + `handleDelete: No session found with id ${sessionId}` + ); + return; + } + const sessionName = session.name || UNSAVED_SESSION_NAME; + const confirm = window.confirm( + `Are you sure you want to delete the space: ${sessionName}?` + ); + + if (confirm) { + performDelete(sessionId, () => { + updateSpacesList(); + reroute(false, false, true); + }); + } } } // import accepts either a newline separated list of urls or a json backup object - function handleImport() { + async function handleImport() { let urlList; - let spacesObject; + let spaces; const rawInput = nodes.modalInput.value; // check for json object try { - spacesObject = JSON.parse(rawInput); - performRestoreFromBackup(spacesObject, () => { - updateSpacesList(); - }); + spaces = JSON.parse(rawInput); + await performRestoreFromBackup(spaces); + updateSpacesList(); } catch (e) { // otherwise treat as a list of newline separated urls if (rawInput.trim().length > 0) { @@ -406,27 +425,26 @@ }); } - function handleExport() { + async function handleExport() { const { sessionId } = globalSelectedSpace; const { windowId } = globalSelectedSpace; let csvContent = ''; let dataString = ''; - fetchSpaceDetail(sessionId, windowId, space => { - space.tabs.forEach(curTab => { - const url = normaliseTabUrl(curTab.url); - dataString += `${url}\n`; - }); - csvContent += dataString; - - const blob = new Blob([csvContent], { type: 'text/plain' }); - const blobUrl = URL.createObjectURL(blob); - const filename = `${space.name || 'untitled'}.txt`; - const link = document.createElement('a'); - link.setAttribute('href', blobUrl); - link.setAttribute('download', filename); - link.click(); + const space = await fetchSpaceDetail(sessionId, windowId); + space.tabs.forEach(curTab => { + const url = normaliseTabUrl(curTab.url); + dataString += `${url}\n`; }); + csvContent += dataString; + + const blob = new Blob([csvContent], { type: 'text/plain' }); + const blobUrl = URL.createObjectURL(blob); + const filename = `${space.name || 'untitled'}.txt`; + const link = document.createElement('a'); + link.setAttribute('href', blobUrl); + link.setAttribute('download', filename); + link.click(); } function normaliseTabUrl(url) { @@ -448,15 +466,12 @@ ); } - function fetchSpaceDetail(sessionId, windowId, callback) { - chrome.runtime.sendMessage( - { - action: 'requestSpaceDetail', - sessionId: sessionId || false, - windowId: windowId || false, - }, - callback - ); + async function fetchSpaceDetail(sessionId, windowId) { + return await chrome.runtime.sendMessage({ + action: 'requestSpaceDetail', + sessionId: sessionId || false, + windowId: windowId || false, + }); } function performLoadSession(sessionId, callback) { @@ -515,6 +530,7 @@ chrome.runtime.sendMessage( { action: 'updateSessionName', + deleteOld: true, sessionName: newName, sessionId, }, @@ -526,6 +542,7 @@ chrome.runtime.sendMessage( { action: 'saveNewSession', + deleteOld: true, sessionName: newName, windowId, }, @@ -543,14 +560,21 @@ ); } - function performRestoreFromBackup(spacesObject, callback) { - chrome.runtime.sendMessage( - { - action: 'restoreFromBackup', - spaces: spacesObject, - }, - callback - ); + async function performRestoreFromBackup(spaces) { + for (const space of spaces) { + const canOverwrite = await utils.checkSessionOverwrite(space.name); + if (!canOverwrite) { + continue; + } + + await chrome.runtime.sendMessage( + { + action: 'restoreFromBackup', + deleteOld: true, + space, + } + ); + } } // EVENT LISTENERS FOR STATIC DOM ELEMENTS @@ -681,7 +705,7 @@ } } - function updateSpaceDetail(useCachedSpace) { + async function updateSpaceDetail(useCachedSpace) { const sessionId = getVariableFromHash('sessionId'); const windowId = getVariableFromHash('windowId'); const editMode = getVariableFromHash('editMode'); @@ -693,13 +717,12 @@ // otherwise refetch space based on hashvars } else if (sessionId || windowId) { - fetchSpaceDetail(sessionId, windowId, space => { - addDuplicateMetadata(space); + const space = await fetchSpaceDetail(sessionId, windowId); + addDuplicateMetadata(space); - // cache current selected space - globalSelectedSpace = space; - renderSpaceDetail(space, editMode); - }); + // cache current selected space + globalSelectedSpace = space; + renderSpaceDetail(space, editMode); // otherwise hide space detail view } else { @@ -751,7 +774,7 @@ nodes.modalInput = document.getElementById('importTextArea'); nodes.modalButton = document.getElementById('importBtn'); - nodes.home.setAttribute('href', chrome.extension.getURL('spaces.html')); + nodes.home.setAttribute('href', chrome.runtime.getURL('spaces.html')); // initialise event listeners for static elements addEventListeners(); @@ -762,4 +785,3 @@ // render main content updateSpaceDetail(); }; -})(); diff --git a/js/spacesRenderer.js b/js/spacesRenderer.js index e00de29..187f0a4 100644 --- a/js/spacesRenderer.js +++ b/js/spacesRenderer.js @@ -1,5 +1,5 @@ // eslint-disable-next-line no-var -var spacesRenderer = { +export var spacesRenderer = { nodes: {}, maxSuggestions: 10, oneClickMode: false, diff --git a/js/spacesService.js b/js/spacesService.js index a889be2..262134b 100644 --- a/js/spacesService.js +++ b/js/spacesService.js @@ -4,8 +4,10 @@ * Copyright (C) 2015 Dean Oemcke */ +import { dbService } from './dbService.js'; + // eslint-disable-next-line no-var -var spacesService = { +export var spacesService = { tabHistoryUrlMap: {}, closedWindowIds: {}, sessions: [], @@ -18,9 +20,9 @@ var spacesService = { noop: () => {}, // initialise spaces - combine open windows with saved sessions - initialiseSpaces: () => { + initialiseSpaces: async () => { // update version numbers - spacesService.lastVersion = spacesService.fetchLastVersion(); + spacesService.lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); dbService.fetchAllSessions(sessions => { @@ -225,17 +227,17 @@ var spacesService = { }, // local storage getters/setters - fetchLastVersion: () => { - let version = localStorage.getItem('spacesVersion'); - if (version !== null) { - version = JSON.parse(version); + fetchLastVersion: async () => { + let version = await chrome.storage.local.get(['spacesVersion']); + if (version !== null && version['spacesVersion']) { + version = JSON.parse(version['spacesVersion']); return version; } return 0; }, setLastVersion: newVersion => { - localStorage.setItem('spacesVersion', JSON.stringify(newVersion)); + chrome.storage.local.set({'spacesVersion': JSON.stringify(newVersion)}); }, // event listener functions for window and tab events diff --git a/js/switcher.js b/js/switcher.js index 741780d..bd56d9d 100644 --- a/js/switcher.js +++ b/js/switcher.js @@ -1,6 +1,7 @@ /* global chrome, spacesRenderer */ -(() => { +import { spacesRenderer } from './spacesRenderer.js'; + function getSelectedSpace() { return document.querySelector('.space.selected'); } @@ -19,8 +20,9 @@ }); } - function getSwitchKeycodes(callback) { - chrome.runtime.sendMessage({ action: 'requestHotkeys' }, commands => { + async function getSwitchKeycodes(callback) { + const commands = await chrome.commands.getAll(); + // eslint-disable-next-line no-console console.dir(commands); @@ -50,7 +52,6 @@ secondaryModifier, mainKeyCode, }); - }); } function addEventListeners() { @@ -87,4 +88,3 @@ addEventListeners(); }); }; -})(); diff --git a/js/utils.js b/js/utils.js index 90e6a35..62ce416 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,6 +1,41 @@ /* global chrome */ + +/** @typedef {import('./common.js').SessionPresence} SessionPresence */ + // eslint-disable-next-line no-var, no-unused-vars -var utils = { +export var utils = { + /** + * Checks if a session with the given name can be overwritten by checking + * with the background script, alerting the user if the session is currently + * open, and confirming if the session already exists but is not open. + * @param {string} sessionName + * @returns {Promise} Returns true if the session can be safely + * overwritten. This happens if the session does not exist or if the + * user has confirmed overwriting. + */ + async checkSessionOverwrite(sessionName) { + /** @type {SessionPresence} */ + const sessionPresence = await chrome.runtime.sendMessage({ + action: 'requestSessionPresence', + sessionName, + }); + + if (!sessionPresence.exists) { + return true; + } + + if (sessionPresence.isOpen) { + // eslint-disable-next-line no-alert + alert( + `A session with the name '${sessionName}' is currently open and cannot be overwritten` + ); + return false; + } + return confirm( + `A session with the name '${sessionName}' already exists. Do you want to overwrite it?` + ); + }, + getHashVariable: (key, urlStr) => { const valuesByKey = {}; const keyPairRegEx = /^(.+)=(.+)/; @@ -11,7 +46,6 @@ var utils = { // extract hash component from url const hashStr = urlStr.replace(/^[^#]+#+(.*)/, '$1'); - if (hashStr.length === 0) { return false; } @@ -26,8 +60,8 @@ var utils = { return valuesByKey[key] || false; }, - getSwitchKeycodes: callback => { - chrome.runtime.sendMessage({ action: 'requestHotkeys' }, commands => { + getSwitchKeycodes: async (callback) => { + const commands = await chrome.commands.getAll(); // eslint-disable-next-line no-console console.dir(commands); @@ -58,6 +92,5 @@ var utils = { secondaryModifier, mainKeyCode, }); - }); }, }; diff --git a/manifest.json b/manifest.json index 06b5114..d1e21de 100644 --- a/manifest.json +++ b/manifest.json @@ -1,26 +1,20 @@ { "name": "Spaces", "description": "Intuitive tab management", - "version": "1.1.3", + "version": "1.1.4", "permissions": [ "tabs", "storage", "history", "unlimitedStorage", - "chrome://favicon/*", + "system.display", "contextMenus" ], "background": { - "scripts": [ - "js/db.js", - "js/dbService.js", - "js/spacesService.js", - "js/utils.js", - "js/background.js" - ], - "persistent": true + "service_worker": "js/background.js", + "type": "module" }, - "browser_action": { + "action": { "default_title": "Spaces", "default_icon": "img/icon128.png", "default_popup": "popup.html" @@ -31,9 +25,8 @@ "48": "img/icon48.png", "128": "img/icon128.png" }, - "web_accessible_resources": [], "incognito": "split", - "manifest_version": 2, + "manifest_version": 3, "minimum_chrome_version": "35", "commands": { diff --git a/popup.html b/popup.html index 520c4ad..76200d0 100644 --- a/popup.html +++ b/popup.html @@ -4,8 +4,8 @@ - - + + diff --git a/spaces.html b/spaces.html index 406c72d..5d2f45b 100644 --- a/spaces.html +++ b/spaces.html @@ -7,7 +7,7 @@ Spaces - + From bae10225d5510b2be29eeeecea50041bfd94315a Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Tue, 22 Jul 2025 22:51:57 -0700 Subject: [PATCH 02/92] Make fetchAllSpaces() async and get rid of the callback. Also simplify handleBackup() by using Array map() --- js/spaces.js | 85 +++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/js/spaces.js b/js/spaces.js index 04083d4..825cbaa 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -392,37 +392,30 @@ import { utils } from './utils.js'; } } - function handleBackup() { - const leanSpaces = []; - - fetchAllSpaces(spaces => { - // strip out unnessary content from each space - spaces.forEach(space => { - const leanTabs = []; - space.tabs.forEach(curTab => { - leanTabs.push({ + async function handleBackup() { + // strip out unnessary content from each space + const leanSpaces = (await fetchAllSpaces()).map(space => { + return { + name: space.name, + tabs: space.tabs.map(curTab => { + return { title: curTab.title, url: normaliseTabUrl(curTab.url), favIconUrl: curTab.favIconUrl, - }); - }); - - leanSpaces.push({ - name: space.name, - tabs: leanTabs, - }); - }); + }; + }), + }; + }); - const blob = new Blob([JSON.stringify(leanSpaces)], { - type: 'application/json', - }); - const blobUrl = URL.createObjectURL(blob); - const filename = 'spaces-backup.json'; - const link = document.createElement('a'); - link.setAttribute('href', blobUrl); - link.setAttribute('download', filename); - link.click(); + const blob = new Blob([JSON.stringify(leanSpaces)], { + type: 'application/json', }); + const blobUrl = URL.createObjectURL(blob); + const filename = 'spaces-backup.json'; + const link = document.createElement('a'); + link.setAttribute('href', blobUrl); + link.setAttribute('download', filename); + link.click(); } async function handleExport() { @@ -455,24 +448,21 @@ import { utils } from './utils.js'; return normalisedUrl; } - // SERVICES +// SERVICES - function fetchAllSpaces(callback) { - chrome.runtime.sendMessage( - { - action: 'requestAllSpaces', - }, - callback - ); - } +async function fetchAllSpaces() { + return await chrome.runtime.sendMessage({ + action: 'requestAllSpaces', + }); +} - async function fetchSpaceDetail(sessionId, windowId) { - return await chrome.runtime.sendMessage({ - action: 'requestSpaceDetail', - sessionId: sessionId || false, - windowId: windowId || false, - }); - } +async function fetchSpaceDetail(sessionId, windowId) { + return await chrome.runtime.sendMessage({ + action: 'requestSpaceDetail', + sessionId: sessionId || false, + windowId: windowId || false, + }); +} function performLoadSession(sessionId, callback) { chrome.runtime.sendMessage( @@ -689,19 +679,18 @@ import { utils } from './utils.js'; return false; } - function updateSpacesList(spaces) { + async function updateSpacesList(spaces) { // if spaces passed in then re-render immediately if (spaces) { renderSpacesList(spaces); // otherwise do a fetch of spaces first } else { - fetchAllSpaces(newSpaces => { - renderSpacesList(newSpaces); + const newSpaces = await fetchAllSpaces(); + renderSpacesList(newSpaces); - // determine if welcome banner should show - initialiseBanner(newSpaces); - }); + // determine if welcome banner should show + initialiseBanner(newSpaces); } } From ab89b80c253e3fce4e27b9a30c22570014d35064 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 23 Jul 2025 22:34:00 -0700 Subject: [PATCH 03/92] Remove callback hell in spaces.js by rewriting as async functions. --- js/spaces.js | 251 +++++++++++++++++++++++---------------------------- 1 file changed, 114 insertions(+), 137 deletions(-) diff --git a/js/spaces.js b/js/spaces.js index 825cbaa..80e019d 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -157,6 +157,7 @@ import { utils } from './utils.js'; if (tab.favIconUrl && tab.favIconUrl.indexOf('chrome://theme') < 0) { faviconSrc = tab.favIconUrl; } else { + // TODO(codedread): Fix this, it errors. faviconSrc = `chrome://favicon/${tab.url}`; } faviconEl.setAttribute('src', faviconSrc); @@ -193,39 +194,36 @@ import { utils } from './utils.js'; } } - function setBannerState(state) { + async function setBannerState(state) { const lessonOneEl = document.getElementById('lessonOne'); const lessonTwoEl = document.getElementById('lessonTwo'); if (state !== bannerState) { bannerState = state; - toggleBanner(false, () => { - if (state > 0) { - nodes.banner.style.display = 'block'; - if (state === 1) { - lessonOneEl.style.display = 'block'; - lessonTwoEl.style.display = 'none'; - } else if (state === 2) { - lessonOneEl.style.display = 'none'; - lessonTwoEl.style.display = 'block'; - } - toggleBanner(true); + await toggleBanner(false); + if (state > 0) { + nodes.banner.style.display = 'block'; + if (state === 1) { + lessonOneEl.style.display = 'block'; + lessonTwoEl.style.display = 'none'; + } else if (state === 2) { + lessonOneEl.style.display = 'none'; + lessonTwoEl.style.display = 'block'; } - }); + await toggleBanner(true); + } } } - function toggleBanner(visible, callback) { +async function toggleBanner(visible) { + return new Promise(resolve => { setTimeout(() => { nodes.banner.className = visible ? ' ' : 'hidden'; - if (typeof callback === 'function') { - setTimeout(() => { - callback(); - }, 200); - } + setTimeout(() => resolve(), 200); }, 100); - } + }); +} function toggleModal(visible) { nodes.modalBlocker.style.display = visible ? 'block' : 'none'; @@ -239,25 +237,23 @@ import { utils } from './utils.js'; // ACTION HANDLERS - function handleLoadSpace(sessionId, windowId) { + async function handleLoadSpace(sessionId, windowId) { if (sessionId) { - performLoadSession(sessionId, () => { - reroute(sessionId, false, false); - }); + await performLoadSession(sessionId); + reroute(sessionId, false, false); } else if (windowId) { - performLoadWindow(windowId, () => { - reroute(false, windowId, false); - }); + await performLoadWindow(windowId); + reroute(false, windowId, false); } } - function handleLoadTab(sessionId, windowId, tabUrl) { + async function handleLoadTab(sessionId, windowId, tabUrl) { const noop = () => {}; if (sessionId) { - performLoadTabInSession(sessionId, tabUrl, noop); + await performLoadTabInSession(sessionId, tabUrl, noop); } else if (windowId) { - performLoadTabInWindow(windowId, tabUrl, noop); + await performLoadTabInWindow(windowId, tabUrl, noop); } } @@ -320,13 +316,11 @@ import { utils } from './utils.js'; // otherwise call the save service if (sessionId) { - performSessionUpdate(newName, sessionId, session => { - if (session) reroute(session.id, false, true); - }); + const session = await performSessionUpdate(newName, sessionId); + if (session) reroute(session.id, false, true); } else if (windowId) { - performNewSessionSave(newName, windowId, session => { - if (session) reroute(session.id, false, true); - }); + const session = await performNewSessionSave(newName, windowId); + if (session) reroute(session.id, false, true); } // handle banner @@ -351,10 +345,9 @@ import { utils } from './utils.js'; ); if (confirm) { - performDelete(sessionId, () => { - updateSpacesList(); - reroute(false, false, true); - }); + await performDelete(sessionId); + updateSpacesList(); + reroute(false, false, true); } } } @@ -384,9 +377,8 @@ import { utils } from './utils.js'; }); if (urlList.length > 0) { - performSessionImport(urlList, session => { - if (session) reroute(session.id, false, true); - }); + const session = await performSessionImport(urlList); + if (session) reroute(session.id, false, true); } } } @@ -450,122 +442,107 @@ import { utils } from './utils.js'; // SERVICES +/** @returns {Promise} */ async function fetchAllSpaces() { - return await chrome.runtime.sendMessage({ + return chrome.runtime.sendMessage({ action: 'requestAllSpaces', }); } +/** @returns {Promise} */ async function fetchSpaceDetail(sessionId, windowId) { - return await chrome.runtime.sendMessage({ + return chrome.runtime.sendMessage({ action: 'requestSpaceDetail', sessionId: sessionId || false, windowId: windowId || false, }); } - function performLoadSession(sessionId, callback) { - chrome.runtime.sendMessage( - { - action: 'loadSession', - sessionId, - }, - callback - ); - } - - function performLoadWindow(windowId, callback) { - chrome.runtime.sendMessage( - { - action: 'loadWindow', - windowId, - }, - callback - ); - } +/** @returns {Promise} */ +async function performLoadSession(sessionId) { + return chrome.runtime.sendMessage({ + action: 'loadSession', + sessionId, + }); +} - function performLoadTabInSession(sessionId, tabUrl, callback) { - chrome.runtime.sendMessage( - { - action: 'loadTabInSession', - sessionId, - tabUrl, - }, - callback - ); - } +/** @returns {Promise} */ +async function performLoadWindow(windowId) { + return chrome.runtime.sendMessage({ + action: 'loadWindow', + windowId, + }); +} - function performLoadTabInWindow(windowId, tabUrl, callback) { - chrome.runtime.sendMessage( - { - action: 'loadTabInWindow', - windowId, - tabUrl, - }, - callback - ); - } +/** @returns {Promise} */ +async function performLoadTabInSession(sessionId, tabUrl) { + return chrome.runtime.sendMessage({ + action: 'loadTabInSession', + sessionId, + tabUrl, + }); +} - function performDelete(sessionId, callback) { - chrome.runtime.sendMessage( - { - action: 'deleteSession', - sessionId, - }, - callback - ); - } +/** @returns {Promise} */ +async function performLoadTabInWindow(windowId, tabUrl) { + return chrome.runtime.sendMessage({ + action: 'loadTabInWindow', + windowId, + tabUrl, + }); +} - function performSessionUpdate(newName, sessionId, callback) { - chrome.runtime.sendMessage( - { - action: 'updateSessionName', - deleteOld: true, - sessionName: newName, - sessionId, - }, - callback - ); - } +/** @returns {Promise} */ +async function performDelete(sessionId) { + return chrome.runtime.sendMessage({ + action: 'deleteSession', + sessionId, + }); +} - function performNewSessionSave(newName, windowId, callback) { - chrome.runtime.sendMessage( - { - action: 'saveNewSession', - deleteOld: true, - sessionName: newName, - windowId, - }, - callback - ); - } +/** @returns {Promise} */ +async function performSessionUpdate(newName, sessionId) { + return chrome.runtime.sendMessage({ + action: 'updateSessionName', + deleteOld: true, + sessionName: newName, + sessionId, + }); +} - function performSessionImport(urlList, callback) { - chrome.runtime.sendMessage( - { - action: 'importNewSession', - urlList, - }, - callback - ); - } +/** @returns {Promise} */ +async function performNewSessionSave(newName, windowId) { + return chrome.runtime.sendMessage({ + action: 'saveNewSession', + deleteOld: true, + sessionName: newName, + windowId, + }); +} - async function performRestoreFromBackup(spaces) { - for (const space of spaces) { - const canOverwrite = await utils.checkSessionOverwrite(space.name); - if (!canOverwrite) { - continue; - } +/** @returns {Promise} */ +async function performSessionImport(urlList) { + return chrome.runtime.sendMessage({ + action: 'importNewSession', + urlList, + }); +} - await chrome.runtime.sendMessage( - { - action: 'restoreFromBackup', - deleteOld: true, - space, - } - ); +/** @returns {Promise} */ +async function performRestoreFromBackup(spaces) { + for (const space of spaces) { + const canOverwrite = await utils.checkSessionOverwrite(space.name); + if (!canOverwrite) { + continue; } + + await chrome.runtime.sendMessage({ + action: 'restoreFromBackup', + deleteOld: true, + space, + }); } +} // EVENT LISTENERS FOR STATIC DOM ELEMENTS From e6a1a9d57e3121b0ca9becbf4461890112ee4d68 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 1 Aug 2025 09:48:47 -0700 Subject: [PATCH 04/92] Modernize some JavaScript, async/await a couple more things, and shuffle manifest permissions. --- js/popup.js | 56 +++++++++++++++++++------------------------- js/spacesRenderer.js | 51 ++++++++++++++++++---------------------- manifest.json | 9 +++---- 3 files changed, 52 insertions(+), 64 deletions(-) diff --git a/js/popup.js b/js/popup.js index afc3cf8..c8a19fe 100644 --- a/js/popup.js +++ b/js/popup.js @@ -3,7 +3,6 @@ import { spacesRenderer } from './spacesRenderer.js'; import { utils } from './utils.js'; -(() => { const UNSAVED_SESSION = '(unnamed window)'; const NO_HOTKEY = 'no hotkey set'; @@ -149,12 +148,11 @@ import { utils } from './utils.js'; }); document .querySelector('#moverLink .optionText') - .addEventListener('click', () => { - chrome.runtime.sendMessage({'action': 'generatePopupParams',}).then(params => { - if (!params) return; - window.location.hash = params; - window.location.reload(); - }); + .addEventListener('click', async () => { + const params = await chrome.runtime.sendMessage({'action': 'generatePopupParams'}); + if (!params) return; + window.location.hash = params; + window.location.reload(); // renderMoveCard() }); } @@ -277,7 +275,7 @@ import { utils } from './utils.js'; * MOVE VIEW */ - function renderMoveCard() { + async function renderMoveCard() { document.getElementById( 'popupContainer' ).innerHTML = document.getElementById('moveTemplate').innerHTML; @@ -332,29 +330,23 @@ import { utils } from './utils.js'; updateTabDetails(); - chrome.runtime.sendMessage( - { - action: 'requestAllSpaces', - }, - spaces => { - // remove currently visible space - const filteredSpaces = spaces.filter(space => { - return `${space.windowId}` !== globalWindowId; - }); - spacesRenderer.initialise(5, false); - spacesRenderer.renderSpaces(filteredSpaces); - - const allSpaceEls = document.querySelectorAll('.space'); - Array.prototype.forEach.call(allSpaceEls, el => { - // eslint-disable-next-line no-param-reassign - const existingClickHandler = el.onclick; - el.onclick = e => { - existingClickHandler(e); - handleSelectAction(); - }; - }); - } - ); + const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); + // remove currently visible space + const filteredSpaces = spaces.filter(space => { + return `${space.windowId}` !== globalWindowId; + }); + spacesRenderer.initialise(5, false); + spacesRenderer.renderSpaces(filteredSpaces); + + const allSpaceEls = document.querySelectorAll('.space'); + for (const el of allSpaceEls) { + // eslint-disable-next-line no-param-reassign + const existingClickHandler = el.onclick; + el.onclick = e => { + existingClickHandler(e); + handleSelectAction(); + }; + } } function updateTabDetails() { @@ -378,6 +370,7 @@ import { utils } from './utils.js'; ) { faviconSrc = tab.favIconUrl; } else { + // TODO(codedread): Fix this, it errors. faviconSrc = `chrome://favicon/${tab.url}`; } nodes.activeTabFavicon.setAttribute('src', faviconSrc); @@ -458,4 +451,3 @@ import { utils } from './utils.js'; edit: 'true', }); } -})(); diff --git a/js/spacesRenderer.js b/js/spacesRenderer.js index 187f0a4..3c18c3c 100644 --- a/js/spacesRenderer.js +++ b/js/spacesRenderer.js @@ -1,10 +1,10 @@ // eslint-disable-next-line no-var -export var spacesRenderer = { +export const spacesRenderer = { nodes: {}, maxSuggestions: 10, oneClickMode: false, - initialise: (maxSuggestions, oneClickMode) => { + initialise(maxSuggestions, oneClickMode) { spacesRenderer.maxSuggestions = maxSuggestions; spacesRenderer.oneClickMode = oneClickMode; @@ -22,11 +22,11 @@ export var spacesRenderer = { spacesRenderer.addEventListeners(); }, - renderSpaces: spaces => { - spaces.forEach(space => { + renderSpaces(spaces) { + for (const space of spaces) { const spaceEl = spacesRenderer.renderSpaceEl(space); spacesRenderer.nodes.spacesList.appendChild(spaceEl); - }); + } spacesRenderer.selectSpace(spacesRenderer.getFirstSpaceEl(), false); spacesRenderer.updateSpacesList(); @@ -34,7 +34,7 @@ export var spacesRenderer = { spacesRenderer.nodes.moveInput.focus(); }, - renderSpaceEl: space => { + renderSpaceEl(space) { const listContainer = document.createElement('div'); const listTitle = document.createElement('span'); const listDetail = document.createElement('span'); @@ -70,37 +70,36 @@ export var spacesRenderer = { return listContainer; }, - handleSpaceClick: e => { + handleSpaceClick(e) { const el = e.target.tagName === 'SPAN' ? e.target.parentElement : e.target; spacesRenderer.selectSpace(el, !spacesRenderer.oneClickMode); }, - handleSelectionNavigation: direction => { + handleSelectionNavigation(direction) { const spaceEls = document.querySelectorAll('#spacesList .space'); let prevEl = false; let selectNext = false; let selectedSpaceEl; - Array.prototype.some.call(spaceEls, el => { - if (el.style.visibility !== 'visible') return false; + for (const el of spaceEls) { + if (el.style.visibility !== 'visible') continue; // locate currently selected space if (el.className.indexOf('selected') >= 0) { if (direction === 'up' && prevEl) { selectedSpaceEl = prevEl; - return true; + break; } if (direction === 'down') { selectNext = true; } } else if (selectNext) { selectedSpaceEl = el; - return true; + break; } prevEl = el; - return false; - }); + } if (selectedSpaceEl) { spacesRenderer.selectSpace( selectedSpaceEl, @@ -109,20 +108,16 @@ export var spacesRenderer = { } }, - getFirstSpaceEl: () => { - const allSpaceEls = document.querySelectorAll('#spacesList .space'); - let firstSpaceEl = false; - Array.prototype.some.call(allSpaceEls, spaceEl => { + getFirstSpaceEl() { + for (const spaceEl of document.querySelectorAll('#spacesList .space')) { if (spaceEl.style.visibility === 'visible') { - firstSpaceEl = spaceEl; - return true; + return spaceEl; } - return false; - }); - return firstSpaceEl; + } + return false; }, - selectSpace: (selectedSpaceEl, updateText) => { + selectSpace(selectedSpaceEl, updateText) { const allSpaceEls = document.querySelectorAll('#spacesList .space'); for (let i = 0; i < allSpaceEls.length; i += 1) { @@ -150,7 +145,7 @@ export var spacesRenderer = { } }, - getDefaultSpaceTitle: space => { + getDefaultSpaceTitle(space) { const count = space.tabs && space.tabs.length; if (!count) return ''; const firstTitle = space.tabs[0].title; @@ -162,7 +157,7 @@ export var spacesRenderer = { : `[${firstTitle}] +${count - 1} more`; }, - getTabDetailsString: space => { + getTabDetailsString(space) { const count = space.tabs && space.tabs.length; const open = space.windowId; @@ -172,7 +167,7 @@ export var spacesRenderer = { return `(${count} tab${count > 1 ? 's' : ''})`; }, - updateSpacesList: () => { + updateSpacesList() { const query = spacesRenderer.nodes.moveInput.value; let match = false; let exactMatch = false; @@ -223,7 +218,7 @@ export var spacesRenderer = { spacesRenderer.selectSpace(spacesRenderer.getFirstSpaceEl(), false); }, - addEventListeners: () => { + addEventListeners() { spacesRenderer.nodes.moveInput.parentElement.parentElement.onkeyup = e => { // listen for 'up' key if (e.keyCode === 38) { diff --git a/manifest.json b/manifest.json index d1e21de..855650f 100644 --- a/manifest.json +++ b/manifest.json @@ -3,12 +3,13 @@ "description": "Intuitive tab management", "version": "1.1.4", "permissions": [ - "tabs", - "storage", + "contextMenus", + "favicon", "history", - "unlimitedStorage", + "storage", "system.display", - "contextMenus" + "tabs", + "unlimitedStorage" ], "background": { "service_worker": "js/background.js", From 0baf441570d77d21ad9c79382d50a6019a1286db Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 10 Aug 2025 19:24:20 -0700 Subject: [PATCH 05/92] Clean up some logging --- js/popup.js | 5 +---- js/spaces.js | 2 +- js/spacesService.js | 5 ++++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js/popup.js b/js/popup.js index c8a19fe..0cf4d7e 100644 --- a/js/popup.js +++ b/js/popup.js @@ -94,7 +94,6 @@ import { utils } from './utils.js'; } function handleCloseAction() { - const { utils } = chrome.extension.getBackgroundPage(); const opener = utils.getHashVariable('opener', window.location.href); if (opener && opener === 'bg') { chrome.runtime.sendMessage({ @@ -158,9 +157,7 @@ import { utils } from './utils.js'; } async function requestHotkeys() { - console.log('requestHotkeys called'); const commands = await chrome.commands.getAll(); - console.dir(commands); let switchStr; let moveStr; let spacesStr; @@ -371,7 +368,7 @@ import { utils } from './utils.js'; faviconSrc = tab.favIconUrl; } else { // TODO(codedread): Fix this, it errors. - faviconSrc = `chrome://favicon/${tab.url}`; + // faviconSrc = `chrome://favicon/${tab.url}`; } nodes.activeTabFavicon.setAttribute('src', faviconSrc); diff --git a/js/spaces.js b/js/spaces.js index 80e019d..6dece0d 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -158,7 +158,7 @@ import { utils } from './utils.js'; faviconSrc = tab.favIconUrl; } else { // TODO(codedread): Fix this, it errors. - faviconSrc = `chrome://favicon/${tab.url}`; + //faviconSrc = `chrome://favicon/${tab.url}`; } faviconEl.setAttribute('src', faviconSrc); diff --git a/js/spacesService.js b/js/spacesService.js index 262134b..8d694cf 100644 --- a/js/spacesService.js +++ b/js/spacesService.js @@ -535,8 +535,11 @@ export var spacesService = { }); return result.length >= 1 ? result[0] : false; }, - getSessionByName: name => { + getSessionByName(name) { const result = spacesService.sessions.filter(session => { + if (!name) { + console.log(`name was undefined`) + } return ( session.name && session.name.toLowerCase() === name.toLowerCase() From cb430c21735d71f247315fc25ff26339121c3eea Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 11 Aug 2025 21:55:38 -0700 Subject: [PATCH 06/92] Remove unnecessary arrow function and tighten up some indents --- js/background.js | 38 +++++++++++++++--------- js/spaces.js | 38 ++++++++++++------------ js/spacesService.js | 71 ++++++++++++++++++++++++++------------------- js/switcher.js | 6 ++-- js/utils.js | 4 +-- 5 files changed, 90 insertions(+), 67 deletions(-) diff --git a/js/background.js b/js/background.js index 6eeb7da..dd04f0d 100644 --- a/js/background.js +++ b/js/background.js @@ -9,6 +9,9 @@ import { spacesService } from './spacesService.js'; /** @typedef {import('./common.js').SessionPresence} SessionPresence */ +/** @typedef {import('./common.js').Space} Space */ + +// TODO(codedread): Eliminate all global variables and use chrome.storage.local. // eslint-disable-next-line no-unused-vars, no-var let spacesPopupWindowId = false; @@ -665,6 +668,7 @@ chrome.runtime.onInstalled.addListener(details => { } function requestCurrentSpace(callback) { + console.log(`codedread: requestCurrentSpace() called`); chrome.windows.getCurrent(window => { requestSpaceFromWindowId(window.id, callback); }); @@ -677,28 +681,35 @@ chrome.runtime.onInstalled.addListener(details => { const session = spacesService.getSessionByWindowId(windowId); if (session) { - callback({ + console.log(`codedread: requestSpaceFromWindowId() found session: ${session.id} for windowId: ${windowId}`); + /** @type {Space} */ + const space = { sessionId: session.id, windowId: session.windowId, name: session.name, tabs: session.tabs, history: session.history, - }); + }; + console.dir(space); + callback(space); - // otherwise build a space object out of the actual window + // otherwise build a space object out of the actual window } else { + console.log(`codedread: requestSpaceFromWindowId() found no session for windowId: ${windowId}`); chrome.windows.get(windowId, { populate: true }, window => { // if failed to load requested window if (chrome.runtime.lastError) { callback(false); } else { - callback({ + /** @type {Space} */ + const space = { sessionId: false, windowId: window.id, name: false, tabs: window.tabs, history: false, - }); + }; + callback(space); } }); } @@ -1059,16 +1070,17 @@ chrome.runtime.onInstalled.addListener(details => { moveTabToWindow(tab, windowId, callback); }); } - function moveTabToWindow(tab, windowId, callback) { - chrome.tabs.move(tab.id, { windowId, index: -1 }); - // NOTE: this move does not seem to trigger any tab event listeners - // so we need to update sessions manually - spacesService.queueWindowEvent(tab.windowId); - spacesService.queueWindowEvent(windowId); +function moveTabToWindow(tab, windowId, callback) { + chrome.tabs.move(tab.id, { windowId, index: -1 }); - callback(true); - } + // NOTE: this move does not seem to trigger any tab event listeners + // so we need to update sessions manually + spacesService.queueWindowEvent(tab.windowId); + spacesService.queueWindowEvent(windowId); + + callback(true); +} spacesService.initialiseSpaces(); spacesService.initialiseTabHistory(); diff --git a/js/spaces.js b/js/spaces.js index 6dece0d..a8107ff 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -2,29 +2,29 @@ import { utils } from './utils.js'; - const UNSAVED_SESSION_NAME = 'Unnamed window'; - const UNSAVED_SESSION = `${UNSAVED_SESSION_NAME}`; - const nodes = {}; - let globalSelectedSpace; - let bannerState; +const UNSAVED_SESSION_NAME = 'Unnamed window'; +const UNSAVED_SESSION = `${UNSAVED_SESSION_NAME}`; +const nodes = {}; +let globalSelectedSpace; +let bannerState; - // METHODS FOR RENDERING SIDENAV (spaces list) +// METHODS FOR RENDERING SIDENAV (spaces list) - function renderSpacesList(spaces) { - let spaceEl; +function renderSpacesList(spaces) { + let spaceEl; - nodes.openSpaces.innerHTML = ''; - nodes.closedSpaces.innerHTML = ''; + nodes.openSpaces.innerHTML = ''; + nodes.closedSpaces.innerHTML = ''; - spaces.forEach(space => { - spaceEl = renderSpaceListEl(space); - if (space.windowId) { - nodes.openSpaces.appendChild(spaceEl); - } else { - nodes.closedSpaces.appendChild(spaceEl); - } - }); - } + spaces.forEach(space => { + spaceEl = renderSpaceListEl(space); + if (space.windowId) { + nodes.openSpaces.appendChild(spaceEl); + } else { + nodes.closedSpaces.appendChild(spaceEl); + } + }); +} function renderSpaceListEl(space) { let hash; diff --git a/js/spacesService.js b/js/spacesService.js index 8d694cf..ee4107b 100644 --- a/js/spacesService.js +++ b/js/spacesService.js @@ -6,6 +6,8 @@ import { dbService } from './dbService.js'; +// TODO(codedread): Eliminate all global variables and use chrome.storage.local. + // eslint-disable-next-line no-var export var spacesService = { tabHistoryUrlMap: {}, @@ -20,7 +22,7 @@ export var spacesService = { noop: () => {}, // initialise spaces - combine open windows with saved sessions - initialiseSpaces: async () => { + async initialiseSpaces() { // update version numbers spacesService.lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); @@ -54,7 +56,7 @@ export var spacesService = { }); }, - resetAllSessionHashes: sessions => { + resetAllSessionHashes(sessions) { sessions.forEach(session => { // eslint-disable-next-line no-param-reassign session.sessionHash = spacesService.generateSessionHash( @@ -65,7 +67,7 @@ export var spacesService = { }, // record each tab's id and url so we can add history items when tabs are removed - initialiseTabHistory: () => { + initialiseTabHistory() { chrome.tabs.query({}, tabs => { tabs.forEach(tab => { spacesService.tabHistoryUrlMap[tab.id] = tab.url; @@ -75,7 +77,7 @@ export var spacesService = { // NOTE: if ever changing this funciton, then we'll need to update all // saved sessionHashes so that they match next time, using: resetAllSessionHashes() - _cleanUrl: url => { + _cleanUrl(url) { if (!url) { return ''; } @@ -116,7 +118,7 @@ export var spacesService = { return cleanUrl; }, - generateSessionHash: tabs => { + generateSessionHash(tabs) { const text = tabs.reduce((prevStr, tab) => { return prevStr + spacesService._cleanUrl(tab.url); }, ''); @@ -133,7 +135,7 @@ export var spacesService = { return Math.abs(hash); }, - filterInternalWindows: curWindow => { + filterInternalWindows(curWindow) { // sanity check to make sure window isnt an internal spaces window if ( curWindow.tabs.length === 1 && @@ -149,7 +151,7 @@ export var spacesService = { return false; }, - checkForSessionMatch: curWindow => { + checkForSessionMatch(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; } @@ -186,7 +188,7 @@ export var spacesService = { } }, - matchSessionToWindow: (session, curWindow) => { + matchSessionToWindow(session, curWindow) { // remove any other sessions tied to this windowId (temporary sessions) for (let i = spacesService.sessions.length - 1; i >= 0; i -= 1) { if (spacesService.sessions[i].windowId === curWindow.id) { @@ -203,7 +205,7 @@ export var spacesService = { session.windowId = curWindow.id; }, - createTemporaryUnmatchedSession: curWindow => { + createTemporaryUnmatchedSession(curWindow) { if (spacesService.debug) { // eslint-disable-next-line no-console console.dir(spacesService.sessions); @@ -227,7 +229,7 @@ export var spacesService = { }, // local storage getters/setters - fetchLastVersion: async () => { + async fetchLastVersion() { let version = await chrome.storage.local.get(['spacesVersion']); if (version !== null && version['spacesVersion']) { version = JSON.parse(version['spacesVersion']); @@ -236,7 +238,7 @@ export var spacesService = { return 0; }, - setLastVersion: newVersion => { + setLastVersion(newVersion) { chrome.storage.local.set({'spacesVersion': JSON.stringify(newVersion)}); }, @@ -244,7 +246,7 @@ export var spacesService = { // (events are received and screened first in background.js) // ----------------------------------------------------------------------------------------- - handleTabRemoved: (tabId, removeInfo, callback) => { + handleTabRemoved(tabId, removeInfo, callback) { if (spacesService.debug) // eslint-disable-next-line no-console console.log( @@ -282,7 +284,8 @@ export var spacesService = { delete spacesService.tabHistoryUrlMap[tabId]; } }, - handleTabMoved: (tabId, moveInfo, callback) => { + + handleTabMoved(tabId, moveInfo, callback) { if (spacesService.debug) // eslint-disable-next-line no-console console.log( @@ -294,14 +297,16 @@ export var spacesService = { callback ); }, - handleTabUpdated: (tab, changeInfo, callback) => { + + handleTabUpdated(tab, changeInfo, callback) { // NOTE: only queue event when tab has completed loading (title property exists at this point) if (tab.status === 'complete') { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `handlingTabUpdated event. windowId: ${tab.windowId}` ); + } // update tab history in case the tab url has changed spacesService.tabHistoryUrlMap[tab.id] = tab.url; @@ -322,7 +327,8 @@ export var spacesService = { }); } }, - handleWindowRemoved: (windowId, markAsClosed, callback) => { + + handleWindowRemoved(windowId, markAsClosed, callback) { // ignore subsequent windowRemoved events for the same windowId (each closing tab will try to call this) if (spacesService.closedWindowIds[windowId]) { callback(); @@ -362,7 +368,8 @@ export var spacesService = { callback(); }, - handleWindowFocussed: windowId => { + + handleWindowFocussed(windowId) { if (spacesService.debug) // eslint-disable-next-line no-console console.log(`handlingWindowFocussed event. windowId: ${windowId}`); @@ -381,7 +388,7 @@ export var spacesService = { // Set a timeout so that multiple tabs all opened at once (like when restoring a session) // only trigger this function once (as per the timeout set by the last tab event) // This will cause multiple triggers if time between tab openings is longer than 1 sec - queueWindowEvent: (windowId, eventId, callback) => { + queueWindowEvent(windowId, eventId, callback) { clearTimeout(spacesService.sessionUpdateTimers[windowId]); spacesService.eventQueueCount += 1; @@ -392,7 +399,7 @@ export var spacesService = { }, // careful here as this function gets called A LOT - handleWindowEvent: (windowId, eventId, callback) => { + handleWindowEvent(windowId, eventId, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; @@ -514,19 +521,21 @@ export var spacesService = { // PUBLIC FUNCTIONS - getSessionBySessionId: sessionId => { + getSessionBySessionId(sessionId) { const result = spacesService.sessions.filter(session => { return session.id === sessionId; }); return result.length === 1 ? result[0] : false; }, - getSessionByWindowId: windowId => { + + getSessionByWindowId(windowId) { const result = spacesService.sessions.filter(session => { return session.windowId === windowId; }); return result.length === 1 ? result[0] : false; }, - getSessionBySessionHash: (hash, closedOnly) => { + + getSessionBySessionHash(hash, closedOnly) { const result = spacesService.sessions.filter(session => { if (closedOnly) { return session.sessionHash === hash && !session.windowId; @@ -535,6 +544,7 @@ export var spacesService = { }); return result.length >= 1 ? result[0] : false; }, + getSessionByName(name) { const result = spacesService.sessions.filter(session => { if (!name) { @@ -547,11 +557,12 @@ export var spacesService = { }); return result.length >= 1 ? result[0] : false; }, - getAllSessions: () => { + + getAllSessions() { return spacesService.sessions; }, - addUrlToSessionHistory: (session, newUrl) => { + addUrlToSessionHistory(session, newUrl) { if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`adding tab to history: ${newUrl}`); @@ -596,7 +607,7 @@ export var spacesService = { return session; }, - removeUrlFromSessionHistory: (session, newUrl) => { + removeUrlFromSessionHistory(session, newUrl) { if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`removing tab from history: ${newUrl}`); @@ -621,7 +632,7 @@ export var spacesService = { // Database actions - updateSessionTabs: (sessionId, tabs, callback) => { + updateSessionTabs(sessionId, tabs, callback) { const session = spacesService.getSessionBySessionId(sessionId); // eslint-disable-next-line no-param-reassign @@ -635,7 +646,7 @@ export var spacesService = { spacesService.saveExistingSession(session.id, callback); }, - updateSessionName: (sessionId, sessionName, callback) => { + updateSessionName(sessionId, sessionName, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; @@ -646,7 +657,7 @@ export var spacesService = { spacesService.saveExistingSession(session.id, callback); }, - saveExistingSession: (sessionId, callback) => { + saveExistingSession(sessionId, callback) { const session = spacesService.getSessionBySessionId(sessionId); // eslint-disable-next-line no-param-reassign @@ -656,7 +667,7 @@ export var spacesService = { dbService.updateSession(session, callback); }, - saveNewSession: (sessionName, tabs, windowId, callback) => { + saveNewSession(sessionName, tabs, windowId, callback) { if (!tabs) { callback(); return; @@ -698,7 +709,7 @@ export var spacesService = { }); }, - deleteSession: (sessionId, callback) => { + deleteSession(sessionId, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; diff --git a/js/switcher.js b/js/switcher.js index bd56d9d..baaade4 100644 --- a/js/switcher.js +++ b/js/switcher.js @@ -2,9 +2,9 @@ import { spacesRenderer } from './spacesRenderer.js'; - function getSelectedSpace() { - return document.querySelector('.space.selected'); - } +function getSelectedSpace() { + return document.querySelector('.space.selected'); +} function handleSwitchAction(selectedSpaceEl) { chrome.runtime.sendMessage({ diff --git a/js/utils.js b/js/utils.js index 62ce416..6f00bad 100644 --- a/js/utils.js +++ b/js/utils.js @@ -36,7 +36,7 @@ export var utils = { ); }, - getHashVariable: (key, urlStr) => { + getHashVariable(key, urlStr) { const valuesByKey = {}; const keyPairRegEx = /^(.+)=(.+)/; @@ -60,7 +60,7 @@ export var utils = { return valuesByKey[key] || false; }, - getSwitchKeycodes: async (callback) => { + async getSwitchKeycodes(callback) { const commands = await chrome.commands.getAll(); // eslint-disable-next-line no-console console.dir(commands); From deaaa90aeab8ef691aee2977e7e60d489457f348 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 11 Aug 2025 22:01:35 -0700 Subject: [PATCH 07/92] Move all service worker files into the background folder --- js/{ => background}/background.js | 0 js/{ => background}/db.js | 0 js/{ => background}/dbService.js | 0 js/{ => background}/spacesService.js | 0 manifest.json | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) rename js/{ => background}/background.js (100%) rename js/{ => background}/db.js (100%) rename js/{ => background}/dbService.js (100%) rename js/{ => background}/spacesService.js (100%) diff --git a/js/background.js b/js/background/background.js similarity index 100% rename from js/background.js rename to js/background/background.js diff --git a/js/db.js b/js/background/db.js similarity index 100% rename from js/db.js rename to js/background/db.js diff --git a/js/dbService.js b/js/background/dbService.js similarity index 100% rename from js/dbService.js rename to js/background/dbService.js diff --git a/js/spacesService.js b/js/background/spacesService.js similarity index 100% rename from js/spacesService.js rename to js/background/spacesService.js diff --git a/manifest.json b/manifest.json index 855650f..8d4bec1 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,7 @@ "unlimitedStorage" ], "background": { - "service_worker": "js/background.js", + "service_worker": "js/background/background.js", "type": "module" }, "action": { From 85bc620e443d8ae81cf32ae5b6b84b5d7de66475 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 11 Aug 2025 23:13:55 -0700 Subject: [PATCH 08/92] Turn more functions into async in spacesService --- js/background/background.js | 72 +++++++++++++++++----------------- js/background/spacesService.js | 66 +++++++++++++++---------------- 2 files changed, 68 insertions(+), 70 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index dd04f0d..604110f 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -44,7 +44,7 @@ chrome.runtime.onInstalled.addListener(details => { }); }); - // LISTENERS +// LISTENERS // add listeners for session monitoring chrome.tabs.onCreated.addListener(tab => { @@ -518,7 +518,7 @@ chrome.runtime.onInstalled.addListener(details => { return ''; } - const session = spacesService.getSessionByWindowId(activeTab.windowId); + const session = await spacesService.getSessionByWindowId(activeTab.windowId); const name = session ? session.name : ''; @@ -654,14 +654,14 @@ chrome.runtime.onInstalled.addListener(details => { return false; } - /** - * @param {string} sessionName - * @returns {SessionPresence} - */ - function requestSessionPresence(sessionName) { - const session = spacesService.getSessionByName(sessionName); - return { exists: !!session, isOpen: !!session && !!session.windowId }; - } +/** + * @param {string} sessionName + * @returns {SessionPresence} + */ +async function requestSessionPresence(sessionName) { + const session = await spacesService.getSessionByName(sessionName); + return { exists: !!session, isOpen: !!session && !!session.windowId }; +} function requestTabDetail(tabId, callback) { chrome.tabs.get(tabId, callback); @@ -676,9 +676,9 @@ chrome.runtime.onInstalled.addListener(details => { // returns a 'space' object which is essentially the same as a session object // except that includes space.sessionId (session.id) and space.windowId - function requestSpaceFromWindowId(windowId, callback) { + async function requestSpaceFromWindowId(windowId, callback) { // first check for an existing session matching this windowId - const session = spacesService.getSessionByWindowId(windowId); + const session = await spacesService.getSessionByWindowId(windowId); if (session) { console.log(`codedread: requestSpaceFromWindowId() found session: ${session.id} for windowId: ${windowId}`); @@ -715,8 +715,8 @@ chrome.runtime.onInstalled.addListener(details => { } } - function requestSpaceFromSessionId(sessionId, callback) { - const session = spacesService.getSessionBySessionId(sessionId); + async function requestSpaceFromSessionId(sessionId, callback) { + const session = await spacesService.getSessionBySessionId(sessionId); callback({ sessionId: session.id, @@ -727,8 +727,8 @@ chrome.runtime.onInstalled.addListener(details => { }); } - function requestAllSpaces(callback) { - const sessions = spacesService.getAllSessions(); + async function requestAllSpaces(callback) { + const sessions = await spacesService.getAllSessions(); const allSpaces = sessions .map(session => { return { sessionId: session.id, ...session }; @@ -762,7 +762,7 @@ chrome.runtime.onInstalled.addListener(details => { } async function handleLoadSession(sessionId, tabUrl) { - const session = spacesService.getSessionBySessionId(sessionId); + const session = await spacesService.getSessionBySessionId(sessionId); // if space is already open, then give it focus if (session.windowId) { @@ -860,8 +860,8 @@ chrome.runtime.onInstalled.addListener(details => { } function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { - chrome.windows.get(windowId, { populate: true }, curWindow => { - const existingSession = spacesService.getSessionByName(sessionName); + chrome.windows.get(windowId, { populate: true }, async curWindow => { + const existingSession = await spacesService.getSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session if (existingSession) { @@ -885,10 +885,9 @@ chrome.runtime.onInstalled.addListener(details => { }); } - function handleRestoreFromBackup(space, deleteOld, callback) { - + async function handleRestoreFromBackup(space, deleteOld, callback) { const existingSession = space.name - ? spacesService.getSessionByName(space.name) + ? await spacesService.getSessionByName(space.name) : false; // if session with same name already exist, then prompt to override the existing session @@ -913,11 +912,11 @@ chrome.runtime.onInstalled.addListener(details => { ); } - function handleImportNewSession(urlList, callback) { + async function handleImportNewSession(urlList, callback) { let tempName = 'Imported space: '; let count = 1; - while (spacesService.getSessionByName(tempName + count)) { + while (await spacesService.getSessionByName(tempName + count)) { count += 1; } @@ -931,9 +930,9 @@ chrome.runtime.onInstalled.addListener(details => { spacesService.saveNewSession(tempName, tabList, false, callback); } - function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { + async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { // check to make sure session name doesn't already exist - const existingSession = spacesService.getSessionByName(sessionName); + const existingSession = await spacesService.getSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session if (existingSession) { @@ -951,8 +950,8 @@ chrome.runtime.onInstalled.addListener(details => { spacesService.updateSessionName(sessionId, sessionName, callback); } - function handleDeleteSession(sessionId, callback) { - const session = spacesService.getSessionBySessionId(sessionId); + async function handleDeleteSession(sessionId, callback) { + const session = await spacesService.getSessionBySessionId(sessionId); if (!session) { console.error(`handleDeleteSession: No session found with id ${sessionId}`); callback(false); @@ -962,8 +961,8 @@ chrome.runtime.onInstalled.addListener(details => { spacesService.deleteSession(sessionId, callback); } - function handleAddLinkToNewSession(url, sessionName, callback) { - const session = spacesService.getSessionByName(sessionName); + async function handleAddLinkToNewSession(url, sessionName, callback) { + const session = await spacesService.getSessionByName(sessionName); const newTabs = [{ url }]; // if we found a session matching this name then return as an error as we are @@ -978,8 +977,8 @@ chrome.runtime.onInstalled.addListener(details => { } function handleMoveTabToNewSession(tabId, sessionName, callback) { - requestTabDetail(tabId, tab => { - const session = spacesService.getSessionByName(sessionName); + requestTabDetail(tabId, async tab => { + const session = await spacesService.getSessionByName(sessionName); // if we found a session matching this name then return as an error as we are // supposed to be creating a new session with this name @@ -1002,8 +1001,8 @@ chrome.runtime.onInstalled.addListener(details => { }); } - function handleAddLinkToSession(url, sessionId, callback) { - const session = spacesService.getSessionBySessionId(sessionId); + async function handleAddLinkToSession(url, sessionId, callback) { + const session = await spacesService.getSessionBySessionId(sessionId); const newTabs = [{ url }]; // if we have not found a session matching this name then return as an error as we are @@ -1035,8 +1034,8 @@ chrome.runtime.onInstalled.addListener(details => { } function handleMoveTabToSession(tabId, sessionId, callback) { - requestTabDetail(tabId, tab => { - const session = spacesService.getSessionBySessionId(sessionId); + requestTabDetail(tabId, async tab => { + const session = await spacesService.getSessionBySessionId(sessionId); const newTabs = [tab]; // if we have not found a session matching this name then return as an error as we are @@ -1082,5 +1081,6 @@ function moveTabToWindow(tab, windowId, callback) { callback(true); } +console.log(`Initializing spacesService...`); spacesService.initialiseSpaces(); spacesService.initialiseTabHistory(); diff --git a/js/background/spacesService.js b/js/background/spacesService.js index ee4107b..57ea492 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -16,22 +16,20 @@ export var spacesService = { sessionUpdateTimers: {}, historyQueue: [], eventQueueCount: 0, - lastVersion: 0, - debug: false, + debug: true, noop: () => {}, // initialise spaces - combine open windows with saved sessions async initialiseSpaces() { // update version numbers - spacesService.lastVersion = await spacesService.fetchLastVersion(); + const lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); dbService.fetchAllSessions(sessions => { if ( chrome.runtime.getManifest().version === '0.18' && - chrome.runtime.getManifest().version !== - spacesService.lastVersion + chrome.runtime.getManifest().version !== lastVersion ) { spacesService.resetAllSessionHashes(sessions); } @@ -69,7 +67,7 @@ export var spacesService = { // record each tab's id and url so we can add history items when tabs are removed initialiseTabHistory() { chrome.tabs.query({}, tabs => { - tabs.forEach(tab => { + tabs.forEach(async tab => { spacesService.tabHistoryUrlMap[tab.id] = tab.url; }); }); @@ -151,16 +149,16 @@ export var spacesService = { return false; }, - checkForSessionMatch(curWindow) { + async checkForSessionMatch(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; } const sessionHash = spacesService.generateSessionHash(curWindow.tabs); - const temporarySession = spacesService.getSessionByWindowId( + const temporarySession = await spacesService.getSessionByWindowId( curWindow.id ); - const matchingSession = spacesService.getSessionBySessionHash( + const matchingSession = await spacesService.getSessionBySessionHash( sessionHash, true ); @@ -188,7 +186,7 @@ export var spacesService = { } }, - matchSessionToWindow(session, curWindow) { + async matchSessionToWindow(session, curWindow) { // remove any other sessions tied to this windowId (temporary sessions) for (let i = spacesService.sessions.length - 1; i >= 0; i -= 1) { if (spacesService.sessions[i].windowId === curWindow.id) { @@ -205,14 +203,14 @@ export var spacesService = { session.windowId = curWindow.id; }, - createTemporaryUnmatchedSession(curWindow) { + async createTemporaryUnmatchedSession(curWindow) { if (spacesService.debug) { // eslint-disable-next-line no-console console.dir(spacesService.sessions); // eslint-disable-next-line no-console console.dir(curWindow); // eslint-disable-next-line no-alert - alert('couldnt match window. creating temporary session'); + // alert('couldnt match window. creating temporary session'); } const sessionHash = spacesService.generateSessionHash(curWindow.tabs); @@ -246,7 +244,7 @@ export var spacesService = { // (events are received and screened first in background.js) // ----------------------------------------------------------------------------------------- - handleTabRemoved(tabId, removeInfo, callback) { + async handleTabRemoved(tabId, removeInfo, callback) { if (spacesService.debug) // eslint-disable-next-line no-console console.log( @@ -298,7 +296,7 @@ export var spacesService = { ); }, - handleTabUpdated(tab, changeInfo, callback) { + async handleTabUpdated(tab, changeInfo, callback) { // NOTE: only queue event when tab has completed loading (title property exists at this point) if (tab.status === 'complete') { if (spacesService.debug) { @@ -328,7 +326,7 @@ export var spacesService = { } }, - handleWindowRemoved(windowId, markAsClosed, callback) { + async handleWindowRemoved(windowId, markAsClosed, callback) { // ignore subsequent windowRemoved events for the same windowId (each closing tab will try to call this) if (spacesService.closedWindowIds[windowId]) { callback(); @@ -348,7 +346,7 @@ export var spacesService = { clearTimeout(spacesService.sessionUpdateTimers[windowId]); } - const session = spacesService.getSessionByWindowId(windowId); + const session = await spacesService.getSessionByWindowId(windowId); if (session) { // if this is a saved session then just remove the windowId reference if (session.id) { @@ -369,7 +367,7 @@ export var spacesService = { callback(); }, - handleWindowFocussed(windowId) { + async handleWindowFocussed(windowId) { if (spacesService.debug) // eslint-disable-next-line no-console console.log(`handlingWindowFocussed event. windowId: ${windowId}`); @@ -378,7 +376,7 @@ export var spacesService = { return; } - const session = spacesService.getSessionByWindowId(windowId); + const session = await spacesService.getSessionByWindowId(windowId); if (session) { session.lastAccess = new Date(); } @@ -423,7 +421,7 @@ export var spacesService = { return; } - chrome.windows.get(windowId, { populate: true }, curWindow => { + chrome.windows.get(windowId, { populate: true }, async curWindow => { if (chrome.runtime.lastError) { // eslint-disable-next-line no-console console.log( @@ -456,7 +454,7 @@ export var spacesService = { } // if window is associated with an open session then update session - const session = spacesService.getSessionByWindowId(windowId); + const session = await spacesService.getSessionByWindowId(windowId); if (session) { if (spacesService.debug) @@ -521,21 +519,21 @@ export var spacesService = { // PUBLIC FUNCTIONS - getSessionBySessionId(sessionId) { + async getSessionBySessionId(sessionId) { const result = spacesService.sessions.filter(session => { return session.id === sessionId; }); return result.length === 1 ? result[0] : false; }, - getSessionByWindowId(windowId) { + async getSessionByWindowId(windowId) { const result = spacesService.sessions.filter(session => { return session.windowId === windowId; }); return result.length === 1 ? result[0] : false; }, - getSessionBySessionHash(hash, closedOnly) { + async getSessionBySessionHash(hash, closedOnly) { const result = spacesService.sessions.filter(session => { if (closedOnly) { return session.sessionHash === hash && !session.windowId; @@ -545,7 +543,7 @@ export var spacesService = { return result.length >= 1 ? result[0] : false; }, - getSessionByName(name) { + async getSessionByName(name) { const result = spacesService.sessions.filter(session => { if (!name) { console.log(`name was undefined`) @@ -558,7 +556,7 @@ export var spacesService = { return result.length >= 1 ? result[0] : false; }, - getAllSessions() { + async getAllSessions() { return spacesService.sessions; }, @@ -632,8 +630,8 @@ export var spacesService = { // Database actions - updateSessionTabs(sessionId, tabs, callback) { - const session = spacesService.getSessionBySessionId(sessionId); + async updateSessionTabs(sessionId, tabs, callback) { + const session = await spacesService.getSessionBySessionId(sessionId); // eslint-disable-next-line no-param-reassign callback = @@ -646,19 +644,19 @@ export var spacesService = { spacesService.saveExistingSession(session.id, callback); }, - updateSessionName(sessionId, sessionName, callback) { + async updateSessionName(sessionId, sessionName, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; - const session = spacesService.getSessionBySessionId(sessionId); + const session = await spacesService.getSessionBySessionId(sessionId); session.name = sessionName; spacesService.saveExistingSession(session.id, callback); }, - saveExistingSession(sessionId, callback) { - const session = spacesService.getSessionBySessionId(sessionId); + async saveExistingSession(sessionId, callback) { + const session = await spacesService.getSessionBySessionId(sessionId); // eslint-disable-next-line no-param-reassign callback = @@ -667,7 +665,7 @@ export var spacesService = { dbService.updateSession(session, callback); }, - saveNewSession(sessionName, tabs, windowId, callback) { + async saveNewSession(sessionName, tabs, windowId, callback) { if (!tabs) { callback(); return; @@ -682,7 +680,7 @@ export var spacesService = { // check for a temporary session with this windowId if (windowId) { - session = spacesService.getSessionByWindowId(windowId); + session = await spacesService.getSessionByWindowId(windowId); } // if no temporary session found with this windowId, then create one @@ -714,7 +712,7 @@ export var spacesService = { callback = typeof callback !== 'function' ? spacesService.noop : callback; - dbService.removeSession(sessionId, () => { + dbService.removeSession(sessionId, async () => { // remove session from cached array spacesService.sessions.some((session, index) => { if (session.id === sessionId) { From 3623cc085f8c036ceae61bf9b772c8726037f469 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 11 Aug 2025 23:27:25 -0700 Subject: [PATCH 09/92] Add {} around debug lines --- js/background/spacesService.js | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 57ea492..06f8f0b 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -22,6 +22,7 @@ export var spacesService = { // initialise spaces - combine open windows with saved sessions async initialiseSpaces() { + console.log(`Inside spacesService.initialiseSpaces()`); // update version numbers const lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); @@ -164,22 +165,24 @@ export var spacesService = { ); if (matchingSession) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `matching session found: ${matchingSession.id}. linking with window: ${curWindow.id}` ); + } spacesService.matchSessionToWindow(matchingSession, curWindow); } // if no match found and this window does not already have a temporary session if (!matchingSession && !temporarySession) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `no matching session found. creating temporary session for window: ${curWindow.id}` ); + } // create a new temporary session for this window (with no sessionId or name) spacesService.createTemporaryUnmatchedSession(curWindow); @@ -245,11 +248,12 @@ export var spacesService = { // ----------------------------------------------------------------------------------------- async handleTabRemoved(tabId, removeInfo, callback) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `handlingTabRemoved event. windowId: ${removeInfo.windowId}` ); + } // NOTE: isWindowClosing is true if the window cross was clicked causing the tab to be removed. // If the tab cross is clicked and it is the last tab in the window @@ -284,11 +288,12 @@ export var spacesService = { }, handleTabMoved(tabId, moveInfo, callback) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `handlingTabMoved event. windowId: ${moveInfo.windowId}` ); + } spacesService.queueWindowEvent( moveInfo.windowId, spacesService.eventQueueCount, @@ -332,16 +337,19 @@ export var spacesService = { callback(); } - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`handlingWindowRemoved event. windowId: ${windowId}`); + } // add windowId to closedWindowIds. the idea is that once a window is closed it can never be // rematched to a new session (hopefully these window ids never get legitimately re-used) if (markAsClosed) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`adding window to closedWindowIds: ${windowId}`); + } + spacesService.closedWindowIds[windowId] = true; clearTimeout(spacesService.sessionUpdateTimers[windowId]); } @@ -368,9 +376,10 @@ export var spacesService = { }, async handleWindowFocussed(windowId) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`handlingWindowFocussed event. windowId: ${windowId}`); + } if (windowId <= 0) { return; @@ -402,22 +411,24 @@ export var spacesService = { callback = typeof callback !== 'function' ? spacesService.noop : callback; - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log('------------------------------------------------'); - if (spacesService.debug) + // eslint-disable-next-line no-console console.log( `event: ${eventId}. attempting session update. windowId: ${windowId}` ); + } // sanity check windowId if (!windowId || windowId <= 0) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `received an event for windowId: ${windowId} which is obviously wrong` ); + } return; } @@ -445,11 +456,12 @@ export var spacesService = { // don't allow event if it pertains to a closed window id if (spacesService.closedWindowIds[windowId]) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `ignoring event as it pertains to a closed windowId: ${windowId}` ); + } return; } @@ -457,7 +469,7 @@ export var spacesService = { const session = await spacesService.getSessionByWindowId(windowId); if (session) { - if (spacesService.debug) + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( `tab statuses: ${curWindow.tabs @@ -466,6 +478,7 @@ export var spacesService = { }) .join('|')}` ); + } // look for tabs recently added/removed from this session and update session history const historyItems = spacesService.historyQueue.filter( From 4659a64e2edc30d23ae98218e644d243a04e7014 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 18:36:43 -0700 Subject: [PATCH 10/92] Update dbService methods to use modern syntax (remove arrow functions) --- js/background/dbService.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index f2daa5e..624e95f 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -53,7 +53,7 @@ export var dbService = { }); }, - _fetchSessionById: id => { + _fetchSessionById(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; return dbService.getDb().then(s => { return s @@ -68,7 +68,7 @@ export var dbService = { }); }, - fetchAllSessions: callback => { + fetchAllSessions(callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; dbService._fetchAllSessions().then(sessions => { @@ -76,7 +76,7 @@ export var dbService = { }); }, - fetchSessionById: (id, callback) => { + fetchSessionById(id, callback) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -85,7 +85,7 @@ export var dbService = { }); }, - fetchSessionNames: callback => { + fetchSessionNames(callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -98,7 +98,7 @@ export var dbService = { }); }, - fetchSessionByName: (sessionName, callback) => { + fetchSessionByName(sessionName, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -120,7 +120,7 @@ export var dbService = { }); }, - createSession: (session, callback) => { + createSession(session, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -139,7 +139,7 @@ export var dbService = { }); }, - updateSession: (session, callback) => { + updateSession(session, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -161,7 +161,7 @@ export var dbService = { }); }, - removeSession: (id, callback) => { + removeSession(id, callback) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; const _callback = typeof callback !== 'function' ? dbService.noop : callback; From 0cd744e9171fcba224010dfef738f0792d7ffc9b Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 18:41:38 -0700 Subject: [PATCH 11/92] Add jsdoc for dbService methods. --- js/background/dbService.js | 49 +++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 624e95f..3685968 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -11,7 +11,8 @@ export var dbService = { noop() {}, /** - * INDEXEDDB FUNCTIONS + * Opens and returns a database connection + * @returns {Promise} Promise that resolves to database connection */ getDb() { return db.open({ @@ -30,6 +31,10 @@ export var dbService = { * session.history: an array of chrome tab objects that have been removed from the session * session.lastAccess: timestamp that gets updated with every window focus */ + /** + * Returns database schema definition + * @returns {Object} Database schema configuration object + */ getSchema() { return { ttSessions: { @@ -44,6 +49,10 @@ export var dbService = { }; }, + /** + * Fetches all sessions from the database + * @returns {Promise} Promise that resolves to array of session objects + */ _fetchAllSessions() { return dbService.getDb().then(s => { return s @@ -53,6 +62,11 @@ export var dbService = { }); }, + /** + * Fetches a session by ID from the database + * @param {string} id - The session ID to fetch + * @returns {Promise} Promise that resolves to session object or null if not found + */ _fetchSessionById(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; return dbService.getDb().then(s => { @@ -68,6 +82,10 @@ export var dbService = { }); }, + /** + * Fetches all sessions from the database and calls callback with results + * @param {Function} callback - Callback function that receives array of sessions + */ fetchAllSessions(callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -76,6 +94,11 @@ export var dbService = { }); }, + /** + * Fetches a session by ID and calls callback with result + * @param {string} id - The session ID to fetch + * @param {Function} callback - Callback function that receives session object or null + */ fetchSessionById(id, callback) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; const _callback = @@ -85,6 +108,10 @@ export var dbService = { }); }, + /** + * Fetches all session names and calls callback with results + * @param {Function} callback - Callback function that receives array of session names + */ fetchSessionNames(callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -98,6 +125,11 @@ export var dbService = { }); }, + /** + * Fetches a session by name and calls callback with result + * @param {string} sessionName - The session name to search for + * @param {Function} callback - Callback function that receives session object or false if not found + */ fetchSessionByName(sessionName, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -120,6 +152,11 @@ export var dbService = { }); }, + /** + * Creates a new session in the database + * @param {Object} session - The session object to create (id will be auto-generated) + * @param {Function} callback - Callback function that receives the created session with ID + */ createSession(session, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -139,6 +176,11 @@ export var dbService = { }); }, + /** + * Updates an existing session in the database + * @param {Object} session - The session object to update (must have valid id) + * @param {Function} callback - Callback function that receives the updated session or false if failed + */ updateSession(session, callback) { const _callback = typeof callback !== 'function' ? dbService.noop : callback; @@ -161,6 +203,11 @@ export var dbService = { }); }, + /** + * Removes a session from the database + * @param {string} id - The session ID to remove + * @param {Function} callback - Callback function called when removal is complete + */ removeSession(id, callback) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; const _callback = From f147dbaf7cb08f904bd2e7cee840eb111888159d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 18:50:23 -0700 Subject: [PATCH 12/92] Convert dbService fetchAllSessions() to async and update caller --- js/background/dbService.js | 38 ++++++++++++++++++---------------- js/background/spacesService.js | 8 +++++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 3685968..0cc1712 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -11,7 +11,7 @@ export var dbService = { noop() {}, /** - * Opens and returns a database connection + * Opens and returns a database connection. * @returns {Promise} Promise that resolves to database connection */ getDb() { @@ -32,7 +32,7 @@ export var dbService = { * session.lastAccess: timestamp that gets updated with every window focus */ /** - * Returns database schema definition + * Returns database schema definition. * @returns {Object} Database schema configuration object */ getSchema() { @@ -50,7 +50,7 @@ export var dbService = { }, /** - * Fetches all sessions from the database + * Fetches all sessions from the database. * @returns {Promise} Promise that resolves to array of session objects */ _fetchAllSessions() { @@ -63,7 +63,7 @@ export var dbService = { }, /** - * Fetches a session by ID from the database + * Fetches a session by ID from the database. * @param {string} id - The session ID to fetch * @returns {Promise} Promise that resolves to session object or null if not found */ @@ -83,19 +83,21 @@ export var dbService = { }, /** - * Fetches all sessions from the database and calls callback with results - * @param {Function} callback - Callback function that receives array of sessions + * Fetches all sessions from the database. + * @returns {Promise} Promise that resolves to array of session objects */ - fetchAllSessions(callback) { - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - dbService._fetchAllSessions().then(sessions => { - _callback(sessions); - }); + async fetchAllSessions() { + try { + const sessions = await dbService._fetchAllSessions(); + return sessions; + } catch (error) { + console.error('Error fetching all sessions:', error); + return []; + } }, /** - * Fetches a session by ID and calls callback with result + * Fetches a session by ID and calls callback with result. * @param {string} id - The session ID to fetch * @param {Function} callback - Callback function that receives session object or null */ @@ -109,7 +111,7 @@ export var dbService = { }, /** - * Fetches all session names and calls callback with results + * Fetches all session names and calls callback with results. * @param {Function} callback - Callback function that receives array of session names */ fetchSessionNames(callback) { @@ -126,7 +128,7 @@ export var dbService = { }, /** - * Fetches a session by name and calls callback with result + * Fetches a session by name and calls callback with result. * @param {string} sessionName - The session name to search for * @param {Function} callback - Callback function that receives session object or false if not found */ @@ -153,7 +155,7 @@ export var dbService = { }, /** - * Creates a new session in the database + * Creates a new session in the database. * @param {Object} session - The session object to create (id will be auto-generated) * @param {Function} callback - Callback function that receives the created session with ID */ @@ -177,7 +179,7 @@ export var dbService = { }, /** - * Updates an existing session in the database + * Updates an existing session in the database. * @param {Object} session - The session object to update (must have valid id) * @param {Function} callback - Callback function that receives the updated session or false if failed */ @@ -204,7 +206,7 @@ export var dbService = { }, /** - * Removes a session from the database + * Removes a session from the database. * @param {string} id - The session ID to remove * @param {Function} callback - Callback function called when removal is complete */ diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 06f8f0b..198669d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -27,7 +27,9 @@ export var spacesService = { const lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); - dbService.fetchAllSessions(sessions => { + try { + const sessions = await dbService.fetchAllSessions(); + if ( chrome.runtime.getManifest().version === '0.18' && chrome.runtime.getManifest().version !== lastVersion @@ -52,7 +54,9 @@ export var spacesService = { } }); }); - }); + } catch (error) { + console.error('Error initializing spaces:', error); + } }, resetAllSessionHashes(sessions) { From 1bba3aab0000484445b0544cbe1f3c33bb8e84ab Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 18:56:45 -0700 Subject: [PATCH 13/92] Change dbService updateSession() to async code. --- js/background/dbService.js | 28 +++++++++++----------------- js/background/spacesService.js | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 0cc1712..7da903b 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -181,28 +181,22 @@ export var dbService = { /** * Updates an existing session in the database. * @param {Object} session - The session object to update (must have valid id) - * @param {Function} callback - Callback function that receives the updated session or false if failed + * @returns {Promise} Promise that resolves to updated session or null if failed */ - updateSession(session, callback) { - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - + async updateSession(session) { // ensure session id is set if (!session.id) { - _callback(false); - return; + return null; } - dbService - .getDb() - .then(s => { - return s.update(dbService.DB_SESSIONS, session); - }) - .then(result => { - if (result.length > 0) { - _callback(result[0]); - } - }); + try { + const s = await dbService.getDb(); + const result = await s.update(dbService.DB_SESSIONS, session); + return result.length > 0 ? result[0] : null; + } catch (error) { + console.error('Error updating session:', error); + return null; + } }, /** diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 198669d..c541272 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -34,7 +34,7 @@ export var spacesService = { chrome.runtime.getManifest().version === '0.18' && chrome.runtime.getManifest().version !== lastVersion ) { - spacesService.resetAllSessionHashes(sessions); + await spacesService.resetAllSessionHashes(sessions); } chrome.windows.getAll({ populate: true }, windows => { @@ -59,14 +59,14 @@ export var spacesService = { } }, - resetAllSessionHashes(sessions) { - sessions.forEach(session => { + async resetAllSessionHashes(sessions) { + for (const session of sessions) { // eslint-disable-next-line no-param-reassign session.sessionHash = spacesService.generateSessionHash( session.tabs ); - dbService.updateSession(session); - }); + await dbService.updateSession(session); + } }, // record each tab's id and url so we can add history items when tabs are removed @@ -679,7 +679,13 @@ export var spacesService = { callback = typeof callback !== 'function' ? spacesService.noop : callback; - dbService.updateSession(session, callback); + try { + const updatedSession = await dbService.updateSession(session); + callback(updatedSession); + } catch (error) { + console.error('Error saving existing session:', error); + callback(null); + } }, async saveNewSession(sessionName, tabs, windowId, callback) { From 07ed0c40dac732abdba6b1ff8dbb0238e85b236d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 19:02:51 -0700 Subject: [PATCH 14/92] Make dbService createSession() async --- js/background/dbService.js | 25 ++++++++++--------------- js/background/spacesService.js | 20 ++++++++++++++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 7da903b..d160884 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -157,25 +157,20 @@ export var dbService = { /** * Creates a new session in the database. * @param {Object} session - The session object to create (id will be auto-generated) - * @param {Function} callback - Callback function that receives the created session with ID + * @returns {Promise} Promise that resolves to created session with ID or null if failed */ - createSession(session, callback) { - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - + async createSession(session) { // delete session id in case it already exists const { id, ..._session } = session; - dbService - .getDb() - .then(s => { - return s.add(dbService.DB_SESSIONS, _session); - }) - .then(result => { - if (result.length > 0) { - _callback(result[0]); - } - }); + try { + const s = await dbService.getDb(); + const result = await s.add(dbService.DB_SESSIONS, _session); + return result.length > 0 ? result[0] : null; + } catch (error) { + console.error('Error creating session:', error); + return null; + } }, /** diff --git a/js/background/spacesService.js b/js/background/spacesService.js index c541272..d81ef24 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -722,12 +722,20 @@ export var spacesService = { session.lastAccess = new Date(); // save session to db - dbService.createSession(session, savedSession => { - // update sessionId in cache - session.id = savedSession.id; - - callback(savedSession); - }); + try { + const savedSession = await dbService.createSession(session); + if (savedSession) { + // update sessionId in cache + session.id = savedSession.id; + callback(savedSession); + } else { + console.error('Failed to create session'); + callback(false); + } + } catch (error) { + console.error('Error creating session:', error); + callback(false); + } }, deleteSession(sessionId, callback) { From 26ca836eb8d1d35d3a4184b050aef52968c4d668 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 20:23:31 -0700 Subject: [PATCH 15/92] Convert dbService removeSession to async --- js/background/dbService.js | 20 ++++++++++---------- js/background/spacesService.js | 30 ++++++++++++++++++------------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index d160884..802cdf9 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -197,18 +197,18 @@ export var dbService = { /** * Removes a session from the database. * @param {string} id - The session ID to remove - * @param {Function} callback - Callback function called when removal is complete + * @returns {Promise} Promise that resolves to true if successful, false if failed */ - removeSession(id, callback) { + async removeSession(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - dbService - .getDb() - .then(s => { - return s.remove(dbService.DB_SESSIONS, _id); - }) - .then(_callback); + try { + const s = await dbService.getDb(); + await s.remove(dbService.DB_SESSIONS, _id); + return true; + } catch (error) { + console.error('Error removing session:', error); + return false; + } }, }; diff --git a/js/background/spacesService.js b/js/background/spacesService.js index d81ef24..283ab38 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -738,21 +738,27 @@ export var spacesService = { } }, - deleteSession(sessionId, callback) { + async deleteSession(sessionId, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; - dbService.removeSession(sessionId, async () => { - // remove session from cached array - spacesService.sessions.some((session, index) => { - if (session.id === sessionId) { - spacesService.sessions.splice(index, 1); - return true; - } - return false; - }); - callback(); - }); + try { + const success = await dbService.removeSession(sessionId); + if (success) { + // remove session from cached array + spacesService.sessions.some((session, index) => { + if (session.id === sessionId) { + spacesService.sessions.splice(index, 1); + return true; + } + return false; + }); + } + callback(success); + } catch (error) { + console.error('Error deleting session:', error); + callback(false); + } }, }; From d9635aba273b6cae296633b18ad96c1c7cef016e Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Aug 2025 20:54:30 -0700 Subject: [PATCH 16/92] Update remaining (unused) functions in dbService to async --- js/background/dbService.js | 73 ++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 802cdf9..9e81294 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -64,15 +64,14 @@ export var dbService = { /** * Fetches a session by ID from the database. - * @param {string} id - The session ID to fetch + * @param {number} id - The session ID to fetch * @returns {Promise} Promise that resolves to session object or null if not found */ _fetchSessionById(id) { - const _id = typeof id === 'string' ? parseInt(id, 10) : id; return dbService.getDb().then(s => { return s .query(dbService.DB_SESSIONS, 'id') - .only(_id) + .only(id) .distinct() .desc() .execute() @@ -97,46 +96,43 @@ export var dbService = { }, /** - * Fetches a session by ID and calls callback with result. - * @param {string} id - The session ID to fetch - * @param {Function} callback - Callback function that receives session object or null + * Fetches a session by ID. + * @param {string|number} id - The session ID to fetch + * @returns {Promise} Promise that resolves to session object or null if not found */ - fetchSessionById(id, callback) { + async fetchSessionById(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - dbService._fetchSessionById(_id).then(session => { - _callback(session); - }); + try { + const session = await dbService._fetchSessionById(_id); + return session; + } catch (error) { + console.error('Error fetching session by ID:', error); + return null; + } }, /** - * Fetches all session names and calls callback with results. - * @param {Function} callback - Callback function that receives array of session names + * Fetches all session names. + * @returns {Promise>} Promise that resolves to array of session names */ - fetchSessionNames(callback) { - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - - dbService._fetchAllSessions().then(sessions => { - _callback( - sessions.map(session => { - return session.name; - }) - ); - }); + async fetchSessionNames() { + try { + const sessions = await dbService._fetchAllSessions(); + return sessions.map(session => session.name); + } catch (error) { + console.error('Error fetching session names:', error); + return []; + } }, /** - * Fetches a session by name and calls callback with result. + * Fetches a session by name. * @param {string} sessionName - The session name to search for - * @param {Function} callback - Callback function that receives session object or false if not found + * @returns {Promise} Promise that resolves to session object or false if not found */ - fetchSessionByName(sessionName, callback) { - const _callback = - typeof callback !== 'function' ? dbService.noop : callback; - - dbService._fetchAllSessions().then(sessions => { + async fetchSessionByName(sessionName) { + try { + const sessions = await dbService._fetchAllSessions(); let matchIndex; const matchFound = sessions.some((session, index) => { if (session.name.toLowerCase() === sessionName.toLowerCase()) { @@ -146,12 +142,11 @@ export var dbService = { return false; }); - if (matchFound) { - _callback(sessions[matchIndex]); - } else { - _callback(false); - } - }); + return matchFound ? sessions[matchIndex] : false; + } catch (error) { + console.error('Error fetching session by name:', error); + return false; + } }, /** @@ -196,7 +191,7 @@ export var dbService = { /** * Removes a session from the database. - * @param {string} id - The session ID to remove + * @param {string|number} id - The session ID to remove * @returns {Promise} Promise that resolves to true if successful, false if failed */ async removeSession(id) { From adde6d07ce16504e9c35d82ad6d71535bd853318 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Thu, 14 Aug 2025 07:10:43 -0700 Subject: [PATCH 17/92] Make the db the source of truth for sessions. Ensure any changes to in-memory sessions in spacesService are persisted to the database. --- js/background/background.js | 31 +++++------ js/background/dbService.js | 16 ++++++ js/background/spacesService.js | 98 ++++++++++++++-------------------- 3 files changed, 71 insertions(+), 74 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 604110f..fc53e0f 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -6,6 +6,7 @@ * Copyright (C) 2015 Dean Oemcke */ +import { dbService } from './dbService.js'; import { spacesService } from './spacesService.js'; /** @typedef {import('./common.js').SessionPresence} SessionPresence */ @@ -518,7 +519,7 @@ chrome.runtime.onInstalled.addListener(details => { return ''; } - const session = await spacesService.getSessionByWindowId(activeTab.windowId); + const session = await dbService.fetchSessionByWindowId(activeTab.windowId); const name = session ? session.name : ''; @@ -659,7 +660,7 @@ chrome.runtime.onInstalled.addListener(details => { * @returns {SessionPresence} */ async function requestSessionPresence(sessionName) { - const session = await spacesService.getSessionByName(sessionName); + const session = await dbService.fetchSessionByName(sessionName); return { exists: !!session, isOpen: !!session && !!session.windowId }; } @@ -678,7 +679,7 @@ async function requestSessionPresence(sessionName) { // except that includes space.sessionId (session.id) and space.windowId async function requestSpaceFromWindowId(windowId, callback) { // first check for an existing session matching this windowId - const session = await spacesService.getSessionByWindowId(windowId); + const session = await dbService.fetchSessionByWindowId(windowId); if (session) { console.log(`codedread: requestSpaceFromWindowId() found session: ${session.id} for windowId: ${windowId}`); @@ -716,7 +717,7 @@ async function requestSessionPresence(sessionName) { } async function requestSpaceFromSessionId(sessionId, callback) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); callback({ sessionId: session.id, @@ -728,7 +729,7 @@ async function requestSessionPresence(sessionName) { } async function requestAllSpaces(callback) { - const sessions = await spacesService.getAllSessions(); + const sessions = await dbService.fetchAllSessions(); const allSpaces = sessions .map(session => { return { sessionId: session.id, ...session }; @@ -762,7 +763,7 @@ async function requestSessionPresence(sessionName) { } async function handleLoadSession(sessionId, tabUrl) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); // if space is already open, then give it focus if (session.windowId) { @@ -861,7 +862,7 @@ async function requestSessionPresence(sessionName) { function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { chrome.windows.get(windowId, { populate: true }, async curWindow => { - const existingSession = await spacesService.getSessionByName(sessionName); + const existingSession = await dbService.fetchSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session if (existingSession) { @@ -887,7 +888,7 @@ async function requestSessionPresence(sessionName) { async function handleRestoreFromBackup(space, deleteOld, callback) { const existingSession = space.name - ? await spacesService.getSessionByName(space.name) + ? await dbService.fetchSessionByName(space.name) : false; // if session with same name already exist, then prompt to override the existing session @@ -916,7 +917,7 @@ async function requestSessionPresence(sessionName) { let tempName = 'Imported space: '; let count = 1; - while (await spacesService.getSessionByName(tempName + count)) { + while (await dbService.fetchSessionByName(tempName + count)) { count += 1; } @@ -932,7 +933,7 @@ async function requestSessionPresence(sessionName) { async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { // check to make sure session name doesn't already exist - const existingSession = await spacesService.getSessionByName(sessionName); + const existingSession = await dbService.fetchSessionByName(sessionName); // if session with same name already exist, then prompt to override the existing session if (existingSession) { @@ -951,7 +952,7 @@ async function requestSessionPresence(sessionName) { } async function handleDeleteSession(sessionId, callback) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); if (!session) { console.error(`handleDeleteSession: No session found with id ${sessionId}`); callback(false); @@ -962,7 +963,7 @@ async function requestSessionPresence(sessionName) { } async function handleAddLinkToNewSession(url, sessionName, callback) { - const session = await spacesService.getSessionByName(sessionName); + const session = await dbService.fetchSessionByName(sessionName); const newTabs = [{ url }]; // if we found a session matching this name then return as an error as we are @@ -978,7 +979,7 @@ async function requestSessionPresence(sessionName) { function handleMoveTabToNewSession(tabId, sessionName, callback) { requestTabDetail(tabId, async tab => { - const session = await spacesService.getSessionByName(sessionName); + const session = await dbService.fetchSessionByName(sessionName); // if we found a session matching this name then return as an error as we are // supposed to be creating a new session with this name @@ -1002,7 +1003,7 @@ async function requestSessionPresence(sessionName) { } async function handleAddLinkToSession(url, sessionId, callback) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); const newTabs = [{ url }]; // if we have not found a session matching this name then return as an error as we are @@ -1035,7 +1036,7 @@ async function requestSessionPresence(sessionName) { function handleMoveTabToSession(tabId, sessionId, callback) { requestTabDetail(tabId, async tab => { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); const newTabs = [tab]; // if we have not found a session matching this name then return as an error as we are diff --git a/js/background/dbService.js b/js/background/dbService.js index 9e81294..8fde79b 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -125,6 +125,22 @@ export var dbService = { } }, + /** + * Fetches a session by window ID. + * @param {number} windowId - The window ID to search for + * @returns {Promise} Promise that resolves to session object or false if not found + */ + async fetchSessionByWindowId(windowId) { + try { + const sessions = await dbService._fetchAllSessions(); + const matchedSession = sessions.find(session => session.windowId === windowId); + return matchedSession || false; + } catch (error) { + console.error('Error fetching session by window ID:', error); + return false; + } + }, + /** * Fetches a session by name. * @param {string} sessionName - The session name to search for diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 283ab38..feba6ca 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -37,15 +37,18 @@ export var spacesService = { await spacesService.resetAllSessionHashes(sessions); } - chrome.windows.getAll({ populate: true }, windows => { + chrome.windows.getAll({ populate: true }, async windows => { // populate session map from database spacesService.sessions = sessions; - // clear any previously saved windowIds - spacesService.sessions.forEach(session => { - // eslint-disable-next-line no-param-reassign - session.windowId = false; - }); + // clear any previously saved windowIds both in memory and database + for (const session of spacesService.sessions) { + if (session.windowId) { + session.windowId = false; + // Persist the cleared windowId to database + await dbService.updateSession(session); + } + } // then try to match current open windows with saved sessions windows.forEach(curWindow => { @@ -160,13 +163,22 @@ export var spacesService = { } const sessionHash = spacesService.generateSessionHash(curWindow.tabs); - const temporarySession = await spacesService.getSessionByWindowId( + const temporarySession = await dbService.fetchSessionByWindowId( curWindow.id ); - const matchingSession = await spacesService.getSessionBySessionHash( - sessionHash, - true - ); + + // Find matching session by hash (closedOnly = true) + let matchingSession = false; + try { + const sessions = await dbService.fetchAllSessions(); + const matchedSession = sessions.find(session => { + return session.sessionHash === sessionHash && !session.windowId; + }); + matchingSession = matchedSession || false; + } catch (error) { + console.error('Error fetching session by hash:', error); + matchingSession = false; + } if (matchingSession) { if (spacesService.debug) { @@ -199,6 +211,8 @@ export var spacesService = { if (spacesService.sessions[i].windowId === curWindow.id) { if (spacesService.sessions[i].id) { spacesService.sessions[i].windowId = false; + // Persist the cleared windowId to database + await dbService.updateSession(spacesService.sessions[i]); } else { spacesService.sessions.splice(i, 1); } @@ -208,6 +222,11 @@ export var spacesService = { // assign windowId to newly matched session // eslint-disable-next-line no-param-reassign session.windowId = curWindow.id; + + // Persist the new windowId association to database + if (session.id) { + await dbService.updateSession(session); + } }, async createTemporaryUnmatchedSession(curWindow) { @@ -358,11 +377,13 @@ export var spacesService = { clearTimeout(spacesService.sessionUpdateTimers[windowId]); } - const session = await spacesService.getSessionByWindowId(windowId); + const session = await dbService.fetchSessionByWindowId(windowId); if (session) { // if this is a saved session then just remove the windowId reference if (session.id) { session.windowId = false; + // Persist the cleared windowId to database + await dbService.updateSession(session); // else if it is temporary session then remove the session from the cache } else { @@ -389,7 +410,7 @@ export var spacesService = { return; } - const session = await spacesService.getSessionByWindowId(windowId); + const session = await dbService.fetchSessionByWindowId(windowId); if (session) { session.lastAccess = new Date(); } @@ -470,7 +491,7 @@ export var spacesService = { } // if window is associated with an open session then update session - const session = await spacesService.getSessionByWindowId(windowId); + const session = await dbService.fetchSessionByWindowId(windowId); if (session) { if (spacesService.debug) { @@ -536,47 +557,6 @@ export var spacesService = { // PUBLIC FUNCTIONS - async getSessionBySessionId(sessionId) { - const result = spacesService.sessions.filter(session => { - return session.id === sessionId; - }); - return result.length === 1 ? result[0] : false; - }, - - async getSessionByWindowId(windowId) { - const result = spacesService.sessions.filter(session => { - return session.windowId === windowId; - }); - return result.length === 1 ? result[0] : false; - }, - - async getSessionBySessionHash(hash, closedOnly) { - const result = spacesService.sessions.filter(session => { - if (closedOnly) { - return session.sessionHash === hash && !session.windowId; - } - return session.sessionHash === hash; - }); - return result.length >= 1 ? result[0] : false; - }, - - async getSessionByName(name) { - const result = spacesService.sessions.filter(session => { - if (!name) { - console.log(`name was undefined`) - } - return ( - session.name && - session.name.toLowerCase() === name.toLowerCase() - ); - }); - return result.length >= 1 ? result[0] : false; - }, - - async getAllSessions() { - return spacesService.sessions; - }, - addUrlToSessionHistory(session, newUrl) { if (spacesService.debug) { // eslint-disable-next-line no-console @@ -648,7 +628,7 @@ export var spacesService = { // Database actions async updateSessionTabs(sessionId, tabs, callback) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); // eslint-disable-next-line no-param-reassign callback = @@ -666,14 +646,14 @@ export var spacesService = { callback = typeof callback !== 'function' ? spacesService.noop : callback; - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); session.name = sessionName; spacesService.saveExistingSession(session.id, callback); }, async saveExistingSession(sessionId, callback) { - const session = await spacesService.getSessionBySessionId(sessionId); + const session = await dbService.fetchSessionById(sessionId); // eslint-disable-next-line no-param-reassign callback = @@ -703,7 +683,7 @@ export var spacesService = { // check for a temporary session with this windowId if (windowId) { - session = await spacesService.getSessionByWindowId(windowId); + session = await dbService.fetchSessionByWindowId(windowId); } // if no temporary session found with this windowId, then create one From 5d4b10e0fbfc4c44578ae93d8927ed91db71a6f9 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 25 Aug 2025 21:27:30 -0700 Subject: [PATCH 18/92] Fix SpacesService async initialization issues. --- js/background/background.js | 43 ++++++++++++++++++++++++++++------ js/background/spacesService.js | 34 +++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index fc53e0f..8e85a24 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -45,10 +45,16 @@ chrome.runtime.onInstalled.addListener(details => { }); }); +// Handle Chrome startup - this is when window IDs get reassigned! +chrome.runtime.onStartup.addListener(async () => { + await spacesService.initialiseSpaces(); +}); + // LISTENERS // add listeners for session monitoring - chrome.tabs.onCreated.addListener(tab => { + chrome.tabs.onCreated.addListener(async (tab) => { + await spacesService.ensureInitialized(); // this call to checkInternalSpacesWindows actually returns false when it should return true // due to the event being called before the globalWindowIds get set. oh well, never mind. if (checkInternalSpacesWindows(tab.windowId, false)) return; @@ -56,26 +62,30 @@ chrome.runtime.onInstalled.addListener(details => { // spacesService.handleTabCreated(tab); updateSpacesWindow('tabs.onCreated'); }); - chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { + chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { + await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; spacesService.handleTabRemoved(tabId, removeInfo, () => { updateSpacesWindow('tabs.onRemoved'); }); }); - chrome.tabs.onMoved.addListener((tabId, moveInfo) => { + chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { + await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; spacesService.handleTabMoved(tabId, moveInfo, () => { updateSpacesWindow('tabs.onMoved'); }); }); - chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(tab.windowId, false)) return; spacesService.handleTabUpdated(tab, changeInfo, () => { updateSpacesWindow('tabs.onUpdated'); }); }); - chrome.windows.onRemoved.addListener(windowId => { + chrome.windows.onRemoved.addListener(async (windowId) => { + await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(windowId, true)) return; spacesService.handleWindowRemoved(windowId, true, () => { updateSpacesWindow('windows.onRemoved'); @@ -101,7 +111,7 @@ chrome.runtime.onInstalled.addListener(details => { // add listeners for tab and window focus changes // when a tab or window is changed, close the move tab popup if it is open - chrome.windows.onFocusChanged.addListener(windowId => { + chrome.windows.onFocusChanged.addListener(async (windowId) => { // Prevent a click in the popup on Ubunto or ChroneOS from closing the // popup prematurely. if ( @@ -116,6 +126,8 @@ chrome.runtime.onInstalled.addListener(details => { closePopupWindow(); } } + + await spacesService.ensureInitialized(); spacesService.handleWindowFocussed(windowId); }); @@ -127,6 +139,21 @@ chrome.runtime.onInstalled.addListener(details => { console.log(`listener fired: ${JSON.stringify(request)}`); } + // Ensure spacesService is initialized before processing any message + spacesService.ensureInitialized().then(() => { + const result = processMessage(request, sender, sendResponse); + // If processMessage returns false, we need to handle that by not sending a response + // But since we're in an async context, we can't change the outer return value + // The key is that processMessage will call sendResponse() for true cases + // and won't call it for false cases, which is the correct behavior + }); + + // We have to return true here because we're handling everything asynchronously + // The actual response sending is controlled by whether processMessage calls sendResponse() + return true; + }); + + function processMessage(request, sender, sendResponse) { let sessionId; let windowId; let tabId; @@ -140,6 +167,7 @@ chrome.runtime.onInstalled.addListener(details => { case 'requestSpaceFromWindowId': windowId = _cleanParameter(request.windowId); + console.log(`background.js: requestSpaceFromWindowId for request.windowId=${request.windowId}, cleaned window ID: ${windowId}`); if (windowId) { requestSpaceFromWindowId(windowId, sendResponse); } @@ -414,7 +442,8 @@ chrome.runtime.onInstalled.addListener(details => { default: return false; } - }); + } + function _cleanParameter(param) { if (typeof param === 'number') { return param; diff --git a/js/background/spacesService.js b/js/background/spacesService.js index feba6ca..5e3d532 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -17,11 +17,33 @@ export var spacesService = { historyQueue: [], eventQueueCount: 0, debug: true, + initialized: false, + initializationPromise: null, noop: () => {}, + // Ensure spacesService is initialized before processing events + async ensureInitialized() { + if (spacesService.initialized) { + return; + } + + if (spacesService.initializationPromise) { + await spacesService.initializationPromise; + return; + } + + spacesService.initializationPromise = spacesService.initialiseSpaces().then(() => { + spacesService.initialized = true; + spacesService.initializationPromise = null; + }); + + await spacesService.initializationPromise; + }, + // initialise spaces - combine open windows with saved sessions async initialiseSpaces() { + spacesService.initialized = false; // Reset on re-initialization console.log(`Inside spacesService.initialiseSpaces()`); // update version numbers const lastVersion = await spacesService.fetchLastVersion(); @@ -51,14 +73,18 @@ export var spacesService = { } // then try to match current open windows with saved sessions - windows.forEach(curWindow => { + for (const curWindow of windows) { if (!spacesService.filterInternalWindows(curWindow)) { - spacesService.checkForSessionMatch(curWindow); + await spacesService.checkForSessionMatch(curWindow); } - }); + } + + // Initialization complete + spacesService.initialized = true; }); } catch (error) { console.error('Error initializing spaces:', error); + spacesService.initialized = false; } }, @@ -403,7 +429,7 @@ export var spacesService = { async handleWindowFocussed(windowId) { if (spacesService.debug) { // eslint-disable-next-line no-console - console.log(`handlingWindowFocussed event. windowId: ${windowId}`); + console.log(`handleWindowFocussed event. windowId: ${windowId}`); } if (windowId <= 0) { From 6eec893b2951251f2995a7ab143e60f216d157b8 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 25 Aug 2025 21:29:20 -0700 Subject: [PATCH 19/92] Fix database session updates not saving current tab state --- js/background/spacesService.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 5e3d532..481f76d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -563,7 +563,7 @@ export var spacesService = { // if it is a saved session then update db if (session.id) { - spacesService.saveExistingSession(session.id); + spacesService.saveExistingSession(session, callback); } } @@ -664,7 +664,7 @@ export var spacesService = { session.tabs = tabs; session.sessionHash = spacesService.generateSessionHash(session.tabs); - spacesService.saveExistingSession(session.id, callback); + spacesService.saveExistingSession(session, callback); }, async updateSessionName(sessionId, sessionName, callback) { @@ -675,12 +675,10 @@ export var spacesService = { const session = await dbService.fetchSessionById(sessionId); session.name = sessionName; - spacesService.saveExistingSession(session.id, callback); + spacesService.saveExistingSession(session, callback); }, - async saveExistingSession(sessionId, callback) { - const session = await dbService.fetchSessionById(sessionId); - + async saveExistingSession(session, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; From 5ef391b17b35d4b32f0712738fe8e182e3e8a98a Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 25 Aug 2025 21:57:07 -0700 Subject: [PATCH 20/92] Spaces window id persistence (survives service worker restarts) --- js/background/background.js | 49 +++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 8e85a24..f2a16e7 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -20,6 +20,38 @@ let spacesOpenWindowId = false; const noop = () => {}; const debug = false; +async function rediscoverSpacesWindow() { + // Try to restore from storage first + const stored = await chrome.storage.local.get('spacesOpenWindowId'); + if (stored.spacesOpenWindowId) { + // Verify the window still exists + try { + const window = await chrome.windows.get(stored.spacesOpenWindowId); + if (window) { + spacesOpenWindowId = stored.spacesOpenWindowId; + return; + } + } catch (error) { + // Window doesn't exist, remove from storage + await chrome.storage.local.remove('spacesOpenWindowId'); + } + } + + // If not in storage or window doesn't exist, search for spaces window by URL + const spacesUrl = chrome.runtime.getURL('spaces.html'); + const allWindows = await chrome.windows.getAll({populate: true}); + + for (const window of allWindows) { + for (const tab of window.tabs) { + if (tab.url && tab.url.startsWith(spacesUrl)) { + spacesOpenWindowId = window.id; + await chrome.storage.local.set({spacesOpenWindowId: window.id}); + return; + } + } + } +} + // runtime extension install listener chrome.runtime.onInstalled.addListener(details => { console.log(`Extension installed: ${JSON.stringify(details)}`); @@ -48,6 +80,7 @@ chrome.runtime.onInstalled.addListener(details => { // Handle Chrome startup - this is when window IDs get reassigned! chrome.runtime.onStartup.addListener(async () => { await spacesService.initialiseSpaces(); + await rediscoverSpacesWindow(); }); // LISTENERS @@ -99,6 +132,8 @@ chrome.runtime.onStartup.addListener(async () => { chrome.windows.getAll({}, windows => { if (windows.length === 1 && spacesOpenWindowId) { chrome.windows.remove(spacesOpenWindowId); + spacesOpenWindowId = false; + chrome.storage.local.remove('spacesOpenWindowId'); } }); }); @@ -523,6 +558,7 @@ chrome.runtime.onStartup.addListener(async () => { }, window => { spacesOpenWindowId = window.id; + chrome.storage.local.set({spacesOpenWindowId: window.id}); } ); } @@ -646,7 +682,12 @@ chrome.runtime.onStartup.addListener(async () => { async function updateSpacesWindow(source) { if (debug) { // eslint-disable-next-line no-console - console.log(`updateSpacesWindow triggered. source: ${source}`); + console.log(`updateSpacesWindow: triggered. source: ${source}`); + } + + // If we don't have a cached spacesOpenWindowId, try to find the spaces window + if (!spacesOpenWindowId) { + await rediscoverSpacesWindow(); } if (spacesOpenWindowId) { @@ -655,6 +696,7 @@ chrome.runtime.onStartup.addListener(async () => { // eslint-disable-next-line no-console console.log(`updateSpacesWindow: Error getting spacesOpenWindow: ${chrome.runtime.lastError}`); spacesOpenWindowId = false; + await chrome.storage.local.remove('spacesOpenWindowId'); return; } @@ -674,7 +716,10 @@ chrome.runtime.onStartup.addListener(async () => { function checkInternalSpacesWindows(windowId, windowClosed) { if (windowId === spacesOpenWindowId) { - if (windowClosed) spacesOpenWindowId = false; + if (windowClosed) { + spacesOpenWindowId = false; + chrome.storage.local.remove('spacesOpenWindowId'); + } return true; } if (windowId === spacesPopupWindowId) { From 31b80792ea4f6639b03a4a13e4a1b9f7a425ac9c Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Mon, 25 Aug 2025 22:08:49 -0700 Subject: [PATCH 21/92] Fix global variables for extension window ids in background.js getting out of sync when the service worker is torn down. --- js/background/background.js | 43 +++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index f2a16e7..f038218 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -12,44 +12,47 @@ import { spacesService } from './spacesService.js'; /** @typedef {import('./common.js').SessionPresence} SessionPresence */ /** @typedef {import('./common.js').Space} Space */ -// TODO(codedread): Eliminate all global variables and use chrome.storage.local. - // eslint-disable-next-line no-unused-vars, no-var let spacesPopupWindowId = false; let spacesOpenWindowId = false; const noop = () => {}; const debug = false; -async function rediscoverSpacesWindow() { +async function rediscoverWindowIds() { + spacesOpenWindowId = await rediscoverWindowByUrl('spacesOpenWindowId', 'spaces.html'); + spacesPopupWindowId = await rediscoverWindowByUrl('spacesPopupWindowId', 'popup.html'); +} + +async function rediscoverWindowByUrl(storageKey, htmlFilename) { // Try to restore from storage first - const stored = await chrome.storage.local.get('spacesOpenWindowId'); - if (stored.spacesOpenWindowId) { + const stored = await chrome.storage.local.get(storageKey); + if (stored[storageKey]) { // Verify the window still exists try { - const window = await chrome.windows.get(stored.spacesOpenWindowId); + const window = await chrome.windows.get(stored[storageKey]); if (window) { - spacesOpenWindowId = stored.spacesOpenWindowId; - return; + return stored[storageKey]; } } catch (error) { // Window doesn't exist, remove from storage - await chrome.storage.local.remove('spacesOpenWindowId'); + await chrome.storage.local.remove(storageKey); } } - // If not in storage or window doesn't exist, search for spaces window by URL - const spacesUrl = chrome.runtime.getURL('spaces.html'); + // If not in storage or window doesn't exist, search for window by URL + const targetUrl = chrome.runtime.getURL(htmlFilename); const allWindows = await chrome.windows.getAll({populate: true}); for (const window of allWindows) { for (const tab of window.tabs) { - if (tab.url && tab.url.startsWith(spacesUrl)) { - spacesOpenWindowId = window.id; - await chrome.storage.local.set({spacesOpenWindowId: window.id}); - return; + if (tab.url && tab.url.startsWith(targetUrl)) { + await chrome.storage.local.set({[storageKey]: window.id}); + return window.id; } } } + + return false; } // runtime extension install listener @@ -80,7 +83,7 @@ chrome.runtime.onInstalled.addListener(details => { // Handle Chrome startup - this is when window IDs get reassigned! chrome.runtime.onStartup.addListener(async () => { await spacesService.initialiseSpaces(); - await rediscoverSpacesWindow(); + await rediscoverWindowIds(); }); // LISTENERS @@ -643,6 +646,7 @@ chrome.runtime.onStartup.addListener(async () => { }, window => { spacesPopupWindowId = window.id; + chrome.storage.local.set({spacesPopupWindowId: window.id}); } ); } @@ -687,7 +691,7 @@ chrome.runtime.onStartup.addListener(async () => { // If we don't have a cached spacesOpenWindowId, try to find the spaces window if (!spacesOpenWindowId) { - await rediscoverSpacesWindow(); + await rediscoverWindowIds(); } if (spacesOpenWindowId) { @@ -723,7 +727,10 @@ chrome.runtime.onStartup.addListener(async () => { return true; } if (windowId === spacesPopupWindowId) { - if (windowClosed) spacesPopupWindowId = false; + if (windowClosed) { + spacesPopupWindowId = false; + chrome.storage.local.remove('spacesPopupWindowId'); + } return true; } return false; From 7dbd3d2f6f02ac722f213515c77c66ba700296cb Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Tue, 26 Aug 2025 20:42:49 -0700 Subject: [PATCH 22/92] Attempt to fix the lost-space name issue once more by not clearing the windowId from the db upon SpacesService initialization. --- js/background/background.js | 1 + js/background/spacesService.js | 66 ++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index f038218..380fbf0 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -82,6 +82,7 @@ chrome.runtime.onInstalled.addListener(details => { // Handle Chrome startup - this is when window IDs get reassigned! chrome.runtime.onStartup.addListener(async () => { + await spacesService.clearWindowIdAssociations(); await spacesService.initialiseSpaces(); await rediscoverWindowIds(); }); diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 481f76d..2427c81 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -63,19 +63,10 @@ export var spacesService = { // populate session map from database spacesService.sessions = sessions; - // clear any previously saved windowIds both in memory and database - for (const session of spacesService.sessions) { - if (session.windowId) { - session.windowId = false; - // Persist the cleared windowId to database - await dbService.updateSession(session); - } - } - // then try to match current open windows with saved sessions for (const curWindow of windows) { if (!spacesService.filterInternalWindows(curWindow)) { - await spacesService.checkForSessionMatch(curWindow); + await spacesService.checkForSessionMatchDuringInit(curWindow); } } @@ -88,6 +79,33 @@ export var spacesService = { } }, + // Clear windowId associations after Chrome restart (when window IDs get reassigned) + async clearWindowIdAssociations() { + try { + const sessions = await dbService.fetchAllSessions(); + + // clear any previously saved windowIds both in memory and database + for (const session of sessions) { + if (session.windowId) { + session.windowId = false; + // Persist the cleared windowId to database + await dbService.updateSession(session); + } + } + + // Also clear from in-memory cache if it's already loaded + if (spacesService.sessions && spacesService.sessions.length > 0) { + for (const session of spacesService.sessions) { + if (session.windowId) { + session.windowId = false; + } + } + } + } catch (error) { + console.error('Error clearing window ID associations:', error); + } + }, + async resetAllSessionHashes(sessions) { for (const session of sessions) { // eslint-disable-next-line no-param-reassign @@ -183,6 +201,34 @@ export var spacesService = { return false; }, + async checkForSessionMatchDuringInit(curWindow) { + if (!curWindow.tabs || curWindow.tabs.length === 0) { + return; + } + + // First, check if there's already a session with this windowId (service worker reactivation case) + let existingSession = null; + try { + existingSession = await dbService.fetchSessionByWindowId(curWindow.id); + } catch (error) { + console.error('Error fetching session by windowId:', error); + } + + if (existingSession) { + if (spacesService.debug) { + // eslint-disable-next-line no-console + console.log( + `existing session found for windowId: ${curWindow.id}. session: ${existingSession.id || 'temporary'}` + ); + } + // Session already exists for this window, no need to create or match anything + return; + } + + // If no existing session, fall back to hash matching (Chrome restart case) + await spacesService.checkForSessionMatch(curWindow); + }, + async checkForSessionMatch(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; From d72340680b6e07bd175dc4ad94b8a11e4c6870eb Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Tue, 26 Aug 2025 23:41:17 -0700 Subject: [PATCH 23/92] Turn off debug, remove some unnecessary logging and a duplicated function --- js/background/spacesService.js | 2 +- js/switcher.js | 4 ---- js/utils.js | 34 ---------------------------------- 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 2427c81..d0b369d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -16,7 +16,7 @@ export var spacesService = { sessionUpdateTimers: {}, historyQueue: [], eventQueueCount: 0, - debug: true, + debug: false, initialized: false, initializationPromise: null, diff --git a/js/switcher.js b/js/switcher.js index baaade4..15351e7 100644 --- a/js/switcher.js +++ b/js/switcher.js @@ -22,10 +22,6 @@ function getSelectedSpace() { async function getSwitchKeycodes(callback) { const commands = await chrome.commands.getAll(); - - // eslint-disable-next-line no-console - console.dir(commands); - const commandStr = commands.switchCode; const keyStrArray = commandStr.split('+'); diff --git a/js/utils.js b/js/utils.js index 6f00bad..58b2e40 100644 --- a/js/utils.js +++ b/js/utils.js @@ -59,38 +59,4 @@ export var utils = { }); return valuesByKey[key] || false; }, - - async getSwitchKeycodes(callback) { - const commands = await chrome.commands.getAll(); - // eslint-disable-next-line no-console - console.dir(commands); - - const commandStr = commands.switchCode; - - const keyStrArray = commandStr.split('+'); - - // get keyStr of primary modifier - const primaryModifier = keyStrArray[0]; - - // get keyStr of secondary modifier - const secondaryModifier = - keyStrArray.length === 3 ? keyStrArray[1] : false; - - // get keycode of main key (last in array) - const curStr = keyStrArray[keyStrArray.length - 1]; - - // TODO: There's others. Period. Up Arrow etc. - let mainKeyCode; - if (curStr === 'Space') { - mainKeyCode = 32; - } else { - mainKeyCode = curStr.toUpperCase().charCodeAt(); - } - - callback({ - primaryModifier, - secondaryModifier, - mainKeyCode, - }); - }, }; From acfb77edb20efeb62e1c9935ff64cb5513f6934e Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 27 Aug 2025 09:04:29 -0700 Subject: [PATCH 24/92] Clean up some more debug logging --- js/background/background.js | 5 ----- js/background/spacesService.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 380fbf0..12b1ace 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -206,14 +206,12 @@ chrome.runtime.onStartup.addListener(async () => { case 'requestSpaceFromWindowId': windowId = _cleanParameter(request.windowId); - console.log(`background.js: requestSpaceFromWindowId for request.windowId=${request.windowId}, cleaned window ID: ${windowId}`); if (windowId) { requestSpaceFromWindowId(windowId, sendResponse); } return true; case 'requestCurrentSpace': - console.log('requestCurrentSpace event received'); requestCurrentSpace(sendResponse); return true; @@ -764,7 +762,6 @@ async function requestSessionPresence(sessionName) { const session = await dbService.fetchSessionByWindowId(windowId); if (session) { - console.log(`codedread: requestSpaceFromWindowId() found session: ${session.id} for windowId: ${windowId}`); /** @type {Space} */ const space = { sessionId: session.id, @@ -773,12 +770,10 @@ async function requestSessionPresence(sessionName) { tabs: session.tabs, history: session.history, }; - console.dir(space); callback(space); // otherwise build a space object out of the actual window } else { - console.log(`codedread: requestSpaceFromWindowId() found no session for windowId: ${windowId}`); chrome.windows.get(windowId, { populate: true }, window => { // if failed to load requested window if (chrome.runtime.lastError) { diff --git a/js/background/spacesService.js b/js/background/spacesService.js index d0b369d..8b4a728 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -44,7 +44,7 @@ export var spacesService = { // initialise spaces - combine open windows with saved sessions async initialiseSpaces() { spacesService.initialized = false; // Reset on re-initialization - console.log(`Inside spacesService.initialiseSpaces()`); + // update version numbers const lastVersion = await spacesService.fetchLastVersion(); spacesService.setLastVersion(chrome.runtime.getManifest().version); From d8d7bea2d36f4b07d91eedfb0766ca97b7ed7a69 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 27 Aug 2025 20:04:23 -0700 Subject: [PATCH 25/92] Move SpaceService async initialization waiting into SpacesService --- js/background/background.js | 6 ------ js/background/spacesService.js | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 12b1ace..f491760 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -91,7 +91,6 @@ chrome.runtime.onStartup.addListener(async () => { // add listeners for session monitoring chrome.tabs.onCreated.addListener(async (tab) => { - await spacesService.ensureInitialized(); // this call to checkInternalSpacesWindows actually returns false when it should return true // due to the event being called before the globalWindowIds get set. oh well, never mind. if (checkInternalSpacesWindows(tab.windowId, false)) return; @@ -100,21 +99,18 @@ chrome.runtime.onStartup.addListener(async () => { updateSpacesWindow('tabs.onCreated'); }); chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { - await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; spacesService.handleTabRemoved(tabId, removeInfo, () => { updateSpacesWindow('tabs.onRemoved'); }); }); chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { - await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; spacesService.handleTabMoved(tabId, moveInfo, () => { updateSpacesWindow('tabs.onMoved'); }); }); chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(tab.windowId, false)) return; spacesService.handleTabUpdated(tab, changeInfo, () => { @@ -122,7 +118,6 @@ chrome.runtime.onStartup.addListener(async () => { }); }); chrome.windows.onRemoved.addListener(async (windowId) => { - await spacesService.ensureInitialized(); if (checkInternalSpacesWindows(windowId, true)) return; spacesService.handleWindowRemoved(windowId, true, () => { updateSpacesWindow('windows.onRemoved'); @@ -166,7 +161,6 @@ chrome.runtime.onStartup.addListener(async () => { } } - await spacesService.ensureInitialized(); spacesService.handleWindowFocussed(windowId); }); diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 8b4a728..6239aa7 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -125,7 +125,7 @@ export var spacesService = { }); }, - // NOTE: if ever changing this funciton, then we'll need to update all + // NOTE: if ever changing this function, then we'll need to update all // saved sessionHashes so that they match next time, using: resetAllSessionHashes() _cleanUrl(url) { if (!url) { @@ -278,6 +278,8 @@ export var spacesService = { }, async matchSessionToWindow(session, curWindow) { + await spacesService.ensureInitialized(); + // remove any other sessions tied to this windowId (temporary sessions) for (let i = spacesService.sessions.length - 1; i >= 0; i -= 1) { if (spacesService.sessions[i].windowId === curWindow.id) { @@ -343,6 +345,8 @@ export var spacesService = { // ----------------------------------------------------------------------------------------- async handleTabRemoved(tabId, removeInfo, callback) { + await spacesService.ensureInitialized(); + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( @@ -382,7 +386,9 @@ export var spacesService = { } }, - handleTabMoved(tabId, moveInfo, callback) { + async handleTabMoved(tabId, moveInfo, callback) { + await spacesService.ensureInitialized(); + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( @@ -397,6 +403,8 @@ export var spacesService = { }, async handleTabUpdated(tab, changeInfo, callback) { + await spacesService.ensureInitialized(); + // NOTE: only queue event when tab has completed loading (title property exists at this point) if (tab.status === 'complete') { if (spacesService.debug) { @@ -427,6 +435,8 @@ export var spacesService = { }, async handleWindowRemoved(windowId, markAsClosed, callback) { + await spacesService.ensureInitialized(); + // ignore subsequent windowRemoved events for the same windowId (each closing tab will try to call this) if (spacesService.closedWindowIds[windowId]) { callback(); @@ -473,6 +483,8 @@ export var spacesService = { }, async handleWindowFocussed(windowId) { + await spacesService.ensureInitialized(); + if (spacesService.debug) { // eslint-disable-next-line no-console console.log(`handleWindowFocussed event. windowId: ${windowId}`); @@ -739,6 +751,8 @@ export var spacesService = { }, async saveNewSession(sessionName, tabs, windowId, callback) { + await spacesService.ensureInitialized(); + if (!tabs) { callback(); return; From 9dfd58b8abbabcae93e5b0c523191b237adc2327 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 27 Aug 2025 20:08:28 -0700 Subject: [PATCH 26/92] Handle a situation when session.name is undefined --- js/background/dbService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 8fde79b..1368c14 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -151,7 +151,7 @@ export var dbService = { const sessions = await dbService._fetchAllSessions(); let matchIndex; const matchFound = sessions.some((session, index) => { - if (session.name.toLowerCase() === sessionName.toLowerCase()) { + if (session.name?.toLowerCase() === sessionName.toLowerCase()) { matchIndex = index; return true; } From ea4c32cebcbdc8ffdc35c5977fcee9a5844c34b1 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 27 Aug 2025 21:43:49 -0700 Subject: [PATCH 27/92] Clean up usages of callback-style for chrome.windows.getAll() --- js/background/background.js | 13 ++++++------- js/background/spacesService.js | 25 ++++++++++++------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index f491760..5014ecf 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -128,13 +128,12 @@ chrome.runtime.onStartup.addListener(async () => { // NOTE: this is a workaround for an issue with the chrome 'restore previous session' option // if the spaces window is the only window open and you try to use it to open a space, // when that space loads, it also loads all the windows from the window that was last closed - chrome.windows.getAll({}, windows => { - if (windows.length === 1 && spacesOpenWindowId) { - chrome.windows.remove(spacesOpenWindowId); - spacesOpenWindowId = false; - chrome.storage.local.remove('spacesOpenWindowId'); - } - }); + const windows = await chrome.windows.getAll({}); + if (windows.length === 1 && spacesOpenWindowId) { + await chrome.windows.remove(spacesOpenWindowId); + spacesOpenWindowId = false; + await chrome.storage.local.remove('spacesOpenWindowId'); + } }); // don't need this listener as the tabUpdated listener also fires when a new window is created // chrome.windows.onCreated.addListener(function (window) { diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 6239aa7..46643ae 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -59,20 +59,19 @@ export var spacesService = { await spacesService.resetAllSessionHashes(sessions); } - chrome.windows.getAll({ populate: true }, async windows => { - // populate session map from database - spacesService.sessions = sessions; - - // then try to match current open windows with saved sessions - for (const curWindow of windows) { - if (!spacesService.filterInternalWindows(curWindow)) { - await spacesService.checkForSessionMatchDuringInit(curWindow); - } + const windows = await chrome.windows.getAll({ populate: true }); + // populate session map from database + spacesService.sessions = sessions; + + // then try to match current open windows with saved sessions + for (const curWindow of windows) { + if (!spacesService.filterInternalWindows(curWindow)) { + await spacesService.checkForSessionMatchDuringInit(curWindow); } - - // Initialization complete - spacesService.initialized = true; - }); + } + + // Initialization complete + spacesService.initialized = true; } catch (error) { console.error('Error initializing spaces:', error); spacesService.initialized = false; From 045e077fd90b54e1b2c08bc970a14cdfea6aaf1f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 27 Aug 2025 23:11:08 -0700 Subject: [PATCH 28/92] Refactor code to use the async version of chrome.windows.get() --- js/background/background.js | 237 +++++++++++++++------------------ js/background/spacesService.js | 161 +++++++++++----------- 2 files changed, 190 insertions(+), 208 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 5014ecf..7f2bba3 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -156,7 +156,7 @@ chrome.runtime.onStartup.addListener(async () => { if (!debug && spacesPopupWindowId) { if (spacesPopupWindowId) { - closePopupWindow(); + await closePopupWindow(); } } @@ -524,25 +524,20 @@ chrome.runtime.onStartup.addListener(async () => { // if spaces open window already exists then just give it focus (should be up to date) if (spacesOpenWindowId) { - chrome.windows.get( - spacesOpenWindowId, - { populate: true }, - window => { - chrome.windows.update(spacesOpenWindowId, { - focused: true, - }); - if (window.tabs[0].id) { - chrome.tabs.update(window.tabs[0].id, { url }); - } - } - ); + const window = await chrome.windows.get(spacesOpenWindowId, { populate: true }); + await chrome.windows.update(spacesOpenWindowId, { + focused: true, + }); + if (window.tabs[0].id) { + await chrome.tabs.update(window.tabs[0].id, { url }); + } // otherwise re-create it } else { // TODO(codedread): Handle multiple displays and errors. const displays = await chrome.system.display.getInfo(); let screen = displays[0].bounds; - chrome.windows.create( + const window = await chrome.windows.create( { type: 'popup', url, @@ -550,12 +545,9 @@ chrome.runtime.onStartup.addListener(async () => { width: Math.min(screen.width, 1000), top: 0, left: 0, - }, - window => { - spacesOpenWindowId = window.id; - chrome.storage.local.set({spacesOpenWindowId: window.id}); - } - ); + }); + spacesOpenWindowId = window.id; + await chrome.storage.local.set({spacesOpenWindowId: window.id}); } } function showSpacesMoveWindow(tabUrl) { @@ -593,85 +585,77 @@ chrome.runtime.onStartup.addListener(async () => { return params; } - function createOrShowSpacesPopupWindow(action, tabUrl) { - generatePopupParams(action, tabUrl).then(async (params) => { - const popupUrl = `${chrome.runtime.getURL( - 'popup.html' - )}#opener=bg&${params}`; - // if spaces window already exists - if (spacesPopupWindowId) { - chrome.windows.get( - spacesPopupWindowId, - { populate: true }, - window => { - // if window is currently focused then don't update - if (window.focused) { - // else update popupUrl and give it focus - } else { - chrome.windows.update(spacesPopupWindowId, { - focused: true, - }); - if (window.tabs[0].id) { - chrome.tabs.update(window.tabs[0].id, { - url: popupUrl, - }); - } - } - } - ); - - // otherwise create it + async function createOrShowSpacesPopupWindow(action, tabUrl) { + const params = await generatePopupParams(action, tabUrl); + const popupUrl = `${chrome.runtime.getURL( + 'popup.html' + )}#opener=bg&${params}`; + // if spaces window already exists + if (spacesPopupWindowId) { + const window = await chrome.windows.get( + spacesPopupWindowId, + { populate: true } + ); + // if window is currently focused then don't update + if (window.focused) { + // else update popupUrl and give it focus } else { - // TODO(codedread): Handle multiple displays and errors. - const displays = await chrome.system.display.getInfo(); - let screen = displays[0].bounds; - - chrome.windows.create( - { - type: 'popup', + await chrome.windows.update(spacesPopupWindowId, { + focused: true, + }); + if (window.tabs[0].id) { + await chrome.tabs.update(window.tabs[0].id, { url: popupUrl, - focused: true, - height: 450, - width: 310, - top: screen.height - 450, - left: screen.width - 310, - }, - window => { - spacesPopupWindowId = window.id; - chrome.storage.local.set({spacesPopupWindowId: window.id}); - } - ); + }); + } } - }); + + // otherwise create it + } else { + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + + const window = await chrome.windows.create( + { + type: 'popup', + url: popupUrl, + focused: true, + height: 450, + width: 310, + top: screen.height - 450, + left: screen.width - 310, + }); + spacesPopupWindowId = window.id; + await chrome.storage.local.set({spacesPopupWindowId: window.id}); + } } - function closePopupWindow() { + async function closePopupWindow() { if (spacesPopupWindowId) { - chrome.windows.get( - spacesPopupWindowId, - { populate: true }, - spacesWindow => { - if (!spacesWindow) return; - - // remove popup from history - if ( - spacesWindow.tabs.length > 0 && - spacesWindow.tabs[0].url - ) { - chrome.history.deleteUrl({ - url: spacesWindow.tabs[0].url, - }); - } - - // remove popup window - chrome.windows.remove(spacesWindow.id, () => { - if (chrome.runtime.lastError) { - // eslint-disable-next-line no-console - console.log(chrome.runtime.lastError.message); - } + try { + const spacesWindow = await chrome.windows.get( + spacesPopupWindowId, + { populate: true } + ); + if (!spacesWindow) return; + + // remove popup from history + if ( + spacesWindow.tabs.length > 0 && + spacesWindow.tabs[0].url + ) { + await chrome.history.deleteUrl({ + url: spacesWindow.tabs[0].url, }); } - ); + + // remove popup window + await chrome.windows.remove(spacesWindow.id); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e.message); + } } } @@ -767,22 +751,20 @@ async function requestSessionPresence(sessionName) { // otherwise build a space object out of the actual window } else { - chrome.windows.get(windowId, { populate: true }, window => { - // if failed to load requested window - if (chrome.runtime.lastError) { - callback(false); - } else { - /** @type {Space} */ - const space = { - sessionId: false, - windowId: window.id, - name: false, - tabs: window.tabs, - history: false, - }; - callback(space); - } - }); + try { + const window = await chrome.windows.get(windowId, { populate: true }); + /** @type {Space} */ + const space = { + sessionId: false, + windowId: window.id, + name: false, + tabs: window.tabs, + history: false, + }; + callback(space); + } catch (e) { + callback(false); + } } } @@ -930,30 +912,29 @@ async function requestSessionPresence(sessionName) { } } - function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { - chrome.windows.get(windowId, { populate: true }, async curWindow => { - const existingSession = await dbService.fetchSessionByName(sessionName); + async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { + const curWindow = await chrome.windows.get(windowId, { populate: true }); + const existingSession = await dbService.fetchSessionByName(sessionName); - // if session with same name already exist, then prompt to override the existing session - if (existingSession) { - if (!deleteOld) { - console.error( - `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` - ); - callback(false); - return; + // if session with same name already exist, then prompt to override the existing session + if (existingSession) { + if (!deleteOld) { + console.error( + `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` + ); + callback(false); + return; - // if we choose to overwrite, delete the existing session - } - handleDeleteSession(existingSession.id, noop); + // if we choose to overwrite, delete the existing session } - spacesService.saveNewSession( - sessionName, - curWindow.tabs, - curWindow.id, - callback - ); - }); + handleDeleteSession(existingSession.id, noop); + } + spacesService.saveNewSession( + sessionName, + curWindow.tabs, + curWindow.id, + callback + ); } async function handleRestoreFromBackup(space, deleteOld, callback) { diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 46643ae..a99717d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -514,7 +514,7 @@ export var spacesService = { }, // careful here as this function gets called A LOT - handleWindowEvent(windowId, eventId, callback) { + async handleWindowEvent(windowId, eventId, callback) { // eslint-disable-next-line no-param-reassign callback = typeof callback !== 'function' ? spacesService.noop : callback; @@ -540,102 +540,103 @@ export var spacesService = { return; } - chrome.windows.get(windowId, { populate: true }, async curWindow => { - if (chrome.runtime.lastError) { + let curWindow; + try { + curWindow = await chrome.windows.get(windowId, { populate: true }); + } catch (e) { + // eslint-disable-next-line no-console + console.log( + `${e.message}. perhaps its the development console???` + ); + + // if we can't find this window, then better remove references to it from the cached sessions + // don't mark as a removed window however, so that the space can be resynced up if the window + // does actually still exist (for some unknown reason) + spacesService.handleWindowRemoved( + windowId, + false, + spacesService.noop + ); + return; + } + + if (!curWindow || spacesService.filterInternalWindows(curWindow)) { + return; + } + + // don't allow event if it pertains to a closed window id + if (spacesService.closedWindowIds[windowId]) { + if (spacesService.debug) { // eslint-disable-next-line no-console console.log( - `${chrome.runtime.lastError.message}. perhaps its the development console???` + `ignoring event as it pertains to a closed windowId: ${windowId}` ); - - // if we can't find this window, then better remove references to it from the cached sessions - // don't mark as a removed window however, so that the space can be resynced up if the window - // does actually still exist (for some unknown reason) - spacesService.handleWindowRemoved( - windowId, - false, - spacesService.noop - ); - return; } + return; + } - if (!curWindow || spacesService.filterInternalWindows(curWindow)) { - return; - } + // if window is associated with an open session then update session + const session = await dbService.fetchSessionByWindowId(windowId); - // don't allow event if it pertains to a closed window id - if (spacesService.closedWindowIds[windowId]) { - if (spacesService.debug) { - // eslint-disable-next-line no-console - console.log( - `ignoring event as it pertains to a closed windowId: ${windowId}` - ); - } - return; + if (session) { + if (spacesService.debug) { + // eslint-disable-next-line no-console + console.log( + `tab statuses: ${curWindow.tabs + .map(curTab => { + return curTab.status; + }) + .join('|')}` + ); } - // if window is associated with an open session then update session - const session = await dbService.fetchSessionByWindowId(windowId); - - if (session) { - if (spacesService.debug) { - // eslint-disable-next-line no-console - console.log( - `tab statuses: ${curWindow.tabs - .map(curTab => { - return curTab.status; - }) - .join('|')}` - ); + // look for tabs recently added/removed from this session and update session history + const historyItems = spacesService.historyQueue.filter( + historyItem => { + return historyItem.windowId === windowId; } + ); - // look for tabs recently added/removed from this session and update session history - const historyItems = spacesService.historyQueue.filter( - historyItem => { - return historyItem.windowId === windowId; - } - ); + for (let i = historyItems.length - 1; i >= 0; i -= 1) { + const historyItem = historyItems[i]; - for (let i = historyItems.length - 1; i >= 0; i -= 1) { - const historyItem = historyItems[i]; - - if (historyItem.action === 'add') { - spacesService.addUrlToSessionHistory( - session, - historyItem.url - ); - } else if (historyItem.action === 'remove') { - spacesService.removeUrlFromSessionHistory( - session, - historyItem.url - ); - } - spacesService.historyQueue.splice(i, 1); + if (historyItem.action === 'add') { + spacesService.addUrlToSessionHistory( + session, + historyItem.url + ); + } else if (historyItem.action === 'remove') { + spacesService.removeUrlFromSessionHistory( + session, + historyItem.url + ); } + spacesService.historyQueue.splice(i, 1); + } - // override session tabs with tabs from window - session.tabs = curWindow.tabs; - session.sessionHash = spacesService.generateSessionHash( - session.tabs - ); + // override session tabs with tabs from window + session.tabs = curWindow.tabs; + session.sessionHash = spacesService.generateSessionHash( + session.tabs + ); - // if it is a saved session then update db - if (session.id) { - spacesService.saveExistingSession(session, callback); - } + // if it is a saved session then update db + if (session.id) { + spacesService.saveExistingSession(session, callback); } + } - // if no session found, it must be a new window. - // if session found without session.id then it must be a temporary session - // check for sessionMatch - if (!session || !session.id) { - if (spacesService.debug) { - // eslint-disable-next-line no-console - console.log('session check triggered'); - } - spacesService.checkForSessionMatch(curWindow); + // if no session found, it must be a new window. + // if session found without session.id then it must be a temporary session + // check for sessionMatch + if (!session || !session.id) { + if (spacesService.debug) { + // eslint-disable-next-line no-console + console.log('session check triggered'); } - callback(); - }); + spacesService.checkForSessionMatch(curWindow); + } + callback(); }, // PUBLIC FUNCTIONS From bf14894b0a5a40d8ac1c7c91dde6d7e9f9a23efb Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 29 Aug 2025 21:48:06 -0700 Subject: [PATCH 29/92] Clean up db.js a bit (remove dead code, change indentation, replace some var with const/let). --- js/background/db.js | 966 +++++++++++++++++++++----------------------- 1 file changed, 458 insertions(+), 508 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index 298d326..cb58370 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -1,479 +1,415 @@ //The MIT License //Copyright (c) 2012 Aaron Powell -//Conversion to ES Module by codedread -const window = self; - var indexedDB, - IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange, - transactionModes = { - readonly: 'readonly', - readwrite: 'readwrite', - }; - - var hasOwn = Object.prototype.hasOwnProperty; - - var getIndexedDB = function() { - if (!indexedDB) { - indexedDB = - window.indexedDB || - window.webkitIndexedDB || - window.mozIndexedDB || - window.oIndexedDB || - window.msIndexedDB; - - if (!indexedDB) { - throw 'IndexedDB required'; - } +/** + * Changes in 2025 by codedread: + * - Removed unused CallbackList class. + * - Modernized code style. + * - Made into an ES module. + */ + +const transactionModes = { + readonly: 'readonly', + readwrite: 'readwrite', +}; + +const hasOwn = Object.prototype.hasOwnProperty; +const defaultMapper = (value) => value; + +const Server = function(db, name) { + const that = this; + let closed = false; + + this.add = function(table) { + if (closed) { + throw 'Database has been closed'; } - return indexedDB; - }; - - var defaultMapper = function(value) { - return value; - }; - var CallbackList = function() { - var state, - list = []; + var records = []; + var counter = 0; - var exec = function(context, args) { - if (list) { - args = args || []; - state = state || [context, args]; - - for (var i = 0, il = list.length; i < il; i++) { - list[i].apply(state[0], state[1]); + for (var i = 0; i < arguments.length - 1; i++) { + if (Array.isArray(arguments[i + 1])) { + for (var j = 0; j < arguments[i + 1].length; j++) { + records[counter] = arguments[i + 1][j]; + counter++; } - - list = []; + } else { + records[counter] = arguments[i + 1]; + counter++; } - }; + } - this.add = function() { - for (var i = 0, il = arguments.length; i < il; i++) { - list.push(arguments[i]); - } + var transaction = db.transaction(table, transactionModes.readwrite), + store = transaction.objectStore(table); - if (state) { - exec(); - } + return new Promise((resolve, reject) => { + records.forEach((record) => { + let req; + if (record.item && record.key) { + var key = record.key; + record = record.item; + req = store.add(record, key); + } else { + req = store.add(record); + } - return this; - }; + req.onsuccess = function(e) { + var target = e.target; + var keyPath = target.source.keyPath; + if (keyPath === null) { + keyPath = '__id__'; + } + Object.defineProperty(record, keyPath, { + value: target.result, + enumerable: true, + }); + }; + }); - this.execute = function() { - exec(this, arguments); - return this; - }; + transaction.oncomplete = function() { + resolve(records, that); + }; + transaction.onerror = function(e) { + reject(e); + }; + transaction.onabort = function(e) { + reject(e); + }; + }); }; - var Server = function(db, name) { - var that = this, - closed = false; - - this.add = function(table) { - if (closed) { - throw 'Database has been closed'; - } + this.update = function(table) { + if (closed) { + throw 'Database has been closed'; + } - var records = []; - var counter = 0; + var records = []; + for (var i = 0; i < arguments.length - 1; i++) { + records[i] = arguments[i + 1]; + } - for (var i = 0; i < arguments.length - 1; i++) { - if (Array.isArray(arguments[i + 1])) { - for (var j = 0; j < arguments[i + 1].length; j++) { - records[counter] = arguments[i + 1][j]; - counter++; - } + var transaction = db.transaction(table, transactionModes.readwrite), + store = transaction.objectStore(table), + keyPath = store.keyPath; + + return new Promise((resolve, reject) => { + records.forEach((record) => { + let req; + let count; + if (record.item && record.key) { + var key = record.key; + record = record.item; + req = store.put(record, key); } else { - records[counter] = arguments[i + 1]; - counter++; + req = store.put(record); } - } - var transaction = db.transaction(table, transactionModes.readwrite), - store = transaction.objectStore(table); - - return new Promise(function(resolve, reject) { - records.forEach(function(record) { - var req; - if (record.item && record.key) { - var key = record.key; - record = record.item; - req = store.add(record, key); - } else { - req = store.add(record); - } - - req.onsuccess = function(e) { - var target = e.target; - var keyPath = target.source.keyPath; - if (keyPath === null) { - keyPath = '__id__'; - } - Object.defineProperty(record, keyPath, { - value: target.result, - enumerable: true, - }); - }; - }); - - transaction.oncomplete = function() { - resolve(records, that); - }; - transaction.onerror = function(e) { - reject(e); - }; - transaction.onabort = function(e) { - reject(e); + req.onsuccess = function(e) { + // deferred.notify(); es6 promise can't notify }; }); - }; - this.update = function(table) { - if (closed) { - throw 'Database has been closed'; - } + transaction.oncomplete = function() { + resolve(records, that); + }; + transaction.onerror = function(e) { + reject(e); + }; + transaction.onabort = function(e) { + reject(e); + }; + }); + }; - var records = []; - for (var i = 0; i < arguments.length - 1; i++) { - records[i] = arguments[i + 1]; - } + this.remove = function(table, key) { + if (closed) { + throw 'Database has been closed'; + } + var transaction = db.transaction(table, transactionModes.readwrite), + store = transaction.objectStore(table); - var transaction = db.transaction(table, transactionModes.readwrite), - store = transaction.objectStore(table), - keyPath = store.keyPath; - - return new Promise(function(resolve, reject) { - records.forEach(function(record) { - var req; - var count; - if (record.item && record.key) { - var key = record.key; - record = record.item; - req = store.put(record, key); - } else { - req = store.put(record); - } + return new Promise((resolve, reject) => { + var req = store['delete'](key); + transaction.oncomplete = function() { + resolve(key); + }; + transaction.onerror = function(e) { + reject(e); + }; + }); + }; - req.onsuccess = function(e) { - // deferred.notify(); es6 promise can't notify - }; - }); + this.clear = function(table) { + if (closed) { + throw 'Database has been closed'; + } + var transaction = db.transaction(table, transactionModes.readwrite), + store = transaction.objectStore(table); - transaction.oncomplete = function() { - resolve(records, that); - }; - transaction.onerror = function(e) { - reject(e); - }; - transaction.onabort = function(e) { - reject(e); - }; - }); - }; + var req = store.clear(); + return new Promise((resolve, reject) => { + transaction.oncomplete = function() { + resolve(); + }; + transaction.onerror = function(e) { + reject(e); + }; + }); + }; - this.remove = function(table, key) { - if (closed) { - throw 'Database has been closed'; - } - var transaction = db.transaction(table, transactionModes.readwrite), - store = transaction.objectStore(table); + this.close = function() { + if (closed) { + throw 'Database has been closed'; + } + db.close(); + closed = true; + delete dbCache[name]; + }; - return new Promise(function(resolve, reject) { - var req = store['delete'](key); - transaction.oncomplete = function() { - resolve(key); - }; - transaction.onerror = function(e) { - reject(e); - }; - }); - }; + this.get = function(table, id) { + if (closed) { + throw 'Database has been closed'; + } + var transaction = db.transaction(table), + store = transaction.objectStore(table); - this.clear = function(table) { - if (closed) { - throw 'Database has been closed'; - } - var transaction = db.transaction(table, transactionModes.readwrite), - store = transaction.objectStore(table); + var req = store.get(id); + return new Promise((resolve, reject) => { + req.onsuccess = function(e) { + resolve(e.target.result); + }; + transaction.onerror = function(e) { + reject(e); + }; + }); + }; - var req = store.clear(); - return new Promise(function(resolve, reject) { - transaction.oncomplete = function() { - resolve(); - }; - transaction.onerror = function(e) { - reject(e); - }; - }); - }; + this.query = function(table, index) { + if (closed) { + throw 'Database has been closed'; + } + return new IndexQuery(table, db, index); + }; - this.close = function() { - if (closed) { - throw 'Database has been closed'; + for (var i = 0, il = db.objectStoreNames.length; i < il; i++) { + (function(storeName) { + that[storeName] = {}; + for (var i in that) { + if (!hasOwn.call(that, i) || i === 'close') { + continue; + } + that[storeName][i] = (function(i) { + return function() { + var args = [storeName].concat( + [].slice.call(arguments, 0) + ); + return that[i].apply(that, args); + }; + })(i); } - db.close(); - closed = true; - delete dbCache[name]; - }; + })(db.objectStoreNames[i]); + } +}; + +var IndexQuery = function(table, db, indexName) { + var that = this; + var modifyObj = false; + + var runQuery = function( + type, + args, + cursorType, + direction, + limitRange, + filters, + mapper + ) { + var transaction = db.transaction( + table, + modifyObj + ? transactionModes.readwrite + : transactionModes.readonly + ), + store = transaction.objectStore(table), + index = indexName ? store.index(indexName) : store, + keyRange = type ? IDBKeyRange[type].apply(null, args) : null, + results = [], + indexArgs = [keyRange], + limitRange = limitRange ? limitRange : null, + filters = filters ? filters : [], + counter = 0; + + if (cursorType !== 'count') { + indexArgs.push(direction || 'next'); + } - this.get = function(table, id) { - if (closed) { - throw 'Database has been closed'; + // create a function that will set in the modifyObj properties into + // the passed record. + var modifyKeys = modifyObj ? Object.keys(modifyObj) : false; + var modifyRecord = function(record) { + for (var i = 0; i < modifyKeys.length; i++) { + var key = modifyKeys[i]; + var val = modifyObj[key]; + if (val instanceof Function) val = val(record); + record[key] = val; } - var transaction = db.transaction(table), - store = transaction.objectStore(table); - - var req = store.get(id); - return new Promise(function(resolve, reject) { - req.onsuccess = function(e) { - resolve(e.target.result); - }; - transaction.onerror = function(e) { - reject(e); - }; - }); + return record; }; - this.query = function(table, index) { - if (closed) { - throw 'Database has been closed'; - } - return new IndexQuery(table, db, index); - }; + index[cursorType].apply(index, indexArgs).onsuccess = function(e) { + var cursor = e.target.result; + if (typeof cursor === typeof 0) { + results = cursor; + } else if (cursor) { + if (limitRange !== null && limitRange[0] > counter) { + counter = limitRange[0]; + cursor.advance(limitRange[0]); + } else if ( + limitRange !== null && + counter >= limitRange[0] + limitRange[1] + ) { + //out of limit range... skip + } else { + var matchFilter = true; + var result = + 'value' in cursor ? cursor.value : cursor.key; + + filters.forEach(function(filter) { + if (!filter || !filter.length) { + //Invalid filter do nothing + } else if (filter.length === 2) { + matchFilter = + matchFilter && + result[filter[0]] === filter[1]; + } else { + matchFilter = + matchFilter && + filter[0].apply(undefined, [result]); + } + }); - for (var i = 0, il = db.objectStoreNames.length; i < il; i++) { - (function(storeName) { - that[storeName] = {}; - for (var i in that) { - if (!hasOwn.call(that, i) || i === 'close') { - continue; + if (matchFilter) { + counter++; + results.push(mapper(result)); + // if we're doing a modify, run it now + if (modifyObj) { + result = modifyRecord(result); + cursor.update(result); + } } - that[storeName][i] = (function(i) { - return function() { - var args = [storeName].concat( - [].slice.call(arguments, 0) - ); - return that[i].apply(that, args); - }; - })(i); + cursor['continue'](); } - })(db.objectStoreNames[i]); - } - }; - - var IndexQuery = function(table, db, indexName) { - var that = this; - var modifyObj = false; - - var runQuery = function( - type, - args, - cursorType, - direction, - limitRange, - filters, - mapper - ) { - var transaction = db.transaction( - table, - modifyObj - ? transactionModes.readwrite - : transactionModes.readonly - ), - store = transaction.objectStore(table), - index = indexName ? store.index(indexName) : store, - keyRange = type ? IDBKeyRange[type].apply(null, args) : null, - results = [], - indexArgs = [keyRange], - limitRange = limitRange ? limitRange : null, - filters = filters ? filters : [], - counter = 0; - - if (cursorType !== 'count') { - indexArgs.push(direction || 'next'); } + }; - // create a function that will set in the modifyObj properties into - // the passed record. - var modifyKeys = modifyObj ? Object.keys(modifyObj) : false; - var modifyRecord = function(record) { - for (var i = 0; i < modifyKeys.length; i++) { - var key = modifyKeys[i]; - var val = modifyObj[key]; - if (val instanceof Function) val = val(record); - record[key] = val; - } - return record; + return new Promise((resolve, reject) => { + transaction.oncomplete = function() { + resolve(results); }; - - index[cursorType].apply(index, indexArgs).onsuccess = function(e) { - var cursor = e.target.result; - if (typeof cursor === typeof 0) { - results = cursor; - } else if (cursor) { - if (limitRange !== null && limitRange[0] > counter) { - counter = limitRange[0]; - cursor.advance(limitRange[0]); - } else if ( - limitRange !== null && - counter >= limitRange[0] + limitRange[1] - ) { - //out of limit range... skip - } else { - var matchFilter = true; - var result = - 'value' in cursor ? cursor.value : cursor.key; - - filters.forEach(function(filter) { - if (!filter || !filter.length) { - //Invalid filter do nothing - } else if (filter.length === 2) { - matchFilter = - matchFilter && - result[filter[0]] === filter[1]; - } else { - matchFilter = - matchFilter && - filter[0].apply(undefined, [result]); - } - }); - - if (matchFilter) { - counter++; - results.push(mapper(result)); - // if we're doing a modify, run it now - if (modifyObj) { - result = modifyRecord(result); - cursor.update(result); - } - } - cursor['continue'](); - } - } + transaction.onerror = function(e) { + reject(e); + }; + transaction.onabort = function(e) { + reject(e); }; + }); + }; - return new Promise(function(resolve, reject) { - transaction.oncomplete = function() { - resolve(results); - }; - transaction.onerror = function(e) { - reject(e); - }; - transaction.onabort = function(e) { - reject(e); - }; - }); + var Query = function(type, args) { + var direction = 'next', + cursorType = 'openCursor', + filters = [], + limitRange = null, + mapper = defaultMapper, + unique = false; + + var execute = function() { + return runQuery( + type, + args, + cursorType, + unique ? direction + 'unique' : direction, + limitRange, + filters, + mapper + ); }; - var Query = function(type, args) { - var direction = 'next', - cursorType = 'openCursor', - filters = [], - limitRange = null, - mapper = defaultMapper, - unique = false; - - var execute = function() { - return runQuery( - type, - args, - cursorType, - unique ? direction + 'unique' : direction, - limitRange, - filters, - mapper - ); - }; - - var limit = function() { - limitRange = Array.prototype.slice.call(arguments, 0, 2); - if (limitRange.length == 1) { - limitRange.unshift(0); - } + var limit = function() { + limitRange = Array.prototype.slice.call(arguments, 0, 2); + if (limitRange.length == 1) { + limitRange.unshift(0); + } - return { - execute: execute, - }; + return { + execute: execute, }; - var count = function() { - direction = null; - cursorType = 'count'; + }; + var count = function() { + direction = null; + cursorType = 'count'; - return { - execute: execute, - }; - }; - var keys = function() { - cursorType = 'openKeyCursor'; - - return { - desc: desc, - execute: execute, - filter: filter, - distinct: distinct, - map: map, - }; + return { + execute: execute, }; - var filter = function() { - filters.push(Array.prototype.slice.call(arguments, 0, 2)); - - return { - keys: keys, - execute: execute, - filter: filter, - desc: desc, - distinct: distinct, - modify: modify, - limit: limit, - map: map, - }; + }; + var keys = function() { + cursorType = 'openKeyCursor'; + + return { + desc: desc, + execute: execute, + filter: filter, + distinct: distinct, + map: map, }; - var desc = function() { - direction = 'prev'; - - return { - keys: keys, - execute: execute, - filter: filter, - distinct: distinct, - modify: modify, - map: map, - }; + }; + var filter = function() { + filters.push(Array.prototype.slice.call(arguments, 0, 2)); + + return { + keys: keys, + execute: execute, + filter: filter, + desc: desc, + distinct: distinct, + modify: modify, + limit: limit, + map: map, }; - var distinct = function() { - unique = true; - return { - keys: keys, - count: count, - execute: execute, - filter: filter, - desc: desc, - modify: modify, - map: map, - }; + }; + var desc = function() { + direction = 'prev'; + + return { + keys: keys, + execute: execute, + filter: filter, + distinct: distinct, + modify: modify, + map: map, }; - var modify = function(update) { - modifyObj = update; - return { - execute: execute, - }; + }; + var distinct = function() { + unique = true; + return { + keys: keys, + count: count, + execute: execute, + filter: filter, + desc: desc, + modify: modify, + map: map, }; - var map = function(fn) { - mapper = fn; - - return { - execute: execute, - count: count, - keys: keys, - filter: filter, - desc: desc, - distinct: distinct, - modify: modify, - limit: limit, - map: map, - }; + }; + var modify = function(update) { + modifyObj = update; + return { + execute: execute, }; + }; + var map = function(fn) { + mapper = fn; return { execute: execute, @@ -488,105 +424,119 @@ const window = self; }; }; - 'only bound upperBound lowerBound'.split(' ').forEach(function(name) { - that[name] = function() { - return new Query(name, arguments); - }; - }); - - this.filter = function() { - var query = new Query(null, null); - return query.filter.apply(query, arguments); + return { + execute: execute, + count: count, + keys: keys, + filter: filter, + desc: desc, + distinct: distinct, + modify: modify, + limit: limit, + map: map, }; + }; - this.all = function() { - return this.filter(); + 'only bound upperBound lowerBound'.split(' ').forEach(function(name) { + that[name] = function() { + return new Query(name, arguments); }; + }); + + this.filter = function() { + var query = new Query(null, null); + return query.filter.apply(query, arguments); }; - var createSchema = function(e, schema, db) { - if (typeof schema === 'function') { - schema = schema(); + this.all = function() { + return this.filter(); + }; +}; + +var createSchema = function(e, schema, db) { + if (typeof schema === 'function') { + schema = schema(); + } + + for (var tableName in schema) { + var table = schema[tableName]; + var store; + if ( + !hasOwn.call(schema, tableName) || + db.objectStoreNames.contains(tableName) + ) { + store = e.currentTarget.transaction.objectStore(tableName); + } else { + store = db.createObjectStore(tableName, table.key); } - for (var tableName in schema) { - var table = schema[tableName]; - var store; - if ( - !hasOwn.call(schema, tableName) || - db.objectStoreNames.contains(tableName) - ) { - store = e.currentTarget.transaction.objectStore(tableName); - } else { - store = db.createObjectStore(tableName, table.key); - } - - for (var indexKey in table.indexes) { - var index = table.indexes[indexKey]; - try { - store.index(indexKey); - } catch (e) { - store.createIndex( - indexKey, - index.key || indexKey, - Object.keys(index).length ? index : { unique: false } - ); - } + for (var indexKey in table.indexes) { + var index = table.indexes[indexKey]; + try { + store.index(indexKey); + } catch (e) { + store.createIndex( + indexKey, + index.key || indexKey, + Object.keys(index).length ? index : { unique: false } + ); } } - }; - - var open = function(e, server, version, schema) { - var db = e.target.result; - var s = new Server(db, server); - var upgrade; - - dbCache[server] = db; - - return Promise.resolve(s); - }; - - var dbCache = {}; - - export var db = { - version: '0.9.2', - open: function(options) { - var request; + } +}; + +var open = function(e, server, version, schema) { + var db = e.target.result; + var s = new Server(db, server); + + dbCache[server] = db; + + return Promise.resolve(s); +}; + +const dbCache = {}; + +export const db = { + version: '0.9.2', + /** @returns {Promise} */ + open(options) { + /** @type {IDBOpenDBRequest} */ + var request; + + return new Promise((resolve, reject) => { + if (dbCache[options.server]) { + open( + { + target: { + result: dbCache[options.server], + }, + }, + options.server, + options.version, + options.schema + ).then(resolve, reject); + } else { + request = indexedDB.open( + options.server, + options.version + ); - return new Promise(function(resolve, reject) { - if (dbCache[options.server]) { + request.onsuccess = function(e) { open( - { - target: { - result: dbCache[options.server], - }, - }, + e, options.server, options.version, options.schema ).then(resolve, reject); - } else { - request = getIndexedDB().open( - options.server, - options.version - ); - - request.onsuccess = function(e) { - open( - e, - options.server, - options.version, - options.schema - ).then(resolve, reject); - }; + }; - request.onupgradeneeded = function(e) { - createSchema(e, options.schema, e.target.result); - }; - request.onerror = function(e) { - reject(e); - }; - } - }); - }, - }; + request.onupgradeneeded = function(e) { + createSchema(e, options.schema, e.target.result); + }; + request.onerror = function(e) { + reject(e); + }; + } + }); + }, +}; From ae06a575970be4ca81af8f837d369d92bbdfa614 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 29 Aug 2025 22:05:01 -0700 Subject: [PATCH 30/92] Change the Server type into a class inside db.js --- js/background/db.js | 125 ++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index cb58370..d797e85 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -15,12 +15,44 @@ const transactionModes = { const hasOwn = Object.prototype.hasOwnProperty; const defaultMapper = (value) => value; -const Server = function(db, name) { - const that = this; - let closed = false; +class Server { + /** @type {IDBDatabase} */ + db; + + /** @type {string} */ + name; + + /** @type {boolean} */ + closed; + + /** + * @param {IDBDatabase} db + * @param {string} name + */ + constructor(db, name) { + this.db = db; + this.name = name; + this.closed = false; + + for (var i = 0, il = db.objectStoreNames.length; i < il; i++) { + (function(storeName) { + this[storeName] = {}; + for (var methodName in this) { + if (hasOwn.call(this, methodName) || methodName === 'close') { + continue; + } + this[storeName][methodName] = ((methodName) => { + return (...args) => { + return this[methodName].apply(this, [storeName, ...args]); + }; + })(methodName); + } + }).bind(this)(db.objectStoreNames[i]); + } + } - this.add = function(table) { - if (closed) { + add(table) { + if (this.closed) { throw 'Database has been closed'; } @@ -39,7 +71,7 @@ const Server = function(db, name) { } } - var transaction = db.transaction(table, transactionModes.readwrite), + var transaction = this.db.transaction(table, transactionModes.readwrite), store = transaction.objectStore(table); return new Promise((resolve, reject) => { @@ -66,8 +98,8 @@ const Server = function(db, name) { }; }); - transaction.oncomplete = function() { - resolve(records, that); + transaction.oncomplete = () => { + resolve(records, this); }; transaction.onerror = function(e) { reject(e); @@ -76,10 +108,10 @@ const Server = function(db, name) { reject(e); }; }); - }; + } - this.update = function(table) { - if (closed) { + update(table) { + if (this.closed) { throw 'Database has been closed'; } @@ -88,7 +120,7 @@ const Server = function(db, name) { records[i] = arguments[i + 1]; } - var transaction = db.transaction(table, transactionModes.readwrite), + var transaction = this.db.transaction(table, transactionModes.readwrite), store = transaction.objectStore(table), keyPath = store.keyPath; @@ -109,8 +141,8 @@ const Server = function(db, name) { }; }); - transaction.oncomplete = function() { - resolve(records, that); + transaction.oncomplete = () => { + resolve(records, this); }; transaction.onerror = function(e) { reject(e); @@ -119,13 +151,13 @@ const Server = function(db, name) { reject(e); }; }); - }; + } - this.remove = function(table, key) { - if (closed) { + remove(table, key) { + if (this.closed) { throw 'Database has been closed'; } - var transaction = db.transaction(table, transactionModes.readwrite), + var transaction = this.db.transaction(table, transactionModes.readwrite), store = transaction.objectStore(table); return new Promise((resolve, reject) => { @@ -137,13 +169,13 @@ const Server = function(db, name) { reject(e); }; }); - }; + } - this.clear = function(table) { - if (closed) { + clear(table) { + if (this.closed) { throw 'Database has been closed'; } - var transaction = db.transaction(table, transactionModes.readwrite), + var transaction = this.db.transaction(table, transactionModes.readwrite), store = transaction.objectStore(table); var req = store.clear(); @@ -155,22 +187,22 @@ const Server = function(db, name) { reject(e); }; }); - }; + } - this.close = function() { - if (closed) { + close() { + if (this.closed) { throw 'Database has been closed'; } - db.close(); - closed = true; - delete dbCache[name]; - }; + this.db.close(); + this.closed = true; + delete dbCache[this.name]; + } - this.get = function(table, id) { - if (closed) { + get(table, id) { + if (this.closed) { throw 'Database has been closed'; } - var transaction = db.transaction(table), + var transaction = this.db.transaction(table), store = transaction.objectStore(table); var req = store.get(id); @@ -182,34 +214,15 @@ const Server = function(db, name) { reject(e); }; }); - }; + } - this.query = function(table, index) { - if (closed) { + query(table, index) { + if (this.closed) { throw 'Database has been closed'; } - return new IndexQuery(table, db, index); - }; - - for (var i = 0, il = db.objectStoreNames.length; i < il; i++) { - (function(storeName) { - that[storeName] = {}; - for (var i in that) { - if (!hasOwn.call(that, i) || i === 'close') { - continue; - } - that[storeName][i] = (function(i) { - return function() { - var args = [storeName].concat( - [].slice.call(arguments, 0) - ); - return that[i].apply(that, args); - }; - })(i); - } - })(db.objectStoreNames[i]); + return new IndexQuery(table, this.db, index); } -}; +} var IndexQuery = function(table, db, indexName) { var that = this; From fa16aa9f6c44429eacb954af7abd0ec050d2c837 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 29 Aug 2025 22:18:23 -0700 Subject: [PATCH 31/92] Update db.js some more with cleaner jsdoc, modernized function syntax --- js/background/db.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index d797e85..b383489 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -466,7 +466,7 @@ var IndexQuery = function(table, db, indexName) { }; }; -var createSchema = function(e, schema, db) { +function createSchema(e, schema, db) { if (typeof schema === 'function') { schema = schema(); } @@ -496,29 +496,46 @@ var createSchema = function(e, schema, db) { } } } -}; +} -var open = function(e, server, version, schema) { +/** + * @param {Event} e + * @param {string} server + * @param {string} version + * @param {Object} schema + * @returns + */ +function dbOpen(e, server, version, schema) { var db = e.target.result; var s = new Server(db, server); dbCache[server] = db; return Promise.resolve(s); -}; +} const dbCache = {}; +/** + * @typedef {object} DbOpenOptions + * @property {string} server The name of the database. + * @property {number} version The version of the database. + * @property {object | function} schema The database schema. + */ + export const db = { version: '0.9.2', - /** @returns {Promise} */ + /** + * @param {DbOpenOptions} options + * @returns {Promise} + */ open(options) { /** @type {IDBOpenDBRequest} */ var request; return new Promise((resolve, reject) => { if (dbCache[options.server]) { - open( + dbOpen( { target: { result: dbCache[options.server], @@ -535,7 +552,7 @@ export const db = { ); request.onsuccess = function(e) { - open( + dbOpen( e, options.server, options.version, From faaf4185d16fab1bc3b6c460454745f8d207feb6 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 29 Aug 2025 22:31:37 -0700 Subject: [PATCH 32/92] Expose Server in db.js and neaten it up some more. --- js/background/db.js | 18 +----------------- js/background/dbService.js | 4 ++-- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index b383489..e32738f 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -15,7 +15,7 @@ const transactionModes = { const hasOwn = Object.prototype.hasOwnProperty; const defaultMapper = (value) => value; -class Server { +export class Server { /** @type {IDBDatabase} */ db; @@ -33,22 +33,6 @@ class Server { this.db = db; this.name = name; this.closed = false; - - for (var i = 0, il = db.objectStoreNames.length; i < il; i++) { - (function(storeName) { - this[storeName] = {}; - for (var methodName in this) { - if (hasOwn.call(this, methodName) || methodName === 'close') { - continue; - } - this[storeName][methodName] = ((methodName) => { - return (...args) => { - return this[methodName].apply(this, [storeName, ...args]); - }; - })(methodName); - } - }).bind(this)(db.objectStoreNames[i]); - } } add(table) { diff --git a/js/background/dbService.js b/js/background/dbService.js index 1368c14..fbd77ba 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -1,6 +1,6 @@ /* global db */ -import { db } from './db.js'; +import { db, Server } from './db.js'; // eslint-disable-next-line no-var export var dbService = { @@ -12,7 +12,7 @@ export var dbService = { /** * Opens and returns a database connection. - * @returns {Promise} Promise that resolves to database connection + * @returns {Promise} Promise that resolves to database connection */ getDb() { return db.open({ From 40d07d2e5044d9f55141285e3a4784c2f13d1759 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 29 Aug 2025 23:44:52 -0700 Subject: [PATCH 33/92] Lil more jsdoc in the db.js --- js/background/db.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/js/background/db.js b/js/background/db.js index e32738f..d39e34c 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -7,6 +7,7 @@ * - Made into an ES module. */ +/** @type {Object} */ const transactionModes = { readonly: 'readonly', readwrite: 'readwrite', @@ -450,6 +451,12 @@ var IndexQuery = function(table, db, indexName) { }; }; +/** + * Creates the database schema. + * @param {Event} e + * @param {*} schema + * @param {*} db + */ function createSchema(e, schema, db) { if (typeof schema === 'function') { schema = schema(); @@ -483,11 +490,12 @@ function createSchema(e, schema, db) { } /** + * Opens a connection to the database, caching it for future use. * @param {Event} e * @param {string} server * @param {string} version * @param {Object} schema - * @returns + * @returns {Promise} */ function dbOpen(e, server, version, schema) { var db = e.target.result; From f54ba54500f2ecca90c9aefcd2575e30313e3c5f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 09:23:58 -0700 Subject: [PATCH 34/92] Define @typedef in dbService.js for Session and update jsdoc --- js/background/dbService.js | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index fbd77ba..5a39a8a 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -2,14 +2,14 @@ import { db, Server } from './db.js'; +/** @typedef {import('./common.js').Space} Space */ + // eslint-disable-next-line no-var export var dbService = { DB_SERVER: 'spaces', DB_VERSION: '1', DB_SESSIONS: 'ttSessions', - noop() {}, - /** * Opens and returns a database connection. * @returns {Promise} Promise that resolves to database connection @@ -18,19 +18,20 @@ export var dbService = { return db.open({ server: dbService.DB_SERVER, version: dbService.DB_VERSION, - schema: dbService.getSchema, + schema: dbService.getSchema(), }); }, /** - * Properties of a session object - * session.id: auto-generated indexedDb object id - * session.sessionHash: a hash formed from the combined urls in the session window - * session.name: the saved name of the session - * session.tabs: an array of chrome tab objects (often taken from the chrome window obj) - * session.history: an array of chrome tab objects that have been removed from the session - * session.lastAccess: timestamp that gets updated with every window focus + * @typedef Session + * @property {number} id Auto-generated indexedDb object id + * @property {number} sessionHash A hash formed from the combined urls in the session window + * @property {string} name The saved name of the session + * @property {Array} tabs An array of chrome tab objects (often taken from the chrome window obj) + * @property {Array} history An array of chrome tab objects that have been removed from the session + * @property {Date} lastAccess Timestamp that gets updated with every window focus */ + /** * Returns database schema definition. * @returns {Object} Database schema configuration object @@ -51,7 +52,7 @@ export var dbService = { /** * Fetches all sessions from the database. - * @returns {Promise} Promise that resolves to array of session objects + * @returns {Promise>} Promise that resolves to array of session objects */ _fetchAllSessions() { return dbService.getDb().then(s => { @@ -65,7 +66,7 @@ export var dbService = { /** * Fetches a session by ID from the database. * @param {number} id - The session ID to fetch - * @returns {Promise} Promise that resolves to session object or null if not found + * @returns {Promise} Promise that resolves to session object or null if not found */ _fetchSessionById(id) { return dbService.getDb().then(s => { @@ -83,7 +84,7 @@ export var dbService = { /** * Fetches all sessions from the database. - * @returns {Promise} Promise that resolves to array of session objects + * @returns {Promise>} Promise that resolves to array of session objects */ async fetchAllSessions() { try { @@ -98,7 +99,7 @@ export var dbService = { /** * Fetches a session by ID. * @param {string|number} id - The session ID to fetch - * @returns {Promise} Promise that resolves to session object or null if not found + * @returns {Promise} Promise that resolves to session object or null if not found */ async fetchSessionById(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; @@ -128,7 +129,7 @@ export var dbService = { /** * Fetches a session by window ID. * @param {number} windowId - The window ID to search for - * @returns {Promise} Promise that resolves to session object or false if not found + * @returns {Promise} Promise that resolves to session object or false if not found */ async fetchSessionByWindowId(windowId) { try { @@ -144,7 +145,7 @@ export var dbService = { /** * Fetches a session by name. * @param {string} sessionName - The session name to search for - * @returns {Promise} Promise that resolves to session object or false if not found + * @returns {Promise} Promise that resolves to session object or false if not found */ async fetchSessionByName(sessionName) { try { @@ -167,7 +168,7 @@ export var dbService = { /** * Creates a new session in the database. - * @param {Object} session - The session object to create (id will be auto-generated) + * @param {Session} session - The session object to create (id will be auto-generated) * @returns {Promise} Promise that resolves to created session with ID or null if failed */ async createSession(session) { @@ -186,8 +187,8 @@ export var dbService = { /** * Updates an existing session in the database. - * @param {Object} session - The session object to update (must have valid id) - * @returns {Promise} Promise that resolves to updated session or null if failed + * @param {Session} session - The session object to update (must have valid id) + * @returns {Promise} Promise that resolves to updated session or null if failed */ async updateSession(session) { // ensure session id is set From e027816fdeda4381e0fd2fad000610bd6877ef32 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 09:24:11 -0700 Subject: [PATCH 35/92] Remove obsolete TODO --- js/background/spacesService.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index a99717d..8f52fe0 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -6,8 +6,6 @@ import { dbService } from './dbService.js'; -// TODO(codedread): Eliminate all global variables and use chrome.storage.local. - // eslint-disable-next-line no-var export var spacesService = { tabHistoryUrlMap: {}, From 93c606ce0308fadb856135517ae07e83a43ca00b Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 09:24:40 -0700 Subject: [PATCH 36/92] Remove ability for schema to be a function in db.js and a bit more modernization using Object.hasOwn() --- js/background/db.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index d39e34c..cfbb258 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -13,7 +13,6 @@ const transactionModes = { readwrite: 'readwrite', }; -const hasOwn = Object.prototype.hasOwnProperty; const defaultMapper = (value) => value; export class Server { @@ -454,19 +453,15 @@ var IndexQuery = function(table, db, indexName) { /** * Creates the database schema. * @param {Event} e - * @param {*} schema - * @param {*} db + * @param {object} schema The database schema object + * @param {IDBDatabase} db */ function createSchema(e, schema, db) { - if (typeof schema === 'function') { - schema = schema(); - } - for (var tableName in schema) { var table = schema[tableName]; var store; if ( - !hasOwn.call(schema, tableName) || + !Object.hasOwn(schema, tableName) || db.objectStoreNames.contains(tableName) ) { store = e.currentTarget.transaction.objectStore(tableName); @@ -512,7 +507,7 @@ const dbCache = {}; * @typedef {object} DbOpenOptions * @property {string} server The name of the database. * @property {number} version The version of the database. - * @property {object | function} schema The database schema. + * @property {object} schema The database schema. */ export const db = { From 6bdb9e60de78d7b45ea10ef8026f5f8d75607f1c Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 09:48:03 -0700 Subject: [PATCH 37/92] Turn DbSevice into a JS class for better readability --- js/background/dbService.js | 129 +++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 5a39a8a..8435c4d 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -4,64 +4,64 @@ import { db, Server } from './db.js'; /** @typedef {import('./common.js').Space} Space */ -// eslint-disable-next-line no-var -export var dbService = { - DB_SERVER: 'spaces', - DB_VERSION: '1', - DB_SESSIONS: 'ttSessions', +/** + * @typedef Session + * @property {number} id Auto-generated indexedDb object id + * @property {number} sessionHash A hash formed from the combined urls in the session window + * @property {string} name The saved name of the session + * @property {Array} tabs An array of chrome tab objects (often taken from the chrome window obj) + * @property {Array} history An array of chrome tab objects that have been removed from the session + * @property {Date} lastAccess Timestamp that gets updated with every window focus + */ + +/** + * Returns database schema definition. + * @returns {Object} Database schema configuration object + */ +function getSchema() { + return { + ttSessions: { + key: { + keyPath: 'id', + autoIncrement: true, + }, + indexes: { + id: {}, + }, + }, + }; +} + +// Database constants +const DB_SERVER = 'spaces'; +const DB_VERSION = '1'; +const DB_SESSIONS = 'ttSessions'; +class DbService { /** * Opens and returns a database connection. * @returns {Promise} Promise that resolves to database connection */ getDb() { return db.open({ - server: dbService.DB_SERVER, - version: dbService.DB_VERSION, - schema: dbService.getSchema(), + server: DB_SERVER, + version: DB_VERSION, + schema: getSchema(), }); - }, - - /** - * @typedef Session - * @property {number} id Auto-generated indexedDb object id - * @property {number} sessionHash A hash formed from the combined urls in the session window - * @property {string} name The saved name of the session - * @property {Array} tabs An array of chrome tab objects (often taken from the chrome window obj) - * @property {Array} history An array of chrome tab objects that have been removed from the session - * @property {Date} lastAccess Timestamp that gets updated with every window focus - */ - - /** - * Returns database schema definition. - * @returns {Object} Database schema configuration object - */ - getSchema() { - return { - ttSessions: { - key: { - keyPath: 'id', - autoIncrement: true, - }, - indexes: { - id: {}, - }, - }, - }; - }, + } /** * Fetches all sessions from the database. * @returns {Promise>} Promise that resolves to array of session objects */ _fetchAllSessions() { - return dbService.getDb().then(s => { + return this.getDb().then(s => { return s - .query(dbService.DB_SESSIONS) + .query(DB_SESSIONS) .all() .execute(); }); - }, + } /** * Fetches a session by ID from the database. @@ -69,9 +69,9 @@ export var dbService = { * @returns {Promise} Promise that resolves to session object or null if not found */ _fetchSessionById(id) { - return dbService.getDb().then(s => { + return this.getDb().then(s => { return s - .query(dbService.DB_SESSIONS, 'id') + .query(DB_SESSIONS, 'id') .only(id) .distinct() .desc() @@ -80,7 +80,7 @@ export var dbService = { return results.length > 0 ? results[0] : null; }); }); - }, + } /** * Fetches all sessions from the database. @@ -88,13 +88,13 @@ export var dbService = { */ async fetchAllSessions() { try { - const sessions = await dbService._fetchAllSessions(); + const sessions = await this._fetchAllSessions(); return sessions; } catch (error) { console.error('Error fetching all sessions:', error); return []; } - }, + } /** * Fetches a session by ID. @@ -104,13 +104,13 @@ export var dbService = { async fetchSessionById(id) { const _id = typeof id === 'string' ? parseInt(id, 10) : id; try { - const session = await dbService._fetchSessionById(_id); + const session = await this._fetchSessionById(_id); return session; } catch (error) { console.error('Error fetching session by ID:', error); return null; } - }, + } /** * Fetches all session names. @@ -118,13 +118,13 @@ export var dbService = { */ async fetchSessionNames() { try { - const sessions = await dbService._fetchAllSessions(); + const sessions = await this._fetchAllSessions(); return sessions.map(session => session.name); } catch (error) { console.error('Error fetching session names:', error); return []; } - }, + } /** * Fetches a session by window ID. @@ -133,14 +133,14 @@ export var dbService = { */ async fetchSessionByWindowId(windowId) { try { - const sessions = await dbService._fetchAllSessions(); + const sessions = await this._fetchAllSessions(); const matchedSession = sessions.find(session => session.windowId === windowId); return matchedSession || false; } catch (error) { console.error('Error fetching session by window ID:', error); return false; } - }, + } /** * Fetches a session by name. @@ -149,7 +149,7 @@ export var dbService = { */ async fetchSessionByName(sessionName) { try { - const sessions = await dbService._fetchAllSessions(); + const sessions = await this._fetchAllSessions(); let matchIndex; const matchFound = sessions.some((session, index) => { if (session.name?.toLowerCase() === sessionName.toLowerCase()) { @@ -164,7 +164,7 @@ export var dbService = { console.error('Error fetching session by name:', error); return false; } - }, + } /** * Creates a new session in the database. @@ -176,14 +176,14 @@ export var dbService = { const { id, ..._session } = session; try { - const s = await dbService.getDb(); - const result = await s.add(dbService.DB_SESSIONS, _session); + const s = await this.getDb(); + const result = await s.add(DB_SESSIONS, _session); return result.length > 0 ? result[0] : null; } catch (error) { console.error('Error creating session:', error); return null; } - }, + } /** * Updates an existing session in the database. @@ -197,14 +197,14 @@ export var dbService = { } try { - const s = await dbService.getDb(); - const result = await s.update(dbService.DB_SESSIONS, session); + const s = await this.getDb(); + const result = await s.update(DB_SESSIONS, session); return result.length > 0 ? result[0] : null; } catch (error) { console.error('Error updating session:', error); return null; } - }, + } /** * Removes a session from the database. @@ -215,12 +215,15 @@ export var dbService = { const _id = typeof id === 'string' ? parseInt(id, 10) : id; try { - const s = await dbService.getDb(); - await s.remove(dbService.DB_SESSIONS, _id); + const s = await this.getDb(); + await s.remove(DB_SESSIONS, _id); return true; } catch (error) { console.error('Error removing session:', error); return false; } - }, -}; + } +} + +// Export an instantiated object +export const dbService = new DbService(); From cfcba2c3b81a37c561436d18c13cc42d53bf5045 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 10:00:23 -0700 Subject: [PATCH 38/92] Fix indent in switcher.js --- js/switcher.js | 132 ++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/js/switcher.js b/js/switcher.js index 15351e7..5b9aa54 100644 --- a/js/switcher.js +++ b/js/switcher.js @@ -6,81 +6,81 @@ function getSelectedSpace() { return document.querySelector('.space.selected'); } - function handleSwitchAction(selectedSpaceEl) { - chrome.runtime.sendMessage({ - action: 'switchToSpace', - sessionId: selectedSpaceEl.getAttribute('data-sessionId'), - windowId: selectedSpaceEl.getAttribute('data-windowId'), - }); - } - - function handleCloseAction() { - chrome.runtime.sendMessage({ - action: 'requestClose', - }); - } +function handleSwitchAction(selectedSpaceEl) { + chrome.runtime.sendMessage({ + action: 'switchToSpace', + sessionId: selectedSpaceEl.getAttribute('data-sessionId'), + windowId: selectedSpaceEl.getAttribute('data-windowId'), + }); +} - async function getSwitchKeycodes(callback) { - const commands = await chrome.commands.getAll(); - const commandStr = commands.switchCode; - const keyStrArray = commandStr.split('+'); +function handleCloseAction() { + chrome.runtime.sendMessage({ + action: 'requestClose', + }); +} - // get keyStr of primary modifier - const primaryModifier = keyStrArray[0]; +async function getSwitchKeycodes(callback) { + const commands = await chrome.commands.getAll(); + const commandStr = commands.switchCode; + const keyStrArray = commandStr.split('+'); - // get keyStr of secondary modifier - const secondaryModifier = - keyStrArray.length === 3 ? keyStrArray[1] : false; + // get keyStr of primary modifier + const primaryModifier = keyStrArray[0]; - // get keycode of main key (last in array) - const curStr = keyStrArray[keyStrArray.length - 1]; - let mainKeyCode; + // get keyStr of secondary modifier + const secondaryModifier = + keyStrArray.length === 3 ? keyStrArray[1] : false; - // TODO: There's others. Period. Up Arrow etc. - if (curStr === 'Space') { - mainKeyCode = 32; - } else { - mainKeyCode = curStr.toUpperCase().charCodeAt(); - } + // get keycode of main key (last in array) + const curStr = keyStrArray[keyStrArray.length - 1]; + let mainKeyCode; - callback({ - primaryModifier, - secondaryModifier, - mainKeyCode, - }); + // TODO: There's others. Period. Up Arrow etc. + if (curStr === 'Space') { + mainKeyCode = 32; + } else { + mainKeyCode = curStr.toUpperCase().charCodeAt(); } - function addEventListeners() { - document.getElementById('spaceSelectForm').onsubmit = e => { - e.preventDefault(); - handleSwitchAction(getSelectedSpace()); - }; + callback({ + primaryModifier, + secondaryModifier, + mainKeyCode, + }); +} - const allSpaceEls = document.querySelectorAll('.space'); - Array.prototype.forEach.call(allSpaceEls, el => { - // eslint-disable-next-line no-param-reassign - el.onclick = () => { - handleSwitchAction(el); - }; - }); +function addEventListeners() { + document.getElementById('spaceSelectForm').onsubmit = e => { + e.preventDefault(); + handleSwitchAction(getSelectedSpace()); + }; - // Here lies some pretty hacky stuff. Yus! Hax! - getSwitchKeycodes(() => { - const body = document.querySelector('body'); + const allSpaceEls = document.querySelectorAll('.space'); + Array.prototype.forEach.call(allSpaceEls, el => { + // eslint-disable-next-line no-param-reassign + el.onclick = () => { + handleSwitchAction(el); + }; + }); - body.onkeyup = e => { - // listen for escape key - if (e.keyCode === 27) { - handleCloseAction(); - } - }; - }); - } + // Here lies some pretty hacky stuff. Yus! Hax! + getSwitchKeycodes(() => { + const body = document.querySelector('body'); - window.onload = () => { - chrome.runtime.sendMessage({ action: 'requestAllSpaces' }, spaces => { - spacesRenderer.initialise(8, true); - spacesRenderer.renderSpaces(spaces); - addEventListeners(); - }); - }; + body.onkeyup = e => { + // listen for escape key + if (e.keyCode === 27) { + handleCloseAction(); + } + }; + }); +} + +window.onload = () => { + chrome.runtime.sendMessage({ action: 'requestAllSpaces' }, spaces => { + spacesRenderer.initialise(8, true); + spacesRenderer.renderSpaces(spaces); + addEventListeners(); + }); +}; From 79fb1d1af930939a9ba0a4b5cb96dfbeb8073e21 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 10:01:03 -0700 Subject: [PATCH 39/92] Update jsdoc for private methods in dbService.js and rename a couple private methods to start with an underscore --- js/background/dbService.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/js/background/dbService.js b/js/background/dbService.js index 8435c4d..7488560 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -40,9 +40,10 @@ const DB_SESSIONS = 'ttSessions'; class DbService { /** * Opens and returns a database connection. + * @private * @returns {Promise} Promise that resolves to database connection */ - getDb() { + _getDb() { return db.open({ server: DB_SERVER, version: DB_VERSION, @@ -52,10 +53,11 @@ class DbService { /** * Fetches all sessions from the database. + * @private * @returns {Promise>} Promise that resolves to array of session objects */ _fetchAllSessions() { - return this.getDb().then(s => { + return this._getDb().then(s => { return s .query(DB_SESSIONS) .all() @@ -65,11 +67,12 @@ class DbService { /** * Fetches a session by ID from the database. + * @private * @param {number} id - The session ID to fetch * @returns {Promise} Promise that resolves to session object or null if not found */ _fetchSessionById(id) { - return this.getDb().then(s => { + return this._getDb().then(s => { return s .query(DB_SESSIONS, 'id') .only(id) @@ -113,10 +116,11 @@ class DbService { } /** - * Fetches all session names. + * Fetches all session names. Not used today. + * @private * @returns {Promise>} Promise that resolves to array of session names */ - async fetchSessionNames() { + async _fetchSessionNames() { try { const sessions = await this._fetchAllSessions(); return sessions.map(session => session.name); @@ -176,7 +180,7 @@ class DbService { const { id, ..._session } = session; try { - const s = await this.getDb(); + const s = await this._getDb(); const result = await s.add(DB_SESSIONS, _session); return result.length > 0 ? result[0] : null; } catch (error) { @@ -197,7 +201,7 @@ class DbService { } try { - const s = await this.getDb(); + const s = await this._getDb(); const result = await s.update(DB_SESSIONS, session); return result.length > 0 ? result[0] : null; } catch (error) { @@ -215,7 +219,7 @@ class DbService { const _id = typeof id === 'string' ? parseInt(id, 10) : id; try { - const s = await this.getDb(); + const s = await this._getDb(); await s.remove(DB_SESSIONS, _id); return true; } catch (error) { From 09e6e49898aa58dd513f2ef56f103bb46bd884fc Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 10:34:45 -0700 Subject: [PATCH 40/92] Use default parameter values for noop callbacks --- js/background/spacesService.js | 453 ++++++++++++++++----------------- 1 file changed, 217 insertions(+), 236 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 8f52fe0..994a7d9 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -6,46 +6,48 @@ import { dbService } from './dbService.js'; -// eslint-disable-next-line no-var -export var spacesService = { - tabHistoryUrlMap: {}, - closedWindowIds: {}, - sessions: [], - sessionUpdateTimers: {}, - historyQueue: [], - eventQueueCount: 0, - debug: false, - initialized: false, - initializationPromise: null, - - noop: () => {}, +// Module-level properties +const debug = false; +const noop = () => {}; + +class SpacesService { + constructor() { + this.tabHistoryUrlMap = {}; + this.closedWindowIds = {}; + this.sessions = []; + this.sessionUpdateTimers = {}; + this.historyQueue = []; + this.eventQueueCount = 0; + this.initialized = false; + this.initializationPromise = null; + } // Ensure spacesService is initialized before processing events async ensureInitialized() { - if (spacesService.initialized) { + if (this.initialized) { return; } - if (spacesService.initializationPromise) { - await spacesService.initializationPromise; + if (this.initializationPromise) { + await this.initializationPromise; return; } - spacesService.initializationPromise = spacesService.initialiseSpaces().then(() => { - spacesService.initialized = true; - spacesService.initializationPromise = null; + this.initializationPromise = this.initialiseSpaces().then(() => { + this.initialized = true; + this.initializationPromise = null; }); - await spacesService.initializationPromise; - }, + await this.initializationPromise; + } // initialise spaces - combine open windows with saved sessions async initialiseSpaces() { - spacesService.initialized = false; // Reset on re-initialization + this.initialized = false; // Reset on re-initialization // update version numbers - const lastVersion = await spacesService.fetchLastVersion(); - spacesService.setLastVersion(chrome.runtime.getManifest().version); + const lastVersion = await this.fetchLastVersion(); + this.setLastVersion(chrome.runtime.getManifest().version); try { const sessions = await dbService.fetchAllSessions(); @@ -54,27 +56,27 @@ export var spacesService = { chrome.runtime.getManifest().version === '0.18' && chrome.runtime.getManifest().version !== lastVersion ) { - await spacesService.resetAllSessionHashes(sessions); + await this.resetAllSessionHashes(sessions); } const windows = await chrome.windows.getAll({ populate: true }); // populate session map from database - spacesService.sessions = sessions; + this.sessions = sessions; // then try to match current open windows with saved sessions for (const curWindow of windows) { - if (!spacesService.filterInternalWindows(curWindow)) { - await spacesService.checkForSessionMatchDuringInit(curWindow); + if (!this.filterInternalWindows(curWindow)) { + await this.checkForSessionMatchDuringInit(curWindow); } } // Initialization complete - spacesService.initialized = true; + this.initialized = true; } catch (error) { console.error('Error initializing spaces:', error); - spacesService.initialized = false; + this.initialized = false; } - }, + } // Clear windowId associations after Chrome restart (when window IDs get reassigned) async clearWindowIdAssociations() { @@ -91,8 +93,8 @@ export var spacesService = { } // Also clear from in-memory cache if it's already loaded - if (spacesService.sessions && spacesService.sessions.length > 0) { - for (const session of spacesService.sessions) { + if (this.sessions && this.sessions.length > 0) { + for (const session of this.sessions) { if (session.windowId) { session.windowId = false; } @@ -101,86 +103,26 @@ export var spacesService = { } catch (error) { console.error('Error clearing window ID associations:', error); } - }, + } async resetAllSessionHashes(sessions) { for (const session of sessions) { // eslint-disable-next-line no-param-reassign - session.sessionHash = spacesService.generateSessionHash( + session.sessionHash = generateSessionHash( session.tabs ); await dbService.updateSession(session); } - }, + } // record each tab's id and url so we can add history items when tabs are removed initialiseTabHistory() { chrome.tabs.query({}, tabs => { tabs.forEach(async tab => { - spacesService.tabHistoryUrlMap[tab.id] = tab.url; + this.tabHistoryUrlMap[tab.id] = tab.url; }); }); - }, - - // NOTE: if ever changing this function, then we'll need to update all - // saved sessionHashes so that they match next time, using: resetAllSessionHashes() - _cleanUrl(url) { - if (!url) { - return ''; - } - - // ignore urls from this extension - if (url.indexOf(chrome.runtime.id) >= 0) { - return ''; - } - - // ignore 'new tab' pages - if (url.indexOf('chrome:// newtab/') >= 0) { - return ''; - } - - let cleanUrl = url; - - // add support for 'The Great Suspender' - if ( - cleanUrl.indexOf('suspended.html') > 0 && - cleanUrl.indexOf('uri=') > 0 - ) { - cleanUrl = cleanUrl.substring( - cleanUrl.indexOf('uri=') + 4, - cleanUrl.length - ); - } - - // remove any text after a '#' symbol - if (cleanUrl.indexOf('#') > 0) { - cleanUrl = cleanUrl.substring(0, cleanUrl.indexOf('#')); - } - - // remove any text after a '?' symbol - if (cleanUrl.indexOf('?') > 0) { - cleanUrl = cleanUrl.substring(0, cleanUrl.indexOf('?')); - } - - return cleanUrl; - }, - - generateSessionHash(tabs) { - const text = tabs.reduce((prevStr, tab) => { - return prevStr + spacesService._cleanUrl(tab.url); - }, ''); - - let hash = 0; - if (text.length === 0) return hash; - for (let i = 0, len = text.length; i < len; i += 1) { - const chr = text.charCodeAt(i); - // eslint-disable-next-line no-bitwise - hash = (hash << 5) - hash + chr; - // eslint-disable-next-line no-bitwise - hash |= 0; // Convert to 32bit integer - } - return Math.abs(hash); - }, + } filterInternalWindows(curWindow) { // sanity check to make sure window isnt an internal spaces window @@ -196,7 +138,7 @@ export var spacesService = { return true; } return false; - }, + } async checkForSessionMatchDuringInit(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { @@ -212,7 +154,7 @@ export var spacesService = { } if (existingSession) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `existing session found for windowId: ${curWindow.id}. session: ${existingSession.id || 'temporary'}` @@ -223,15 +165,15 @@ export var spacesService = { } // If no existing session, fall back to hash matching (Chrome restart case) - await spacesService.checkForSessionMatch(curWindow); - }, + await this.checkForSessionMatch(curWindow); + } async checkForSessionMatch(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; } - const sessionHash = spacesService.generateSessionHash(curWindow.tabs); + const sessionHash = generateSessionHash(curWindow.tabs); const temporarySession = await dbService.fetchSessionByWindowId( curWindow.id ); @@ -250,19 +192,19 @@ export var spacesService = { } if (matchingSession) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `matching session found: ${matchingSession.id}. linking with window: ${curWindow.id}` ); } - spacesService.matchSessionToWindow(matchingSession, curWindow); + this.matchSessionToWindow(matchingSession, curWindow); } // if no match found and this window does not already have a temporary session if (!matchingSession && !temporarySession) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `no matching session found. creating temporary session for window: ${curWindow.id}` @@ -270,22 +212,22 @@ export var spacesService = { } // create a new temporary session for this window (with no sessionId or name) - spacesService.createTemporaryUnmatchedSession(curWindow); + this.createTemporaryUnmatchedSession(curWindow); } - }, + } async matchSessionToWindow(session, curWindow) { - await spacesService.ensureInitialized(); + await this.ensureInitialized(); // remove any other sessions tied to this windowId (temporary sessions) - for (let i = spacesService.sessions.length - 1; i >= 0; i -= 1) { - if (spacesService.sessions[i].windowId === curWindow.id) { - if (spacesService.sessions[i].id) { - spacesService.sessions[i].windowId = false; + for (let i = this.sessions.length - 1; i >= 0; i -= 1) { + if (this.sessions[i].windowId === curWindow.id) { + if (this.sessions[i].id) { + this.sessions[i].windowId = false; // Persist the cleared windowId to database - await dbService.updateSession(spacesService.sessions[i]); + await dbService.updateSession(this.sessions[i]); } else { - spacesService.sessions.splice(i, 1); + this.sessions.splice(i, 1); } } } @@ -298,21 +240,21 @@ export var spacesService = { if (session.id) { await dbService.updateSession(session); } - }, + } async createTemporaryUnmatchedSession(curWindow) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console - console.dir(spacesService.sessions); + console.dir(this.sessions); // eslint-disable-next-line no-console console.dir(curWindow); // eslint-disable-next-line no-alert // alert('couldnt match window. creating temporary session'); } - const sessionHash = spacesService.generateSessionHash(curWindow.tabs); + const sessionHash = generateSessionHash(curWindow.tabs); - spacesService.sessions.push({ + this.sessions.push({ id: false, windowId: curWindow.id, sessionHash, @@ -321,7 +263,7 @@ export var spacesService = { history: [], lastAccess: new Date(), }); - }, + } // local storage getters/setters async fetchLastVersion() { @@ -331,20 +273,20 @@ export var spacesService = { return version; } return 0; - }, + } setLastVersion(newVersion) { chrome.storage.local.set({'spacesVersion': JSON.stringify(newVersion)}); - }, + } // event listener functions for window and tab events // (events are received and screened first in background.js) // ----------------------------------------------------------------------------------------- async handleTabRemoved(tabId, removeInfo, callback) { - await spacesService.ensureInitialized(); + await this.ensureInitialized(); - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `handlingTabRemoved event. windowId: ${removeInfo.windowId}` @@ -359,52 +301,51 @@ export var spacesService = { // as part of the session (effectively corrupting the session) // should be handled by the window removed listener - spacesService.handleWindowRemoved( + this.handleWindowRemoved( removeInfo.windowId, - true, - spacesService.noop + true ); // if this is a legitimate single tab removal from a window then update session/window } else { - spacesService.historyQueue.push({ - url: spacesService.tabHistoryUrlMap[tabId], + this.historyQueue.push({ + url: this.tabHistoryUrlMap[tabId], windowId: removeInfo.windowId, action: 'add', }); - spacesService.queueWindowEvent( + this.queueWindowEvent( removeInfo.windowId, - spacesService.eventQueueCount, + this.eventQueueCount, callback ); // remove tab from tabHistoryUrlMap - delete spacesService.tabHistoryUrlMap[tabId]; + delete this.tabHistoryUrlMap[tabId]; } - }, + } async handleTabMoved(tabId, moveInfo, callback) { - await spacesService.ensureInitialized(); + await this.ensureInitialized(); - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `handlingTabMoved event. windowId: ${moveInfo.windowId}` ); } - spacesService.queueWindowEvent( + this.queueWindowEvent( moveInfo.windowId, - spacesService.eventQueueCount, + this.eventQueueCount, callback ); - }, + } async handleTabUpdated(tab, changeInfo, callback) { - await spacesService.ensureInitialized(); + await this.ensureInitialized(); // NOTE: only queue event when tab has completed loading (title property exists at this point) if (tab.status === 'complete') { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `handlingTabUpdated event. windowId: ${tab.windowId}` @@ -412,10 +353,10 @@ export var spacesService = { } // update tab history in case the tab url has changed - spacesService.tabHistoryUrlMap[tab.id] = tab.url; - spacesService.queueWindowEvent( + this.tabHistoryUrlMap[tab.id] = tab.url; + this.queueWindowEvent( tab.windowId, - spacesService.eventQueueCount, + this.eventQueueCount, callback ); } @@ -423,23 +364,23 @@ export var spacesService = { // check for change in tab url. if so, update history if (changeInfo.url) { // add tab to history queue as an item to be removed (as it is open for this window) - spacesService.historyQueue.push({ + this.historyQueue.push({ url: changeInfo.url, windowId: tab.windowId, action: 'remove', }); } - }, + } - async handleWindowRemoved(windowId, markAsClosed, callback) { - await spacesService.ensureInitialized(); + async handleWindowRemoved(windowId, markAsClosed, callback = noop) { + await this.ensureInitialized(); // ignore subsequent windowRemoved events for the same windowId (each closing tab will try to call this) - if (spacesService.closedWindowIds[windowId]) { + if (this.closedWindowIds[windowId]) { callback(); } - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log(`handlingWindowRemoved event. windowId: ${windowId}`); } @@ -447,13 +388,13 @@ export var spacesService = { // add windowId to closedWindowIds. the idea is that once a window is closed it can never be // rematched to a new session (hopefully these window ids never get legitimately re-used) if (markAsClosed) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log(`adding window to closedWindowIds: ${windowId}`); } - spacesService.closedWindowIds[windowId] = true; - clearTimeout(spacesService.sessionUpdateTimers[windowId]); + this.closedWindowIds[windowId] = true; + clearTimeout(this.sessionUpdateTimers[windowId]); } const session = await dbService.fetchSessionByWindowId(windowId); @@ -466,9 +407,9 @@ export var spacesService = { // else if it is temporary session then remove the session from the cache } else { - spacesService.sessions.some((curSession, index) => { + this.sessions.some((curSession, index) => { if (curSession.windowId === windowId) { - spacesService.sessions.splice(index, 1); + this.sessions.splice(index, 1); return true; } return false; @@ -477,12 +418,12 @@ export var spacesService = { } callback(); - }, + } async handleWindowFocussed(windowId) { - await spacesService.ensureInitialized(); + await this.ensureInitialized(); - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log(`handleWindowFocussed event. windowId: ${windowId}`); } @@ -495,29 +436,25 @@ export var spacesService = { if (session) { session.lastAccess = new Date(); } - }, + } // 1sec timer-based batching system. // Set a timeout so that multiple tabs all opened at once (like when restoring a session) // only trigger this function once (as per the timeout set by the last tab event) // This will cause multiple triggers if time between tab openings is longer than 1 sec queueWindowEvent(windowId, eventId, callback) { - clearTimeout(spacesService.sessionUpdateTimers[windowId]); + clearTimeout(this.sessionUpdateTimers[windowId]); - spacesService.eventQueueCount += 1; + this.eventQueueCount += 1; - spacesService.sessionUpdateTimers[windowId] = setTimeout(() => { - spacesService.handleWindowEvent(windowId, eventId, callback); + this.sessionUpdateTimers[windowId] = setTimeout(() => { + this.handleWindowEvent(windowId, eventId, callback); }, 1000); - }, + } // careful here as this function gets called A LOT - async handleWindowEvent(windowId, eventId, callback) { - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; - - if (spacesService.debug) { + async handleWindowEvent(windowId, eventId, callback = noop) { + if (debug) { // eslint-disable-next-line no-console console.log('------------------------------------------------'); @@ -529,7 +466,7 @@ export var spacesService = { // sanity check windowId if (!windowId || windowId <= 0) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `received an event for windowId: ${windowId} which is obviously wrong` @@ -550,21 +487,20 @@ export var spacesService = { // if we can't find this window, then better remove references to it from the cached sessions // don't mark as a removed window however, so that the space can be resynced up if the window // does actually still exist (for some unknown reason) - spacesService.handleWindowRemoved( + this.handleWindowRemoved( windowId, - false, - spacesService.noop + false ); return; } - if (!curWindow || spacesService.filterInternalWindows(curWindow)) { + if (!curWindow || this.filterInternalWindows(curWindow)) { return; } // don't allow event if it pertains to a closed window id - if (spacesService.closedWindowIds[windowId]) { - if (spacesService.debug) { + if (this.closedWindowIds[windowId]) { + if (debug) { // eslint-disable-next-line no-console console.log( `ignoring event as it pertains to a closed windowId: ${windowId}` @@ -577,7 +513,7 @@ export var spacesService = { const session = await dbService.fetchSessionByWindowId(windowId); if (session) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log( `tab statuses: ${curWindow.tabs @@ -589,7 +525,7 @@ export var spacesService = { } // look for tabs recently added/removed from this session and update session history - const historyItems = spacesService.historyQueue.filter( + const historyItems = this.historyQueue.filter( historyItem => { return historyItem.windowId === windowId; } @@ -599,28 +535,28 @@ export var spacesService = { const historyItem = historyItems[i]; if (historyItem.action === 'add') { - spacesService.addUrlToSessionHistory( + this.addUrlToSessionHistory( session, historyItem.url ); } else if (historyItem.action === 'remove') { - spacesService.removeUrlFromSessionHistory( + this.removeUrlFromSessionHistory( session, historyItem.url ); } - spacesService.historyQueue.splice(i, 1); + this.historyQueue.splice(i, 1); } // override session tabs with tabs from window session.tabs = curWindow.tabs; - session.sessionHash = spacesService.generateSessionHash( + session.sessionHash = generateSessionHash( session.tabs ); // if it is a saved session then update db if (session.id) { - spacesService.saveExistingSession(session, callback); + this.saveExistingSession(session, callback); } } @@ -628,33 +564,33 @@ export var spacesService = { // if session found without session.id then it must be a temporary session // check for sessionMatch if (!session || !session.id) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log('session check triggered'); } - spacesService.checkForSessionMatch(curWindow); + this.checkForSessionMatch(curWindow); } callback(); - }, + } // PUBLIC FUNCTIONS addUrlToSessionHistory(session, newUrl) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log(`adding tab to history: ${newUrl}`); } - const cleanUrl = spacesService._cleanUrl(newUrl); + const cleanUrlResult = cleanUrl(newUrl); - if (cleanUrl.length === 0) { + if (cleanUrlResult.length === 0) { return false; } // don't add removed tab to history if there is still a tab open with same url // note: assumes tab has NOT already been removed from session.tabs const tabBeingRemoved = session.tabs.filter(curTab => { - return spacesService._cleanUrl(curTab.url) === cleanUrl; + return cleanUrl(curTab.url) === cleanUrlResult; }); if (tabBeingRemoved.length !== 1) { @@ -666,7 +602,7 @@ export var spacesService = { // see if tab already exists in history. if so then remove it (it will be re-added) session.history.some((historyTab, index) => { - if (spacesService._cleanUrl(historyTab.url) === cleanUrl) { + if (cleanUrl(historyTab.url) === cleanUrlResult) { session.history.splice(index, 1); return true; } @@ -682,16 +618,16 @@ export var spacesService = { session.history = session.history.slice(0, 200); return session; - }, + } removeUrlFromSessionHistory(session, newUrl) { - if (spacesService.debug) { + if (debug) { // eslint-disable-next-line no-console console.log(`removing tab from history: ${newUrl}`); } // eslint-disable-next-line no-param-reassign - newUrl = spacesService._cleanUrl(newUrl); + newUrl = cleanUrl(newUrl); if (newUrl.length === 0) { return; @@ -699,46 +635,34 @@ export var spacesService = { // see if tab already exists in history. if so then remove it session.history.some((historyTab, index) => { - if (spacesService._cleanUrl(historyTab.url) === newUrl) { + if (cleanUrl(historyTab.url) === newUrl) { session.history.splice(index, 1); return true; } return false; }); - }, + } // Database actions - async updateSessionTabs(sessionId, tabs, callback) { + async updateSessionTabs(sessionId, tabs, callback = noop) { const session = await dbService.fetchSessionById(sessionId); - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; - // update tabs in session session.tabs = tabs; - session.sessionHash = spacesService.generateSessionHash(session.tabs); - - spacesService.saveExistingSession(session, callback); - }, + session.sessionHash = generateSessionHash(session.tabs); - async updateSessionName(sessionId, sessionName, callback) { - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; + this.saveExistingSession(session, callback); + } + async updateSessionName(sessionId, sessionName, callback = noop) { const session = await dbService.fetchSessionById(sessionId); session.name = sessionName; - spacesService.saveExistingSession(session, callback); - }, - - async saveExistingSession(session, callback) { - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; + this.saveExistingSession(session, callback); + } + async saveExistingSession(session, callback = noop) { try { const updatedSession = await dbService.updateSession(session); callback(updatedSession); @@ -746,23 +670,19 @@ export var spacesService = { console.error('Error saving existing session:', error); callback(null); } - }, + } - async saveNewSession(sessionName, tabs, windowId, callback) { - await spacesService.ensureInitialized(); + async saveNewSession(sessionName, tabs, windowId, callback = noop) { + await this.ensureInitialized(); if (!tabs) { callback(); return; } - const sessionHash = spacesService.generateSessionHash(tabs); + const sessionHash = generateSessionHash(tabs); let session; - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; - // check for a temporary session with this windowId if (windowId) { session = await dbService.fetchSessionByWindowId(windowId); @@ -774,7 +694,7 @@ export var spacesService = { windowId, history: [], }; - spacesService.sessions.push(session); + this.sessions.push(session); } // update temporary session details @@ -798,20 +718,16 @@ export var spacesService = { console.error('Error creating session:', error); callback(false); } - }, - - async deleteSession(sessionId, callback) { - // eslint-disable-next-line no-param-reassign - callback = - typeof callback !== 'function' ? spacesService.noop : callback; + } + async deleteSession(sessionId, callback = noop) { try { const success = await dbService.removeSession(sessionId); if (success) { // remove session from cached array - spacesService.sessions.some((session, index) => { + this.sessions.some((session, index) => { if (session.id === sessionId) { - spacesService.sessions.splice(index, 1); + this.sessions.splice(index, 1); return true; } return false; @@ -822,5 +738,70 @@ export var spacesService = { console.error('Error deleting session:', error); callback(false); } - }, -}; + } +} + +// Module-level helper functions. + +// NOTE: if ever changing this function, then we'll need to update all +// saved sessionHashes so that they match next time, using: resetAllSessionHashes() +function cleanUrl(url) { + if (!url) { + return ''; + } + + // ignore urls from this extension + if (url.indexOf(chrome.runtime.id) >= 0) { + return ''; + } + + // ignore 'new tab' pages + if (url.indexOf('chrome:// newtab/') >= 0) { + return ''; + } + + let processedUrl = url; + + // add support for 'The Great Suspender' + if ( + processedUrl.indexOf('suspended.html') > 0 && + processedUrl.indexOf('uri=') > 0 + ) { + processedUrl = processedUrl.substring( + processedUrl.indexOf('uri=') + 4, + processedUrl.length + ); + } + + // remove any text after a '#' symbol + if (processedUrl.indexOf('#') > 0) { + processedUrl = processedUrl.substring(0, processedUrl.indexOf('#')); + } + + // remove any text after a '?' symbol + if (processedUrl.indexOf('?') > 0) { + processedUrl = processedUrl.substring(0, processedUrl.indexOf('?')); + } + + return processedUrl; +} + +function generateSessionHash(tabs) { + const text = tabs.reduce((prevStr, tab) => { + return prevStr + cleanUrl(tab.url); + }, ''); + + let hash = 0; + if (text.length === 0) return hash; + for (let i = 0, len = text.length; i < len; i += 1) { + const chr = text.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = (hash << 5) - hash + chr; + // eslint-disable-next-line no-bitwise + hash |= 0; // Convert to 32bit integer + } + return Math.abs(hash); +} + +// Export an instance of the SpacesService class +export const spacesService = new SpacesService(); From 63a5b74ee210030fb11b6ea84535a4fe0638f21e Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 11:02:50 -0700 Subject: [PATCH 41/92] jsdoc for helper functions in spacesService.js --- js/background/spacesService.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 994a7d9..2c91fe2 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -743,8 +743,22 @@ class SpacesService { // Module-level helper functions. -// NOTE: if ever changing this function, then we'll need to update all -// saved sessionHashes so that they match next time, using: resetAllSessionHashes() +/** + * Cleans and normalizes a URL by removing query parameters, fragments, and filtering out + * internal Chrome extension URLs and new tab pages. Also handles special cases like + * 'The Great Suspender' extension URLs. + * + * NOTE: if ever changing this function, then we'll need to update all + * saved sessionHashes so that they match next time, using: resetAllSessionHashes() + * + * @param {string} url - The URL to clean and normalize + * @returns {string} The cleaned URL, or empty string if URL should be ignored + * + * @example + * cleanUrl('https://example.com/page?param=value#section') // returns 'https://example.com/page' + * cleanUrl('chrome://newtab/') // returns '' + * cleanUrl('chrome-extension://abc123/page.html') // returns '' + */ function cleanUrl(url) { if (!url) { return ''; @@ -786,6 +800,21 @@ function cleanUrl(url) { return processedUrl; } +/** + * Generates a unique hash for a browser session based on the URLs of its tabs. + * This hash is used to match existing sessions when windows are reopened after Chrome restart. + * The hash is created by concatenating all cleaned tab URLs and applying a 32-bit hash algorithm. + * + * @param {Array} tabs - Array of tab objects, each containing a 'url' property + * @returns {number} A positive 32-bit integer hash representing the session + * + * @example + * const tabs = [ + * { url: 'https://example.com' }, + * { url: 'https://google.com' } + * ]; + * generateSessionHash(tabs) // returns something like 1234567890 + */ function generateSessionHash(tabs) { const text = tabs.reduce((prevStr, tab) => { return prevStr + cleanUrl(tab.url); From 8e79e42a93f8a0ca7106a30b9e8f07ceec047fda Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 11:35:31 -0700 Subject: [PATCH 42/92] Use async version of chrome.tabs.query() and make initialiseSpaces() private --- js/background/background.js | 10 +++------- js/background/spacesService.js | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 7f2bba3..d6bfe4b 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -559,9 +559,7 @@ chrome.runtime.onStartup.addListener(async () => { async function generatePopupParams(action, tabUrl) { // get currently highlighted tab - const tabs = await new Promise(resolve => { - chrome.tabs.query({ active: true, currentWindow: true }, resolve); - }); + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs.length === 0) return ''; const activeTab = tabs[0]; @@ -874,9 +872,8 @@ async function requestSessionPresence(sessionName) { chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false}); }); - chrome.tabs.query({windowId: newWindow.id, index: 0}, function (tabs) { - chrome.tabs.remove(tabs[0].id); - }); */ + const tabs = await chrome.tabs.query({windowId: newWindow.id, index: 0}); + chrome.tabs.remove(tabs[0].id); */ } } @@ -1135,4 +1132,3 @@ function moveTabToWindow(tab, windowId, callback) { console.log(`Initializing spacesService...`); spacesService.initialiseSpaces(); -spacesService.initialiseTabHistory(); diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 2c91fe2..af4bc14 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -33,7 +33,8 @@ class SpacesService { return; } - this.initializationPromise = this.initialiseSpaces().then(() => { + this.initializationPromise = this.initialiseSpaces().then(async () => { + await this._initialiseTabHistory(); this.initialized = true; this.initializationPromise = null; }); @@ -115,13 +116,15 @@ class SpacesService { } } - // record each tab's id and url so we can add history items when tabs are removed - initialiseTabHistory() { - chrome.tabs.query({}, tabs => { - tabs.forEach(async tab => { - this.tabHistoryUrlMap[tab.id] = tab.url; - }); - }); + /** + * Record each tab's id and url so we can add history items when tabs are removed + * @private + */ + async _initialiseTabHistory() { + const tabs = await chrome.tabs.query({}); + for (const tab of tabs) { + this.tabHistoryUrlMap[tab.id] = tab.url; + } } filterInternalWindows(curWindow) { From dc8e819780954c1fb3a02c1d11f51abf1fd40385 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 11:46:54 -0700 Subject: [PATCH 43/92] Change chrome.tabs.get() usage to use the async variety instead of the callback one. --- js/background/background.js | 138 ++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 60 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index d6bfe4b..9cfa155 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -327,7 +327,7 @@ chrome.runtime.onStartup.addListener(async () => { case 'requestTabDetail': tabId = _cleanParameter(request.tabId); if (tabId) { - requestTabDetail(tabId, tab => { + requestTabDetail(tabId).then(tab => { if (tab) { sendResponse(tab); } else { @@ -719,16 +719,23 @@ async function requestSessionPresence(sessionName) { return { exists: !!session, isOpen: !!session && !!session.windowId }; } - function requestTabDetail(tabId, callback) { - chrome.tabs.get(tabId, callback); +/** + * @param {number} tabId - The ID of the tab to retrieve details for + * @returns {Promise} A Promise that resolves to the tab object or null. + */ +async function requestTabDetail(tabId) { + try { + return await chrome.tabs.get(tabId); + } catch (error) { + return null; } +} - function requestCurrentSpace(callback) { - console.log(`codedread: requestCurrentSpace() called`); - chrome.windows.getCurrent(window => { - requestSpaceFromWindowId(window.id, callback); - }); - } +function requestCurrentSpace(callback) { + chrome.windows.getCurrent(window => { + requestSpaceFromWindowId(window.id, callback); + }); +} // returns a 'space' object which is essentially the same as a session object // except that includes space.sessionId (session.id) and space.windowId @@ -1025,29 +1032,33 @@ async function requestSessionPresence(sessionName) { } } - function handleMoveTabToNewSession(tabId, sessionName, callback) { - requestTabDetail(tabId, async tab => { - const session = await dbService.fetchSessionByName(sessionName); + async function handleMoveTabToNewSession(tabId, sessionName, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; + } - // if we found a session matching this name then return as an error as we are - // supposed to be creating a new session with this name - if (session) { - callback(false); + const session = await dbService.fetchSessionByName(sessionName); - // else create a new session with this name containing this tab - } else { - // remove tab from current window (should generate window events) - chrome.tabs.remove(tab.id); - - // save session to database - spacesService.saveNewSession( - sessionName, - [tab], - false, - callback - ); - } - }); + // if we found a session matching this name then return as an error as we are + // supposed to be creating a new session with this name + if (session) { + callback(false); + + // else create a new session with this name containing this tab + } else { + // remove tab from current window (should generate window events) + chrome.tabs.remove(tab.id); + + // save session to database + spacesService.saveNewSession( + sessionName, + [tab], + false, + callback + ); + } } async function handleAddLinkToSession(url, sessionId, callback) { @@ -1082,41 +1093,48 @@ async function requestSessionPresence(sessionName) { callback(true); } - function handleMoveTabToSession(tabId, sessionId, callback) { - requestTabDetail(tabId, async tab => { - const session = await dbService.fetchSessionById(sessionId); - const newTabs = [tab]; - - // if we have not found a session matching this name then return as an error as we are - // supposed to be adding the tab to an existing session - if (!session) { - callback(false); - } else { - // if session is currently open then move it directly - if (session.windowId) { - moveTabToWindow(tab, session.windowId, callback); - return; - } + async function handleMoveTabToSession(tabId, sessionId, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; + } - // else add tab to saved session in database - // remove tab from current window - chrome.tabs.remove(tab.id); + const session = await dbService.fetchSessionById(sessionId); + const newTabs = [tab]; - // update session in db - session.tabs = session.tabs.concat(newTabs); - spacesService.updateSessionTabs( - session.id, - session.tabs, - callback - ); + // if we have not found a session matching this name then return as an error as we are + // supposed to be adding the tab to an existing session + if (!session) { + callback(false); + } else { + // if session is currently open then move it directly + if (session.windowId) { + moveTabToWindow(tab, session.windowId, callback); + return; } - }); + + // else add tab to saved session in database + // remove tab from current window + chrome.tabs.remove(tab.id); + + // update session in db + session.tabs = session.tabs.concat(newTabs); + spacesService.updateSessionTabs( + session.id, + session.tabs, + callback + ); + } } - function handleMoveTabToWindow(tabId, windowId, callback) { - requestTabDetail(tabId, tab => { - moveTabToWindow(tab, windowId, callback); - }); + async function handleMoveTabToWindow(tabId, windowId, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; + } + moveTabToWindow(tab, windowId, callback); } function moveTabToWindow(tab, windowId, callback) { From bbf62b2eb684e77076eda64293d224bbc3e13b77 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 12:07:37 -0700 Subject: [PATCH 44/92] Remove callback from requestSpaceFromWindowId() and make it async --- js/background/background.js | 76 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 9cfa155..8938c42 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -200,7 +200,9 @@ chrome.runtime.onStartup.addListener(async () => { case 'requestSpaceFromWindowId': windowId = _cleanParameter(request.windowId); if (windowId) { - requestSpaceFromWindowId(windowId, sendResponse); + requestSpaceFromWindowId(windowId).then(space => { + sendResponse(space); + }); } return true; @@ -309,7 +311,9 @@ chrome.runtime.onStartup.addListener(async () => { if (checkInternalSpacesWindows(windowId, false)) { sendResponse(false); } else { - requestSpaceFromWindowId(windowId, sendResponse); + requestSpaceFromWindowId(windowId).then(space => { + sendResponse(space); + }); } } else if (sessionId) { requestSpaceFromSessionId(sessionId, sendResponse); @@ -731,47 +735,49 @@ async function requestTabDetail(tabId) { } } -function requestCurrentSpace(callback) { - chrome.windows.getCurrent(window => { - requestSpaceFromWindowId(window.id, callback); - }); +async function requestCurrentSpace(callback) { + const window = await chrome.windows.getCurrent(); + const space = await requestSpaceFromWindowId(window.id); + callback(space); } - // returns a 'space' object which is essentially the same as a session object - // except that includes space.sessionId (session.id) and space.windowId - async function requestSpaceFromWindowId(windowId, callback) { - // first check for an existing session matching this windowId - const session = await dbService.fetchSessionByWindowId(windowId); +/** + * @param {number} windowId + * @returns {Promise} + */ +async function requestSpaceFromWindowId(windowId) { + // first check for an existing session matching this windowId + const session = await dbService.fetchSessionByWindowId(windowId); - if (session) { + if (session) { + /** @type {Space} */ + const space = { + sessionId: session.id, + windowId: session.windowId, + name: session.name, + tabs: session.tabs, + history: session.history, + }; + return space; + + // otherwise build a space object out of the actual window + } else { + try { + const window = await chrome.windows.get(windowId, { populate: true }); /** @type {Space} */ const space = { - sessionId: session.id, - windowId: session.windowId, - name: session.name, - tabs: session.tabs, - history: session.history, + sessionId: false, + windowId: window.id, + name: false, + tabs: window.tabs, + history: false, }; - callback(space); - - // otherwise build a space object out of the actual window - } else { - try { - const window = await chrome.windows.get(windowId, { populate: true }); - /** @type {Space} */ - const space = { - sessionId: false, - windowId: window.id, - name: false, - tabs: window.tabs, - history: false, - }; - callback(space); - } catch (e) { - callback(false); - } + return space; + } catch (e) { + return false; } } +} async function requestSpaceFromSessionId(sessionId, callback) { const session = await dbService.fetchSessionById(sessionId); From e8c3301b915a6caa4992417971bc0e4f899819d0 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 12:09:44 -0700 Subject: [PATCH 45/92] Update requestCurrentSpace() to not use a callback. --- js/background/background.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 8938c42..cde526a 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -207,7 +207,9 @@ chrome.runtime.onStartup.addListener(async () => { return true; case 'requestCurrentSpace': - requestCurrentSpace(sendResponse); + requestCurrentSpace().then(space => { + sendResponse(space); + }); return true; case 'generatePopupParams': @@ -735,10 +737,13 @@ async function requestTabDetail(tabId) { } } -async function requestCurrentSpace(callback) { +/** + * Requests the current space based on the current window. + * @returns {Promise} + */ +async function requestCurrentSpace() { const window = await chrome.windows.getCurrent(); - const space = await requestSpaceFromWindowId(window.id); - callback(space); + return await requestSpaceFromWindowId(window.id); } /** From 84fde29612f8e02b12a2fec0dd79a91d56046844 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 12:28:36 -0700 Subject: [PATCH 46/92] Update extension to use the async version of chrome.runtime.sendMessage() consistently --- js/popup.js | 186 +++++++++++++++++++++++-------------------------- js/switcher.js | 11 ++- 2 files changed, 93 insertions(+), 104 deletions(-) diff --git a/js/popup.js b/js/popup.js index 0cf4d7e..d798a4b 100644 --- a/js/popup.js +++ b/js/popup.js @@ -138,11 +138,10 @@ import { utils } from './utils.js'; document .querySelector('#switcherLink .optionText') .addEventListener('click', async () => { - chrome.runtime.sendMessage({'action': 'switch',}).then(params => { - if (!params) return; - window.location.hash = params; - window.location.reload(); - }); + const params = await chrome.runtime.sendMessage({'action': 'switch'}); + if (!params) return; + window.location.hash = params; + window.location.reload(); renderSwitchCard(); }); document @@ -204,54 +203,48 @@ import { utils } from './utils.js'; } if (globalCurrentSpace.sessionId) { - chrome.runtime.sendMessage( - { - action: 'updateSessionName', - deleteOld: true, - sessionName: newName, - sessionId: globalCurrentSpace.sessionId, - }, - () => {} - ); + chrome.runtime.sendMessage({ + action: 'updateSessionName', + deleteOld: true, + sessionName: newName, + sessionId: globalCurrentSpace.sessionId, + }); } else { - chrome.runtime.sendMessage( - { - action: 'saveNewSession', - deleteOld: true, - sessionName: newName, - windowId: globalCurrentSpace.windowId, - }, - () => {} - ); + chrome.runtime.sendMessage({ + action: 'saveNewSession', + deleteOld: true, + sessionName: newName, + windowId: globalCurrentSpace.windowId, + }); } } - /* - * SWITCHER VIEW - */ - - function renderSwitchCard() { - document.getElementById( - 'popupContainer' - ).innerHTML = document.getElementById('switcherTemplate').innerHTML; - chrome.runtime.sendMessage({ action: 'requestAllSpaces' }, spaces => { - spacesRenderer.initialise(8, true); - spacesRenderer.renderSpaces(spaces); - - document.getElementById('spaceSelectForm').onsubmit = e => { - e.preventDefault(); - handleSwitchAction(getSelectedSpace()); - }; - - const allSpaceEls = document.querySelectorAll('.space'); - Array.prototype.forEach.call(allSpaceEls, el => { - // eslint-disable-next-line no-param-reassign - el.onclick = () => { - handleSwitchAction(el); - }; - }); - }); - } +/* + * SWITCHER VIEW + */ + +async function renderSwitchCard() { + document.getElementById( + 'popupContainer' + ).innerHTML = document.getElementById('switcherTemplate').innerHTML; + + const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); + spacesRenderer.initialise(8, true); + spacesRenderer.renderSpaces(spaces); + + document.getElementById('spaceSelectForm').onsubmit = e => { + e.preventDefault(); + handleSwitchAction(getSelectedSpace()); + }; + + const allSpaceEls = document.querySelectorAll('.space'); + Array.prototype.forEach.call(allSpaceEls, el => { + // eslint-disable-next-line no-param-reassign + el.onclick = () => { + handleSwitchAction(el); + }; + }); +} function getSelectedSpace() { return document.querySelector('.space.selected'); @@ -325,7 +318,7 @@ import { utils } from './utils.js'; nodes.activeSpaceTitle.innerHTML = globalSessionName || '(unnamed)'; // selectSpace(nodes.activeSpace); - updateTabDetails(); + await updateTabDetails(); const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); // remove currently visible space @@ -346,58 +339,55 @@ import { utils } from './utils.js'; } } - function updateTabDetails() { - let faviconSrc; - - // if we are working with an open chrome tab - if (globalTabId.length > 0) { - chrome.runtime.sendMessage( - { - action: 'requestTabDetail', - tabId: globalTabId, - }, - tab => { - if (tab) { - nodes.activeTabTitle.innerHTML = tab.title; - - // try to get best favicon url path - if ( - tab.favIconUrl && - tab.favIconUrl.indexOf('chrome://theme') < 0 - ) { - faviconSrc = tab.favIconUrl; - } else { - // TODO(codedread): Fix this, it errors. - // faviconSrc = `chrome://favicon/${tab.url}`; - } - nodes.activeTabFavicon.setAttribute('src', faviconSrc); - - nodes.moveInput.setAttribute( - 'placeholder', - 'Move tab to..' - ); - - // nodes.windowTitle.innerHTML = tab.title; - // nodes.windowFavicon.setAttribute('href', faviconSrc); - } - } +async function updateTabDetails() { + let faviconSrc; + + // if we are working with an open chrome tab + if (globalTabId.length > 0) { + const tab = await chrome.runtime.sendMessage({ + action: 'requestTabDetail', + tabId: globalTabId, + }); + + if (tab) { + nodes.activeTabTitle.innerHTML = tab.title; + + // try to get best favicon url path + if ( + tab.favIconUrl && + tab.favIconUrl.indexOf('chrome://theme') < 0 + ) { + faviconSrc = tab.favIconUrl; + } else { + // TODO(codedread): Fix this, it errors. + // faviconSrc = `chrome://favicon/${tab.url}`; + } + nodes.activeTabFavicon.setAttribute('src', faviconSrc); + + nodes.moveInput.setAttribute( + 'placeholder', + 'Move tab to..' ); - // else if we are dealing with a url only - } else if (globalUrl) { - const cleanUrl = - globalUrl.indexOf('://') > 0 - ? globalUrl.substr( - globalUrl.indexOf('://') + 3, - globalUrl.length - ) - : globalUrl; - nodes.activeTabTitle.innerHTML = cleanUrl; - nodes.activeTabFavicon.setAttribute('src', '/img/new.png'); - - nodes.moveInput.setAttribute('placeholder', 'Add tab to..'); + // nodes.windowTitle.innerHTML = tab.title; + // nodes.windowFavicon.setAttribute('href', faviconSrc); } + + // else if we are dealing with a url only + } else if (globalUrl) { + const cleanUrl = + globalUrl.indexOf('://') > 0 + ? globalUrl.substr( + globalUrl.indexOf('://') + 3, + globalUrl.length + ) + : globalUrl; + nodes.activeTabTitle.innerHTML = cleanUrl; + nodes.activeTabFavicon.setAttribute('src', '/img/new.png'); + + nodes.moveInput.setAttribute('placeholder', 'Add tab to..'); } +} function handleSelectAction() { const selectedSpaceEl = document.querySelector('.space.selected'); diff --git a/js/switcher.js b/js/switcher.js index 5b9aa54..d90f0c2 100644 --- a/js/switcher.js +++ b/js/switcher.js @@ -77,10 +77,9 @@ function addEventListeners() { }); } -window.onload = () => { - chrome.runtime.sendMessage({ action: 'requestAllSpaces' }, spaces => { - spacesRenderer.initialise(8, true); - spacesRenderer.renderSpaces(spaces); - addEventListeners(); - }); +window.onload = async () => { + const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); + spacesRenderer.initialise(8, true); + spacesRenderer.renderSpaces(spaces); + addEventListeners(); }; From ced53eaeb79612f52eaa08174d1bb7361fb1fca3 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 13:59:06 -0700 Subject: [PATCH 47/92] Finish unindenting top-level functions --- js/background/background.js | 1633 ++++++++++++++++++----------------- js/popup.js | 643 +++++++------- js/spaces.js | 1026 +++++++++++----------- 3 files changed, 1655 insertions(+), 1647 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index cde526a..eada299 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -89,632 +89,639 @@ chrome.runtime.onStartup.addListener(async () => { // LISTENERS - // add listeners for session monitoring - chrome.tabs.onCreated.addListener(async (tab) => { - // this call to checkInternalSpacesWindows actually returns false when it should return true - // due to the event being called before the globalWindowIds get set. oh well, never mind. - if (checkInternalSpacesWindows(tab.windowId, false)) return; - // don't need this listener as the tabUpdated listener also fires when a new tab is created - // spacesService.handleTabCreated(tab); - updateSpacesWindow('tabs.onCreated'); - }); - chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { - if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; - spacesService.handleTabRemoved(tabId, removeInfo, () => { - updateSpacesWindow('tabs.onRemoved'); - }); - }); - chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { - if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; - spacesService.handleTabMoved(tabId, moveInfo, () => { - updateSpacesWindow('tabs.onMoved'); - }); +// add listeners for session monitoring +chrome.tabs.onCreated.addListener(async (tab) => { + // this call to checkInternalSpacesWindows actually returns false when it should return true + // due to the event being called before the globalWindowIds get set. oh well, never mind. + if (checkInternalSpacesWindows(tab.windowId, false)) return; + // don't need this listener as the tabUpdated listener also fires when a new tab is created + // spacesService.handleTabCreated(tab); + updateSpacesWindow('tabs.onCreated'); +}); + +chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { + if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; + spacesService.handleTabRemoved(tabId, removeInfo, () => { + updateSpacesWindow('tabs.onRemoved'); }); - chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (checkInternalSpacesWindows(tab.windowId, false)) return; +}); - spacesService.handleTabUpdated(tab, changeInfo, () => { - updateSpacesWindow('tabs.onUpdated'); - }); +chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { + if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; + spacesService.handleTabMoved(tabId, moveInfo, () => { + updateSpacesWindow('tabs.onMoved'); }); - chrome.windows.onRemoved.addListener(async (windowId) => { - if (checkInternalSpacesWindows(windowId, true)) return; - spacesService.handleWindowRemoved(windowId, true, () => { - updateSpacesWindow('windows.onRemoved'); - }); +}); - // if this was the last window open and the spaces window is stil open - // then close the spaces window also so that chrome exits fully - // NOTE: this is a workaround for an issue with the chrome 'restore previous session' option - // if the spaces window is the only window open and you try to use it to open a space, - // when that space loads, it also loads all the windows from the window that was last closed - const windows = await chrome.windows.getAll({}); - if (windows.length === 1 && spacesOpenWindowId) { - await chrome.windows.remove(spacesOpenWindowId); - spacesOpenWindowId = false; - await chrome.storage.local.remove('spacesOpenWindowId'); - } +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (checkInternalSpacesWindows(tab.windowId, false)) return; + + spacesService.handleTabUpdated(tab, changeInfo, () => { + updateSpacesWindow('tabs.onUpdated'); }); - // don't need this listener as the tabUpdated listener also fires when a new window is created - // chrome.windows.onCreated.addListener(function (window) { - - // if (checkInternalSpacesWindows(window.id, false)) return; - // spacesService.handleWindowCreated(window); - // }); - - // add listeners for tab and window focus changes - // when a tab or window is changed, close the move tab popup if it is open - chrome.windows.onFocusChanged.addListener(async (windowId) => { - // Prevent a click in the popup on Ubunto or ChroneOS from closing the - // popup prematurely. - if ( - windowId === chrome.windows.WINDOW_ID_NONE || - windowId === spacesPopupWindowId - ) { - return; - } +}); - if (!debug && spacesPopupWindowId) { - if (spacesPopupWindowId) { - await closePopupWindow(); - } - } - - spacesService.handleWindowFocussed(windowId); +chrome.windows.onRemoved.addListener(async (windowId) => { + if (checkInternalSpacesWindows(windowId, true)) return; + spacesService.handleWindowRemoved(windowId, true, () => { + updateSpacesWindow('windows.onRemoved'); }); - // add listeners for message requests from other extension pages (spaces.html & tab.html) + // if this was the last window open and the spaces window is stil open + // then close the spaces window also so that chrome exits fully + // NOTE: this is a workaround for an issue with the chrome 'restore previous session' option + // if the spaces window is the only window open and you try to use it to open a space, + // when that space loads, it also loads all the windows from the window that was last closed + const windows = await chrome.windows.getAll({}); + if (windows.length === 1 && spacesOpenWindowId) { + await chrome.windows.remove(spacesOpenWindowId); + spacesOpenWindowId = false; + await chrome.storage.local.remove('spacesOpenWindowId'); + } +}); - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (debug) { - // eslint-disable-next-line no-console - console.log(`listener fired: ${JSON.stringify(request)}`); +// don't need this listener as the tabUpdated listener also fires when a new window is created +// chrome.windows.onCreated.addListener(function (window) { + +// if (checkInternalSpacesWindows(window.id, false)) return; +// spacesService.handleWindowCreated(window); +// }); + +// add listeners for tab and window focus changes +// when a tab or window is changed, close the move tab popup if it is open +chrome.windows.onFocusChanged.addListener(async (windowId) => { + // Prevent a click in the popup on Ubunto or ChroneOS from closing the + // popup prematurely. + if ( + windowId === chrome.windows.WINDOW_ID_NONE || + windowId === spacesPopupWindowId + ) { + return; + } + + if (!debug && spacesPopupWindowId) { + if (spacesPopupWindowId) { + await closePopupWindow(); } + } + + spacesService.handleWindowFocussed(windowId); +}); - // Ensure spacesService is initialized before processing any message - spacesService.ensureInitialized().then(() => { - const result = processMessage(request, sender, sendResponse); - // If processMessage returns false, we need to handle that by not sending a response - // But since we're in an async context, we can't change the outer return value - // The key is that processMessage will call sendResponse() for true cases - // and won't call it for false cases, which is the correct behavior - }); - - // We have to return true here because we're handling everything asynchronously - // The actual response sending is controlled by whether processMessage calls sendResponse() - return true; - }); +// add listeners for message requests from other extension pages (spaces.html & tab.html) - function processMessage(request, sender, sendResponse) { - let sessionId; - let windowId; - let tabId; +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (debug) { + // eslint-disable-next-line no-console + console.log(`listener fired: ${JSON.stringify(request)}`); + } - // endpoints called by spaces.js - switch (request.action) { - case 'requestSessionPresence': - const sessionPresence = requestSessionPresence(request.sessionName); - sendResponse(sessionPresence); - return true; + // Ensure spacesService is initialized before processing any message + spacesService.ensureInitialized().then(() => { + const result = processMessage(request, sender, sendResponse); + // If processMessage returns false, we need to handle that by not sending a response + // But since we're in an async context, we can't change the outer return value + // The key is that processMessage will call sendResponse() for true cases + // and won't call it for false cases, which is the correct behavior + }); + + // We have to return true here because we're handling everything asynchronously + // The actual response sending is controlled by whether processMessage calls sendResponse() + return true; +}); - case 'requestSpaceFromWindowId': - windowId = _cleanParameter(request.windowId); - if (windowId) { - requestSpaceFromWindowId(windowId).then(space => { - sendResponse(space); - }); - } - return true; +function processMessage(request, sender, sendResponse) { + let sessionId; + let windowId; + let tabId; - case 'requestCurrentSpace': - requestCurrentSpace().then(space => { + // endpoints called by spaces.js + switch (request.action) { + case 'requestSessionPresence': + const sessionPresence = requestSessionPresence(request.sessionName); + sendResponse(sessionPresence); + return true; + + case 'requestSpaceFromWindowId': + windowId = _cleanParameter(request.windowId); + if (windowId) { + requestSpaceFromWindowId(windowId).then(space => { sendResponse(space); }); - return true; + } + return true; - case 'generatePopupParams': - generatePopupParams(request.action, request.tabUrl).then(params => { - sendResponse(params); - }); - return true; + case 'requestCurrentSpace': + requestCurrentSpace().then(space => { + sendResponse(space); + }); + return true; - case 'loadSession': - sessionId = _cleanParameter(request.sessionId); - if (sessionId) { - handleLoadSession(sessionId); - sendResponse(true); - } - // close the requesting tab (should be spaces.html) - // if (!debug) closeChromeTab(sender.tab.id); + case 'generatePopupParams': + generatePopupParams(request.action, request.tabUrl).then(params => { + sendResponse(params); + }); + return true; - return true; + case 'loadSession': + sessionId = _cleanParameter(request.sessionId); + if (sessionId) { + handleLoadSession(sessionId); + sendResponse(true); + } + // close the requesting tab (should be spaces.html) + // if (!debug) closeChromeTab(sender.tab.id); - case 'loadWindow': - windowId = _cleanParameter(request.windowId); - if (windowId) { - handleLoadWindow(windowId); - sendResponse(true); - } - // close the requesting tab (should be spaces.html) - // if (!debug) closeChromeTab(sender.tab.id); + return true; - return true; + case 'loadWindow': + windowId = _cleanParameter(request.windowId); + if (windowId) { + handleLoadWindow(windowId); + sendResponse(true); + } + // close the requesting tab (should be spaces.html) + // if (!debug) closeChromeTab(sender.tab.id); - case 'loadTabInSession': - sessionId = _cleanParameter(request.sessionId); - if (sessionId && request.tabUrl) { - handleLoadSession(sessionId, request.tabUrl); - sendResponse(true); - } - // close the requesting tab (should be spaces.html) - // if (!debug) closeChromeTab(sender.tab.id); + return true; - return true; + case 'loadTabInSession': + sessionId = _cleanParameter(request.sessionId); + if (sessionId && request.tabUrl) { + handleLoadSession(sessionId, request.tabUrl); + sendResponse(true); + } + // close the requesting tab (should be spaces.html) + // if (!debug) closeChromeTab(sender.tab.id); - case 'loadTabInWindow': - windowId = _cleanParameter(request.windowId); - if (windowId && request.tabUrl) { - handleLoadWindow(windowId, request.tabUrl); - sendResponse(true); - } - // close the requesting tab (should be spaces.html) - // if (!debug) closeChromeTab(sender.tab.id); - - return true; - - case 'saveNewSession': - windowId = _cleanParameter(request.windowId); - if (windowId && request.sessionName) { - handleSaveNewSession( - windowId, - request.sessionName, - !!request.deleteOld, - sendResponse - ); - } - return true; // allow async response + return true; - case 'importNewSession': - if (request.urlList) { - handleImportNewSession(request.urlList, sendResponse); - } - return true; // allow async response + case 'loadTabInWindow': + windowId = _cleanParameter(request.windowId); + if (windowId && request.tabUrl) { + handleLoadWindow(windowId, request.tabUrl); + sendResponse(true); + } + // close the requesting tab (should be spaces.html) + // if (!debug) closeChromeTab(sender.tab.id); - case 'restoreFromBackup': - if (request.space) { - handleRestoreFromBackup(request.space, !!request.deleteOld, sendResponse); - } - return true; // allow async response + return true; - case 'deleteSession': - sessionId = _cleanParameter(request.sessionId); - if (sessionId) { - handleDeleteSession(sessionId, sendResponse); - } - return true; - - case 'updateSessionName': - sessionId = _cleanParameter(request.sessionId); - if (sessionId && request.sessionName) { - handleUpdateSessionName( - sessionId, - request.sessionName, - !!request.deleteOld, - sendResponse - ); - } - return true; + case 'saveNewSession': + windowId = _cleanParameter(request.windowId); + if (windowId && request.sessionName) { + handleSaveNewSession( + windowId, + request.sessionName, + !!request.deleteOld, + sendResponse + ); + } + return true; // allow async response - case 'requestSpaceDetail': - windowId = _cleanParameter(request.windowId); - sessionId = _cleanParameter(request.sessionId); + case 'importNewSession': + if (request.urlList) { + handleImportNewSession(request.urlList, sendResponse); + } + return true; // allow async response - if (windowId) { - if (checkInternalSpacesWindows(windowId, false)) { - sendResponse(false); - } else { - requestSpaceFromWindowId(windowId).then(space => { - sendResponse(space); - }); - } - } else if (sessionId) { - requestSpaceFromSessionId(sessionId, sendResponse); - } - return true; + case 'restoreFromBackup': + if (request.space) { + handleRestoreFromBackup(request.space, !!request.deleteOld, sendResponse); + } + return true; // allow async response - // end points called by tag.js and switcher.js - // note: some of these endpoints will close the requesting tab - case 'requestAllSpaces': - requestAllSpaces(allSpaces => { - sendResponse(allSpaces); - }); - return true; - - case 'requestTabDetail': - tabId = _cleanParameter(request.tabId); - if (tabId) { - requestTabDetail(tabId).then(tab => { - if (tab) { - sendResponse(tab); - } else { - // close the requesting tab (should be tab.html) - closePopupWindow(); - } - }); - } - return true; + case 'deleteSession': + sessionId = _cleanParameter(request.sessionId); + if (sessionId) { + handleDeleteSession(sessionId, sendResponse); + } + return true; - case 'requestShowSpaces': - windowId = _cleanParameter(request.windowId); + case 'updateSessionName': + sessionId = _cleanParameter(request.sessionId); + if (sessionId && request.sessionName) { + handleUpdateSessionName( + sessionId, + request.sessionName, + !!request.deleteOld, + sendResponse + ); + } + return true; - // show the spaces tab in edit mode for the passed in windowId - if (windowId) { - showSpacesOpenWindow(windowId, request.edit); + case 'requestSpaceDetail': + windowId = _cleanParameter(request.windowId); + sessionId = _cleanParameter(request.sessionId); + + if (windowId) { + if (checkInternalSpacesWindows(windowId, false)) { + sendResponse(false); } else { - showSpacesOpenWindow(); - } - return false; - - case 'requestShowSwitcher': - showSpacesSwitchWindow(); - return false; - - case 'requestShowMover': - showSpacesMoveWindow(); - return false; - - case 'requestShowKeyboardShortcuts': - createShortcutsWindow(); - return false; - - case 'requestClose': - // close the requesting tab (should be tab.html) - closePopupWindow(); - return false; - - case 'switchToSpace': - windowId = _cleanParameter(request.windowId); - sessionId = _cleanParameter(request.sessionId); - - (async () => { - if (windowId) { - await handleLoadWindow(windowId); - } else if (sessionId) { - await handleLoadSession(sessionId); - } - sendResponse(true); - })(); - - return true; - - case 'addLinkToNewSession': - tabId = _cleanParameter(request.tabId); - if (request.sessionName && request.url) { - handleAddLinkToNewSession( - request.url, - request.sessionName, - result => { - if (result) - updateSpacesWindow('addLinkToNewSession'); - - // close the requesting tab (should be tab.html) - closePopupWindow(); - } - ); - } - return false; - - case 'moveTabToNewSession': - tabId = _cleanParameter(request.tabId); - if (request.sessionName && tabId) { - handleMoveTabToNewSession( - tabId, - request.sessionName, - result => { - if (result) - updateSpacesWindow('moveTabToNewSession'); - - // close the requesting tab (should be tab.html) - closePopupWindow(); - } - ); + requestSpaceFromWindowId(windowId).then(space => { + sendResponse(space); + }); } - return false; - - case 'addLinkToSession': - sessionId = _cleanParameter(request.sessionId); + } else if (sessionId) { + requestSpaceFromSessionId(sessionId, sendResponse); + } + return true; - if (sessionId && request.url) { - handleAddLinkToSession(request.url, sessionId, result => { - if (result) updateSpacesWindow('addLinkToSession'); + // end points called by tag.js and switcher.js + // note: some of these endpoints will close the requesting tab + case 'requestAllSpaces': + requestAllSpaces(allSpaces => { + sendResponse(allSpaces); + }); + return true; + case 'requestTabDetail': + tabId = _cleanParameter(request.tabId); + if (tabId) { + requestTabDetail(tabId).then(tab => { + if (tab) { + sendResponse(tab); + } else { // close the requesting tab (should be tab.html) closePopupWindow(); - }); - } - return false; + } + }); + } + return true; - case 'moveTabToSession': - sessionId = _cleanParameter(request.sessionId); - tabId = _cleanParameter(request.tabId); + case 'requestShowSpaces': + windowId = _cleanParameter(request.windowId); - if (sessionId && tabId) { - handleMoveTabToSession(tabId, sessionId, result => { - if (result) updateSpacesWindow('moveTabToSession'); + // show the spaces tab in edit mode for the passed in windowId + if (windowId) { + showSpacesOpenWindow(windowId, request.edit); + } else { + showSpacesOpenWindow(); + } + return false; - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); - } - return false; + case 'requestShowSwitcher': + showSpacesSwitchWindow(); + return false; - case 'addLinkToWindow': - windowId = _cleanParameter(request.windowId); + case 'requestShowMover': + showSpacesMoveWindow(); + return false; - if (windowId && request.url) { - handleAddLinkToWindow(request.url, windowId, result => { - if (result) updateSpacesWindow('addLinkToWindow'); + case 'requestShowKeyboardShortcuts': + createShortcutsWindow(); + return false; - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); + case 'requestClose': + // close the requesting tab (should be tab.html) + closePopupWindow(); + return false; + + case 'switchToSpace': + windowId = _cleanParameter(request.windowId); + sessionId = _cleanParameter(request.sessionId); + + (async () => { + if (windowId) { + await handleLoadWindow(windowId); + } else if (sessionId) { + await handleLoadSession(sessionId); } - return false; + sendResponse(true); + })(); - case 'moveTabToWindow': - windowId = _cleanParameter(request.windowId); - tabId = _cleanParameter(request.tabId); + return true; - if (windowId && tabId) { - handleMoveTabToWindow(tabId, windowId, result => { - if (result) updateSpacesWindow('moveTabToWindow'); + case 'addLinkToNewSession': + tabId = _cleanParameter(request.tabId); + if (request.sessionName && request.url) { + handleAddLinkToNewSession( + request.url, + request.sessionName, + result => { + if (result) + updateSpacesWindow('addLinkToNewSession'); // close the requesting tab (should be tab.html) closePopupWindow(); - }); - } - return false; + } + ); + } + return false; - default: - return false; - } - } - - function _cleanParameter(param) { - if (typeof param === 'number') { - return param; - } - if (param === 'false') { + case 'moveTabToNewSession': + tabId = _cleanParameter(request.tabId); + if (request.sessionName && tabId) { + handleMoveTabToNewSession( + tabId, + request.sessionName, + result => { + if (result) + updateSpacesWindow('moveTabToNewSession'); + + // close the requesting tab (should be tab.html) + closePopupWindow(); + } + ); + } return false; - } - if (param === 'true') { - return true; - } - return parseInt(param, 10); - } - // add listeners for keyboard commands + case 'addLinkToSession': + sessionId = _cleanParameter(request.sessionId); - chrome.commands.onCommand.addListener(command => { - // handle showing the move tab popup (tab.html) - if (command === 'spaces-move') { - showSpacesMoveWindow(); + if (sessionId && request.url) { + handleAddLinkToSession(request.url, sessionId, result => { + if (result) updateSpacesWindow('addLinkToSession'); - // handle showing the switcher tab popup (switcher.html) - } else if (command === 'spaces-switch') { - showSpacesSwitchWindow(); - } - }); + // close the requesting tab (should be tab.html) + closePopupWindow(); + }); + } + return false; - chrome.contextMenus.onClicked.addListener(info => { - // handle showing the move tab popup (tab.html) - if (info.menuItemId === 'spaces-add-link') { - showSpacesMoveWindow(info.linkUrl); - } - }); + case 'moveTabToSession': + sessionId = _cleanParameter(request.sessionId); + tabId = _cleanParameter(request.tabId); - function createShortcutsWindow() { - chrome.tabs.create({ url: 'chrome://extensions/configureCommands' }); - } + if (sessionId && tabId) { + handleMoveTabToSession(tabId, sessionId, result => { + if (result) updateSpacesWindow('moveTabToSession'); + + // close the requesting tab (should be tab.html) + closePopupWindow(); + }); + } + return false; - async function showSpacesOpenWindow(windowId, editMode) { - let url; + case 'addLinkToWindow': + windowId = _cleanParameter(request.windowId); - if (editMode && windowId) { - url = chrome.runtime.getURL( - `spaces.html#windowId=${windowId}&editMode=true` - ); - } else { - url = chrome.runtime.getURL('spaces.html'); - } + if (windowId && request.url) { + handleAddLinkToWindow(request.url, windowId, result => { + if (result) updateSpacesWindow('addLinkToWindow'); - // if spaces open window already exists then just give it focus (should be up to date) - if (spacesOpenWindowId) { - const window = await chrome.windows.get(spacesOpenWindowId, { populate: true }); - await chrome.windows.update(spacesOpenWindowId, { - focused: true, - }); - if (window.tabs[0].id) { - await chrome.tabs.update(window.tabs[0].id, { url }); + // close the requesting tab (should be tab.html) + closePopupWindow(); + }); } + return false; - // otherwise re-create it - } else { - // TODO(codedread): Handle multiple displays and errors. - const displays = await chrome.system.display.getInfo(); - let screen = displays[0].bounds; - const window = await chrome.windows.create( - { - type: 'popup', - url, - height: screen.height - 100, - width: Math.min(screen.width, 1000), - top: 0, - left: 0, + case 'moveTabToWindow': + windowId = _cleanParameter(request.windowId); + tabId = _cleanParameter(request.tabId); + + if (windowId && tabId) { + handleMoveTabToWindow(tabId, windowId, result => { + if (result) updateSpacesWindow('moveTabToWindow'); + + // close the requesting tab (should be tab.html) + closePopupWindow(); }); - spacesOpenWindowId = window.id; - await chrome.storage.local.set({spacesOpenWindowId: window.id}); - } + } + return false; + + default: + return false; } - function showSpacesMoveWindow(tabUrl) { - createOrShowSpacesPopupWindow('move', tabUrl); +} + +function _cleanParameter(param) { + if (typeof param === 'number') { + return param; } - function showSpacesSwitchWindow() { - createOrShowSpacesPopupWindow('switch'); + if (param === 'false') { + return false; } + if (param === 'true') { + return true; + } + return parseInt(param, 10); +} + +// add listeners for keyboard commands - async function generatePopupParams(action, tabUrl) { - // get currently highlighted tab - const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); - if (tabs.length === 0) return ''; +chrome.commands.onCommand.addListener(command => { + // handle showing the move tab popup (tab.html) + if (command === 'spaces-move') { + showSpacesMoveWindow(); - const activeTab = tabs[0]; + // handle showing the switcher tab popup (switcher.html) + } else if (command === 'spaces-switch') { + showSpacesSwitchWindow(); + } +}); - // make sure that the active tab is not from an internal spaces window - if (checkInternalSpacesWindows(activeTab.windowId, false)) { - return ''; +chrome.contextMenus.onClicked.addListener(info => { + // handle showing the move tab popup (tab.html) + if (info.menuItemId === 'spaces-add-link') { + showSpacesMoveWindow(info.linkUrl); + } +}); + +function createShortcutsWindow() { + chrome.tabs.create({ url: 'chrome://extensions/configureCommands' }); +} + +async function showSpacesOpenWindow(windowId, editMode) { + let url; + + if (editMode && windowId) { + url = chrome.runtime.getURL( + `spaces.html#windowId=${windowId}&editMode=true` + ); + } else { + url = chrome.runtime.getURL('spaces.html'); + } + + // if spaces open window already exists then just give it focus (should be up to date) + if (spacesOpenWindowId) { + const window = await chrome.windows.get(spacesOpenWindowId, { populate: true }); + await chrome.windows.update(spacesOpenWindowId, { + focused: true, + }); + if (window.tabs[0].id) { + await chrome.tabs.update(window.tabs[0].id, { url }); } - const session = await dbService.fetchSessionByWindowId(activeTab.windowId); + // otherwise re-create it + } else { + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + const window = await chrome.windows.create( + { + type: 'popup', + url, + height: screen.height - 100, + width: Math.min(screen.width, 1000), + top: 0, + left: 0, + }); + spacesOpenWindowId = window.id; + await chrome.storage.local.set({spacesOpenWindowId: window.id}); + } +} + +function showSpacesMoveWindow(tabUrl) { + createOrShowSpacesPopupWindow('move', tabUrl); +} - const name = session ? session.name : ''; +function showSpacesSwitchWindow() { + createOrShowSpacesPopupWindow('switch'); +} - let params = `action=${action}&windowId=${activeTab.windowId}&sessionName=${name}`; +async function generatePopupParams(action, tabUrl) { + // get currently highlighted tab + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tabs.length === 0) return ''; - if (tabUrl) { - params += `&url=${encodeURIComponent(tabUrl)}`; + const activeTab = tabs[0]; + + // make sure that the active tab is not from an internal spaces window + if (checkInternalSpacesWindows(activeTab.windowId, false)) { + return ''; + } + + const session = await dbService.fetchSessionByWindowId(activeTab.windowId); + + const name = session ? session.name : ''; + + let params = `action=${action}&windowId=${activeTab.windowId}&sessionName=${name}`; + + if (tabUrl) { + params += `&url=${encodeURIComponent(tabUrl)}`; + } else { + params += `&tabId=${activeTab.id}`; + } + return params; +} + +async function createOrShowSpacesPopupWindow(action, tabUrl) { + const params = await generatePopupParams(action, tabUrl); + const popupUrl = `${chrome.runtime.getURL( + 'popup.html' + )}#opener=bg&${params}`; + // if spaces window already exists + if (spacesPopupWindowId) { + const window = await chrome.windows.get( + spacesPopupWindowId, + { populate: true } + ); + // if window is currently focused then don't update + if (window.focused) { + // else update popupUrl and give it focus } else { - params += `&tabId=${activeTab.id}`; + await chrome.windows.update(spacesPopupWindowId, { + focused: true, + }); + if (window.tabs[0].id) { + await chrome.tabs.update(window.tabs[0].id, { + url: popupUrl, + }); + } } - return params; + + // otherwise create it + } else { + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + + const window = await chrome.windows.create( + { + type: 'popup', + url: popupUrl, + focused: true, + height: 450, + width: 310, + top: screen.height - 450, + left: screen.width - 310, + }); + spacesPopupWindowId = window.id; + await chrome.storage.local.set({spacesPopupWindowId: window.id}); } +} - async function createOrShowSpacesPopupWindow(action, tabUrl) { - const params = await generatePopupParams(action, tabUrl); - const popupUrl = `${chrome.runtime.getURL( - 'popup.html' - )}#opener=bg&${params}`; - // if spaces window already exists - if (spacesPopupWindowId) { - const window = await chrome.windows.get( +async function closePopupWindow() { + if (spacesPopupWindowId) { + try { + const spacesWindow = await chrome.windows.get( spacesPopupWindowId, { populate: true } ); - // if window is currently focused then don't update - if (window.focused) { - // else update popupUrl and give it focus - } else { - await chrome.windows.update(spacesPopupWindowId, { - focused: true, + if (!spacesWindow) return; + + // remove popup from history + if ( + spacesWindow.tabs.length > 0 && + spacesWindow.tabs[0].url + ) { + await chrome.history.deleteUrl({ + url: spacesWindow.tabs[0].url, }); - if (window.tabs[0].id) { - await chrome.tabs.update(window.tabs[0].id, { - url: popupUrl, - }); - } } - // otherwise create it - } else { - // TODO(codedread): Handle multiple displays and errors. - const displays = await chrome.system.display.getInfo(); - let screen = displays[0].bounds; - - const window = await chrome.windows.create( - { - type: 'popup', - url: popupUrl, - focused: true, - height: 450, - width: 310, - top: screen.height - 450, - left: screen.width - 310, - }); - spacesPopupWindowId = window.id; - await chrome.storage.local.set({spacesPopupWindowId: window.id}); + // remove popup window + await chrome.windows.remove(spacesWindow.id); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e.message); } } +} - async function closePopupWindow() { - if (spacesPopupWindowId) { - try { - const spacesWindow = await chrome.windows.get( - spacesPopupWindowId, - { populate: true } - ); - if (!spacesWindow) return; - - // remove popup from history - if ( - spacesWindow.tabs.length > 0 && - spacesWindow.tabs[0].url - ) { - await chrome.history.deleteUrl({ - url: spacesWindow.tabs[0].url, - }); - } +async function updateSpacesWindow(source) { + if (debug) { + // eslint-disable-next-line no-console + console.log(`updateSpacesWindow: triggered. source: ${source}`); + } - // remove popup window - await chrome.windows.remove(spacesWindow.id); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e.message); - } - } + // If we don't have a cached spacesOpenWindowId, try to find the spaces window + if (!spacesOpenWindowId) { + await rediscoverWindowIds(); } - async function updateSpacesWindow(source) { - if (debug) { + if (spacesOpenWindowId) { + const spacesOpenWindow = await chrome.windows.get(spacesOpenWindowId); + if (chrome.runtime.lastError || !spacesOpenWindow) { // eslint-disable-next-line no-console - console.log(`updateSpacesWindow: triggered. source: ${source}`); - } - - // If we don't have a cached spacesOpenWindowId, try to find the spaces window - if (!spacesOpenWindowId) { - await rediscoverWindowIds(); + console.log(`updateSpacesWindow: Error getting spacesOpenWindow: ${chrome.runtime.lastError}`); + spacesOpenWindowId = false; + await chrome.storage.local.remove('spacesOpenWindowId'); + return; } - if (spacesOpenWindowId) { - const spacesOpenWindow = await chrome.windows.get(spacesOpenWindowId); - if (chrome.runtime.lastError || !spacesOpenWindow) { - // eslint-disable-next-line no-console - console.log(`updateSpacesWindow: Error getting spacesOpenWindow: ${chrome.runtime.lastError}`); - spacesOpenWindowId = false; - await chrome.storage.local.remove('spacesOpenWindowId'); - return; + requestAllSpaces(allSpaces => { + try { + chrome.runtime.sendMessage({ + action: 'updateSpaces', + spaces: allSpaces, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`updateSpacesWindow: Error updating spaces window: ${err}`); } - - requestAllSpaces(allSpaces => { - try { - chrome.runtime.sendMessage({ - action: 'updateSpaces', - spaces: allSpaces, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`updateSpacesWindow: Error updating spaces window: ${err}`); - } - }); - } + }); } +} - function checkInternalSpacesWindows(windowId, windowClosed) { - if (windowId === spacesOpenWindowId) { - if (windowClosed) { - spacesOpenWindowId = false; - chrome.storage.local.remove('spacesOpenWindowId'); - } - return true; +function checkInternalSpacesWindows(windowId, windowClosed) { + if (windowId === spacesOpenWindowId) { + if (windowClosed) { + spacesOpenWindowId = false; + chrome.storage.local.remove('spacesOpenWindowId'); } - if (windowId === spacesPopupWindowId) { - if (windowClosed) { - spacesPopupWindowId = false; - chrome.storage.local.remove('spacesPopupWindowId'); - } - return true; + return true; + } + if (windowId === spacesPopupWindowId) { + if (windowClosed) { + spacesPopupWindowId = false; + chrome.storage.local.remove('spacesPopupWindowId'); } - return false; + return true; } + return false; +} /** * @param {string} sessionName @@ -784,369 +791,369 @@ async function requestSpaceFromWindowId(windowId) { } } - async function requestSpaceFromSessionId(sessionId, callback) { - const session = await dbService.fetchSessionById(sessionId); +async function requestSpaceFromSessionId(sessionId, callback) { + const session = await dbService.fetchSessionById(sessionId); - callback({ - sessionId: session.id, - windowId: session.windowId, - name: session.name, - tabs: session.tabs, - history: session.history, + callback({ + sessionId: session.id, + windowId: session.windowId, + name: session.name, + tabs: session.tabs, + history: session.history, + }); +} + +async function requestAllSpaces(callback) { + const sessions = await dbService.fetchAllSessions(); + const allSpaces = sessions + .map(session => { + return { sessionId: session.id, ...session }; + }) + .filter(session => { + return session && session.tabs && session.tabs.length > 0; }); - } - async function requestAllSpaces(callback) { - const sessions = await dbService.fetchAllSessions(); - const allSpaces = sessions - .map(session => { - return { sessionId: session.id, ...session }; - }) - .filter(session => { - return session && session.tabs && session.tabs.length > 0; - }); + // sort results + allSpaces.sort(spaceDateCompare); - // sort results - allSpaces.sort(spaceDateCompare); + callback(allSpaces); +} - callback(allSpaces); +function spaceDateCompare(a, b) { + // order open sessions first + if (a.windowId && !b.windowId) { + return -1; } - - function spaceDateCompare(a, b) { - // order open sessions first - if (a.windowId && !b.windowId) { - return -1; - } - if (!a.windowId && b.windowId) { - return 1; - } - // then order by last access date - if (a.lastAccess > b.lastAccess) { - return -1; - } - if (a.lastAccess < b.lastAccess) { - return 1; - } - return 0; + if (!a.windowId && b.windowId) { + return 1; + } + // then order by last access date + if (a.lastAccess > b.lastAccess) { + return -1; } + if (a.lastAccess < b.lastAccess) { + return 1; + } + return 0; +} - async function handleLoadSession(sessionId, tabUrl) { - const session = await dbService.fetchSessionById(sessionId); + async function handleLoadSession(sessionId, tabUrl) { + const session = await dbService.fetchSessionById(sessionId); - // if space is already open, then give it focus - if (session.windowId) { - await handleLoadWindow(session.windowId, tabUrl); + // if space is already open, then give it focus + if (session.windowId) { + await handleLoadWindow(session.windowId, tabUrl); - // else load space in new window - } else { - const urls = session.tabs.map(curTab => { - return curTab.url; + // else load space in new window + } else { + const urls = session.tabs.map(curTab => { + return curTab.url; + }); + + // TODO(codedread): Handle multiple displays and errors. + const displays = await chrome.system.display.getInfo(); + let screen = displays[0].bounds; + + const newWindow = await chrome.windows.create( + { + url: urls, + height: screen.height - 100, + width: screen.width - 100, + top: 0, + left: 0, }); - // TODO(codedread): Handle multiple displays and errors. - const displays = await chrome.system.display.getInfo(); - let screen = displays[0].bounds; - - const newWindow = await chrome.windows.create( - { - url: urls, - height: screen.height - 100, - width: screen.width - 100, - top: 0, - left: 0, + // force match this new window to the session + spacesService.matchSessionToWindow(session, newWindow); + + // after window has loaded try to pin any previously pinned tabs + for (const curSessionTab of session.tabs) { + if (curSessionTab.pinned) { + let pinnedTabId = false; + newWindow.tabs.some(curNewTab => { + if ( + curNewTab.url === curSessionTab.url || + curNewTab.pendingUrl === curSessionTab.url + ) { + pinnedTabId = curNewTab.id; + return true; + } + return false; }); - - // force match this new window to the session - spacesService.matchSessionToWindow(session, newWindow); - - // after window has loaded try to pin any previously pinned tabs - for (const curSessionTab of session.tabs) { - if (curSessionTab.pinned) { - let pinnedTabId = false; - newWindow.tabs.some(curNewTab => { - if ( - curNewTab.url === curSessionTab.url || - curNewTab.pendingUrl === curSessionTab.url - ) { - pinnedTabId = curNewTab.id; - return true; - } - return false; + if (pinnedTabId) { + await chrome.tabs.update(pinnedTabId, { + pinned: true, }); - if (pinnedTabId) { - await chrome.tabs.update(pinnedTabId, { - pinned: true, - }); - } } } - - // if tabUrl is defined, then focus this tab - if (tabUrl) { - await focusOrLoadTabInWindow(newWindow, tabUrl); - } - - /* session.tabs.forEach(function (curTab) { - chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false}); - }); - - const tabs = await chrome.tabs.query({windowId: newWindow.id, index: 0}); - chrome.tabs.remove(tabs[0].id); */ - } - } - - async function handleLoadWindow(windowId, tabUrl) { - // assume window is already open, give it focus - if (windowId) { - await focusWindow(windowId); } // if tabUrl is defined, then focus this tab if (tabUrl) { - const theWin = await chrome.windows.get(windowId, { populate: true }); - await focusOrLoadTabInWindow(theWin, tabUrl); + await focusOrLoadTabInWindow(newWindow, tabUrl); } - } - async function focusWindow(windowId) { - await chrome.windows.update(windowId, { focused: true }); - } + /* session.tabs.forEach(function (curTab) { + chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false}); + }); - async function focusOrLoadTabInWindow(window, tabUrl) { - let match = false; - for (const tab of window.tabs) { - if (tab.url === tabUrl) { - await chrome.tabs.update(tab.id, { active: true }); - match = true; - break; - } - } + const tabs = await chrome.tabs.query({windowId: newWindow.id, index: 0}); + chrome.tabs.remove(tabs[0].id); */ + } +} - if (!match) { - await chrome.tabs.create({ url: tabUrl }); - } +async function handleLoadWindow(windowId, tabUrl) { + // assume window is already open, give it focus + if (windowId) { + await focusWindow(windowId); } - async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { - const curWindow = await chrome.windows.get(windowId, { populate: true }); - const existingSession = await dbService.fetchSessionByName(sessionName); + // if tabUrl is defined, then focus this tab + if (tabUrl) { + const theWin = await chrome.windows.get(windowId, { populate: true }); + await focusOrLoadTabInWindow(theWin, tabUrl); + } +} - // if session with same name already exist, then prompt to override the existing session - if (existingSession) { - if (!deleteOld) { - console.error( - `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` - ); - callback(false); - return; +async function focusWindow(windowId) { + await chrome.windows.update(windowId, { focused: true }); +} - // if we choose to overwrite, delete the existing session - } - handleDeleteSession(existingSession.id, noop); +async function focusOrLoadTabInWindow(window, tabUrl) { + let match = false; + for (const tab of window.tabs) { + if (tab.url === tabUrl) { + await chrome.tabs.update(tab.id, { active: true }); + match = true; + break; } - spacesService.saveNewSession( - sessionName, - curWindow.tabs, - curWindow.id, - callback - ); } - async function handleRestoreFromBackup(space, deleteOld, callback) { - const existingSession = space.name - ? await dbService.fetchSessionByName(space.name) - : false; + if (!match) { + await chrome.tabs.create({ url: tabUrl }); + } +} - // if session with same name already exist, then prompt to override the existing session - if (existingSession) { - if (!deleteOld) { - console.error( - `handleRestoreFromBackup: Session with name "${space.name}" already exists and deleteOld was not true.` - ); - callback(false); - return; +async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { + const curWindow = await chrome.windows.get(windowId, { populate: true }); + const existingSession = await dbService.fetchSessionByName(sessionName); - // if we choose to overwrite, delete the existing session - } - handleDeleteSession(existingSession.id, noop); - } + // if session with same name already exist, then prompt to override the existing session + if (existingSession) { + if (!deleteOld) { + console.error( + `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` + ); + callback(false); + return; - spacesService.saveNewSession( - space.name, - space.tabs, - false, - callback - ); + // if we choose to overwrite, delete the existing session + } + handleDeleteSession(existingSession.id, noop); } + spacesService.saveNewSession( + sessionName, + curWindow.tabs, + curWindow.id, + callback + ); +} - async function handleImportNewSession(urlList, callback) { - let tempName = 'Imported space: '; - let count = 1; +async function handleRestoreFromBackup(space, deleteOld, callback) { + const existingSession = space.name + ? await dbService.fetchSessionByName(space.name) + : false; - while (await dbService.fetchSessionByName(tempName + count)) { - count += 1; + // if session with same name already exist, then prompt to override the existing session + if (existingSession) { + if (!deleteOld) { + console.error( + `handleRestoreFromBackup: Session with name "${space.name}" already exists and deleteOld was not true.` + ); + callback(false); + return; + + // if we choose to overwrite, delete the existing session } + handleDeleteSession(existingSession.id, noop); + } - tempName += count; + spacesService.saveNewSession( + space.name, + space.tabs, + false, + callback + ); +} - const tabList = urlList.map(text => { - return { url: text }; - }); +async function handleImportNewSession(urlList, callback) { + let tempName = 'Imported space: '; + let count = 1; - // save session to database - spacesService.saveNewSession(tempName, tabList, false, callback); + while (await dbService.fetchSessionByName(tempName + count)) { + count += 1; } - async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { - // check to make sure session name doesn't already exist - const existingSession = await dbService.fetchSessionByName(sessionName); + tempName += count; - // if session with same name already exist, then prompt to override the existing session - if (existingSession) { - if (!deleteOld) { - console.error( - `handleUpdateSessionName: Session with name "${sessionName}" already exists and deleteOld was not true.` - ); - callback(false); - return; + const tabList = urlList.map(text => { + return { url: text }; + }); - // if we choose to override, then delete the existing session - } - handleDeleteSession(existingSession.id, noop); - } - spacesService.updateSessionName(sessionId, sessionName, callback); - } + // save session to database + spacesService.saveNewSession(tempName, tabList, false, callback); +} - async function handleDeleteSession(sessionId, callback) { - const session = await dbService.fetchSessionById(sessionId); - if (!session) { - console.error(`handleDeleteSession: No session found with id ${sessionId}`); +async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { + // check to make sure session name doesn't already exist + const existingSession = await dbService.fetchSessionByName(sessionName); + + // if session with same name already exist, then prompt to override the existing session + if (existingSession) { + if (!deleteOld) { + console.error( + `handleUpdateSessionName: Session with name "${sessionName}" already exists and deleteOld was not true.` + ); callback(false); return; + + // if we choose to override, then delete the existing session } + handleDeleteSession(existingSession.id, noop); + } + spacesService.updateSessionName(sessionId, sessionName, callback); +} - spacesService.deleteSession(sessionId, callback); +async function handleDeleteSession(sessionId, callback) { + const session = await dbService.fetchSessionById(sessionId); + if (!session) { + console.error(`handleDeleteSession: No session found with id ${sessionId}`); + callback(false); + return; } - async function handleAddLinkToNewSession(url, sessionName, callback) { - const session = await dbService.fetchSessionByName(sessionName); - const newTabs = [{ url }]; + spacesService.deleteSession(sessionId, callback); +} - // if we found a session matching this name then return as an error as we are - // supposed to be creating a new session with this name - if (session) { - callback(false); +async function handleAddLinkToNewSession(url, sessionName, callback) { + const session = await dbService.fetchSessionByName(sessionName); + const newTabs = [{ url }]; - // else create a new session with this name containing this url - } else { - spacesService.saveNewSession(sessionName, newTabs, false, callback); - } + // if we found a session matching this name then return as an error as we are + // supposed to be creating a new session with this name + if (session) { + callback(false); + + // else create a new session with this name containing this url + } else { + spacesService.saveNewSession(sessionName, newTabs, false, callback); } +} - async function handleMoveTabToNewSession(tabId, sessionName, callback) { - const tab = await requestTabDetail(tabId); - if (!tab) { - callback(false); - return; - } +async function handleMoveTabToNewSession(tabId, sessionName, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; + } - const session = await dbService.fetchSessionByName(sessionName); + const session = await dbService.fetchSessionByName(sessionName); - // if we found a session matching this name then return as an error as we are - // supposed to be creating a new session with this name - if (session) { - callback(false); + // if we found a session matching this name then return as an error as we are + // supposed to be creating a new session with this name + if (session) { + callback(false); - // else create a new session with this name containing this tab - } else { - // remove tab from current window (should generate window events) - chrome.tabs.remove(tab.id); - - // save session to database - spacesService.saveNewSession( - sessionName, - [tab], - false, - callback - ); - } + // else create a new session with this name containing this tab + } else { + // remove tab from current window (should generate window events) + chrome.tabs.remove(tab.id); + + // save session to database + spacesService.saveNewSession( + sessionName, + [tab], + false, + callback + ); } +} - async function handleAddLinkToSession(url, sessionId, callback) { - const session = await dbService.fetchSessionById(sessionId); - const newTabs = [{ url }]; +async function handleAddLinkToSession(url, sessionId, callback) { + const session = await dbService.fetchSessionById(sessionId); + const newTabs = [{ url }]; - // if we have not found a session matching this name then return as an error as we are - // supposed to be adding the tab to an existing session - if (!session) { - callback(false); - return; - } - // if session is currently open then add link directly - if (session.windowId) { - handleAddLinkToWindow(url, session.windowId, callback); + // if we have not found a session matching this name then return as an error as we are + // supposed to be adding the tab to an existing session + if (!session) { + callback(false); + return; + } + // if session is currently open then add link directly + if (session.windowId) { + handleAddLinkToWindow(url, session.windowId, callback); - // else add tab to saved session in database - } else { - // update session in db - session.tabs = session.tabs.concat(newTabs); - spacesService.updateSessionTabs(session.id, session.tabs, callback); - } + // else add tab to saved session in database + } else { + // update session in db + session.tabs = session.tabs.concat(newTabs); + spacesService.updateSessionTabs(session.id, session.tabs, callback); } +} - function handleAddLinkToWindow(url, windowId, callback) { - chrome.tabs.create({ windowId, url, active: false }); +function handleAddLinkToWindow(url, windowId, callback) { + chrome.tabs.create({ windowId, url, active: false }); - // NOTE: this move does not seem to trigger any tab event listeners - // so we need to update sessions manually - spacesService.queueWindowEvent(windowId); + // NOTE: this move does not seem to trigger any tab event listeners + // so we need to update sessions manually + spacesService.queueWindowEvent(windowId); - callback(true); + callback(true); +} + +async function handleMoveTabToSession(tabId, sessionId, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; } - async function handleMoveTabToSession(tabId, sessionId, callback) { - const tab = await requestTabDetail(tabId); - if (!tab) { - callback(false); + const session = await dbService.fetchSessionById(sessionId); + const newTabs = [tab]; + + // if we have not found a session matching this name then return as an error as we are + // supposed to be adding the tab to an existing session + if (!session) { + callback(false); + } else { + // if session is currently open then move it directly + if (session.windowId) { + moveTabToWindow(tab, session.windowId, callback); return; } - const session = await dbService.fetchSessionById(sessionId); - const newTabs = [tab]; - - // if we have not found a session matching this name then return as an error as we are - // supposed to be adding the tab to an existing session - if (!session) { - callback(false); - } else { - // if session is currently open then move it directly - if (session.windowId) { - moveTabToWindow(tab, session.windowId, callback); - return; - } - - // else add tab to saved session in database - // remove tab from current window - chrome.tabs.remove(tab.id); + // else add tab to saved session in database + // remove tab from current window + chrome.tabs.remove(tab.id); - // update session in db - session.tabs = session.tabs.concat(newTabs); - spacesService.updateSessionTabs( - session.id, - session.tabs, - callback - ); - } + // update session in db + session.tabs = session.tabs.concat(newTabs); + spacesService.updateSessionTabs( + session.id, + session.tabs, + callback + ); } +} - async function handleMoveTabToWindow(tabId, windowId, callback) { - const tab = await requestTabDetail(tabId); - if (!tab) { - callback(false); - return; - } - moveTabToWindow(tab, windowId, callback); +async function handleMoveTabToWindow(tabId, windowId, callback) { + const tab = await requestTabDetail(tabId); + if (!tab) { + callback(false); + return; } + moveTabToWindow(tab, windowId, callback); +} function moveTabToWindow(tab, windowId, callback) { chrome.tabs.move(tab.id, { windowId, index: -1 }); diff --git a/js/popup.js b/js/popup.js index d798a4b..1644ce9 100644 --- a/js/popup.js +++ b/js/popup.js @@ -3,221 +3,221 @@ import { spacesRenderer } from './spacesRenderer.js'; import { utils } from './utils.js'; - const UNSAVED_SESSION = '(unnamed window)'; - const NO_HOTKEY = 'no hotkey set'; - - const nodes = {}; - let globalCurrentSpace; - let globalTabId; - let globalUrl; - let globalWindowId; - let globalSessionName; - - /* - * POPUP INIT - */ - - document.addEventListener('DOMContentLoaded', async () => { - const url = utils.getHashVariable('url', window.location.href); - globalUrl = url !== '' ? decodeURIComponent(url) : false; - const currentWindow = await chrome.windows.getCurrent({ populate: true }); - const windowId = currentWindow.id; - globalWindowId = windowId !== '' ? windowId : false; - globalTabId = utils.getHashVariable('tabId', window.location.href); - const sessionName = utils.getHashVariable( - 'sessionName', - window.location.href - ); - globalSessionName = - sessionName && sessionName !== 'false' ? sessionName : false; - const action = utils.getHashVariable('action', window.location.href); - - const requestSpacePromise = globalWindowId - ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) - : chrome.runtime.sendMessage({ action: 'requestCurrentSpace' }); - - requestSpacePromise.then(space => { - globalCurrentSpace = space; - renderCommon(); - routeView(action); - }); - }); +const UNSAVED_SESSION = '(unnamed window)'; +const NO_HOTKEY = 'no hotkey set'; - function routeView(action) { - if (action === 'move') { - renderMoveCard(); - } else if (action === 'switch') { - renderSwitchCard(); - } else { - renderMainCard(); - } +const nodes = {}; +let globalCurrentSpace; +let globalTabId; +let globalUrl; +let globalWindowId; +let globalSessionName; + +/* + * POPUP INIT + */ + +document.addEventListener('DOMContentLoaded', async () => { + const url = utils.getHashVariable('url', window.location.href); + globalUrl = url !== '' ? decodeURIComponent(url) : false; + const currentWindow = await chrome.windows.getCurrent({ populate: true }); + const windowId = currentWindow.id; + globalWindowId = windowId !== '' ? windowId : false; + globalTabId = utils.getHashVariable('tabId', window.location.href); + const sessionName = utils.getHashVariable( + 'sessionName', + window.location.href + ); + globalSessionName = + sessionName && sessionName !== 'false' ? sessionName : false; + const action = utils.getHashVariable('action', window.location.href); + + const requestSpacePromise = globalWindowId + ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) + : chrome.runtime.sendMessage({ action: 'requestCurrentSpace' }); + + requestSpacePromise.then(space => { + globalCurrentSpace = space; + renderCommon(); + routeView(action); + }); +}); + +function routeView(action) { + if (action === 'move') { + renderMoveCard(); + } else if (action === 'switch') { + renderSwitchCard(); + } else { + renderMainCard(); } +} - /* - * COMMON - */ - - function renderCommon() { - document.getElementById( - 'activeSpaceTitle' - ).value = globalCurrentSpace.name - ? globalCurrentSpace.name - : UNSAVED_SESSION; - - document.querySelector('body').onkeyup = e => { - // listen for escape key - if (e.keyCode === 27) { - handleCloseAction(); - // } else if (e.keyCode === 13) { - // handleNameSave(); - } - }; - document.getElementById('spaceEdit').addEventListener('click', () => { +/* + * COMMON + */ + +function renderCommon() { + document.getElementById( + 'activeSpaceTitle' + ).value = globalCurrentSpace.name + ? globalCurrentSpace.name + : UNSAVED_SESSION; + + document.querySelector('body').onkeyup = e => { + // listen for escape key + if (e.keyCode === 27) { + handleCloseAction(); + // } else if (e.keyCode === 13) { + // handleNameSave(); + } + }; + document.getElementById('spaceEdit').addEventListener('click', () => { + handleNameEdit(); + }); + document + .getElementById('activeSpaceTitle') + .addEventListener('focus', () => { handleNameEdit(); }); - document - .getElementById('activeSpaceTitle') - .addEventListener('focus', () => { - handleNameEdit(); - }); - document.getElementById('activeSpaceTitle').onkeyup = e => { - // listen for enter key - if (e.keyCode === 13) { - document.getElementById('activeSpaceTitle').blur(); - } - }; - document - .getElementById('activeSpaceTitle') - .addEventListener('blur', () => { - handleNameSave(); - }); + document.getElementById('activeSpaceTitle').onkeyup = e => { + // listen for enter key + if (e.keyCode === 13) { + document.getElementById('activeSpaceTitle').blur(); + } + }; + document + .getElementById('activeSpaceTitle') + .addEventListener('blur', () => { + handleNameSave(); + }); +} + +function handleCloseAction() { + const opener = utils.getHashVariable('opener', window.location.href); + if (opener && opener === 'bg') { + chrome.runtime.sendMessage({ + action: 'requestClose', + }); + } else { + window.close(); } +} - function handleCloseAction() { - const opener = utils.getHashVariable('opener', window.location.href); - if (opener && opener === 'bg') { +/* + * MAIN POPUP VIEW + */ + +async function renderMainCard() { + const hotkeys = await requestHotkeys(); + document.querySelector( + '#switcherLink .hotkey' + ).innerHTML = hotkeys.switchCode ? hotkeys.switchCode : NO_HOTKEY; + document.querySelector( + '#moverLink .hotkey' + ).innerHTML = hotkeys.moveCode ? hotkeys.moveCode : NO_HOTKEY; + + const hotkeyEls = document.querySelectorAll('.hotkey'); + for (let i = 0; i < hotkeyEls.length; i += 1) { + hotkeyEls[i].addEventListener('click', () => { chrome.runtime.sendMessage({ - action: 'requestClose', + action: 'requestShowKeyboardShortcuts', }); - } else { window.close(); - } + }); } - /* - * MAIN POPUP VIEW - */ - - async function renderMainCard() { - const hotkeys = await requestHotkeys(); - document.querySelector( - '#switcherLink .hotkey' - ).innerHTML = hotkeys.switchCode ? hotkeys.switchCode : NO_HOTKEY; - document.querySelector( - '#moverLink .hotkey' - ).innerHTML = hotkeys.moveCode ? hotkeys.moveCode : NO_HOTKEY; - - const hotkeyEls = document.querySelectorAll('.hotkey'); - for (let i = 0; i < hotkeyEls.length; i += 1) { - hotkeyEls[i].addEventListener('click', () => { - chrome.runtime.sendMessage({ - action: 'requestShowKeyboardShortcuts', - }); - window.close(); + document + .querySelector('#allSpacesLink .optionText') + .addEventListener('click', () => { + chrome.runtime.sendMessage({ + action: 'requestShowSpaces', }); + window.close(); + }); + document + .querySelector('#switcherLink .optionText') + .addEventListener('click', async () => { + const params = await chrome.runtime.sendMessage({'action': 'switch'}); + if (!params) return; + window.location.hash = params; + window.location.reload(); + renderSwitchCard(); + }); + document + .querySelector('#moverLink .optionText') + .addEventListener('click', async () => { + const params = await chrome.runtime.sendMessage({'action': 'generatePopupParams'}); + if (!params) return; + window.location.hash = params; + window.location.reload(); + // renderMoveCard() + }); +} + +async function requestHotkeys() { + const commands = await chrome.commands.getAll(); + let switchStr; + let moveStr; + let spacesStr; + + commands.forEach(command => { + if (command.name === 'spaces-switch') { + switchStr = command.shortcut; + } else if (command.name === 'spaces-move') { + moveStr = command.shortcut; + } else if (command.name === 'spaces-open') { + spacesStr = command.shortcut; } + }); - document - .querySelector('#allSpacesLink .optionText') - .addEventListener('click', () => { - chrome.runtime.sendMessage({ - action: 'requestShowSpaces', - }); - window.close(); - }); - document - .querySelector('#switcherLink .optionText') - .addEventListener('click', async () => { - const params = await chrome.runtime.sendMessage({'action': 'switch'}); - if (!params) return; - window.location.hash = params; - window.location.reload(); - renderSwitchCard(); - }); - document - .querySelector('#moverLink .optionText') - .addEventListener('click', async () => { - const params = await chrome.runtime.sendMessage({'action': 'generatePopupParams'}); - if (!params) return; - window.location.hash = params; - window.location.reload(); - // renderMoveCard() - }); + return { + switchCode: switchStr, + moveCode: moveStr, + spacesCode: spacesStr, + }; +} + +function handleNameEdit() { + const inputEl = document.getElementById('activeSpaceTitle'); + inputEl.focus(); + if (inputEl.value === UNSAVED_SESSION) { + inputEl.value = ''; } +} - async function requestHotkeys() { - const commands = await chrome.commands.getAll(); - let switchStr; - let moveStr; - let spacesStr; - - commands.forEach(command => { - if (command.name === 'spaces-switch') { - switchStr = command.shortcut; - } else if (command.name === 'spaces-move') { - moveStr = command.shortcut; - } else if (command.name === 'spaces-open') { - spacesStr = command.shortcut; - } - }); +async function handleNameSave() { + const inputEl = document.getElementById('activeSpaceTitle'); + const newName = inputEl.value; - return { - switchCode: switchStr, - moveCode: moveStr, - spacesCode: spacesStr, - }; + if ( + newName === UNSAVED_SESSION || + newName === globalCurrentSpace.name + ) { + return; } - function handleNameEdit() { - const inputEl = document.getElementById('activeSpaceTitle'); - inputEl.focus(); - if (inputEl.value === UNSAVED_SESSION) { - inputEl.value = ''; - } + const canOverwrite = await utils.checkSessionOverwrite(newName); + if (!canOverwrite) { + return; } - async function handleNameSave() { - const inputEl = document.getElementById('activeSpaceTitle'); - const newName = inputEl.value; - - if ( - newName === UNSAVED_SESSION || - newName === globalCurrentSpace.name - ) { - return; - } - - const canOverwrite = await utils.checkSessionOverwrite(newName); - if (!canOverwrite) { - return; - } - - if (globalCurrentSpace.sessionId) { - chrome.runtime.sendMessage({ - action: 'updateSessionName', - deleteOld: true, - sessionName: newName, - sessionId: globalCurrentSpace.sessionId, - }); - } else { - chrome.runtime.sendMessage({ - action: 'saveNewSession', - deleteOld: true, - sessionName: newName, - windowId: globalCurrentSpace.windowId, - }); - } + if (globalCurrentSpace.sessionId) { + chrome.runtime.sendMessage({ + action: 'updateSessionName', + deleteOld: true, + sessionName: newName, + sessionId: globalCurrentSpace.sessionId, + }); + } else { + chrome.runtime.sendMessage({ + action: 'saveNewSession', + deleteOld: true, + sessionName: newName, + windowId: globalCurrentSpace.windowId, + }); } +} /* * SWITCHER VIEW @@ -246,98 +246,98 @@ async function renderSwitchCard() { }); } - function getSelectedSpace() { - return document.querySelector('.space.selected'); - } +function getSelectedSpace() { + return document.querySelector('.space.selected'); +} - async function handleSwitchAction(selectedSpaceEl) { - await chrome.runtime.sendMessage({ - action: 'switchToSpace', - sessionId: selectedSpaceEl.getAttribute('data-sessionId'), - windowId: selectedSpaceEl.getAttribute('data-windowId'), - }); - // Wait for the response from the background message handler before - // closing the window. - window.close(); - } +async function handleSwitchAction(selectedSpaceEl) { + await chrome.runtime.sendMessage({ + action: 'switchToSpace', + sessionId: selectedSpaceEl.getAttribute('data-sessionId'), + windowId: selectedSpaceEl.getAttribute('data-windowId'), + }); + // Wait for the response from the background message handler before + // closing the window. + window.close(); +} - /* - * MOVE VIEW - */ - - async function renderMoveCard() { - document.getElementById( - 'popupContainer' - ).innerHTML = document.getElementById('moveTemplate').innerHTML; - - // initialise global handles to key elements (singletons) - // nodes.home = document.getElementById('spacesHome'); - nodes.body = document.querySelector('body'); - nodes.spaceEditButton = document.getElementById('spaceEdit'); - nodes.moveForm = document.getElementById('spaceSelectForm'); - nodes.moveInput = document.getElementById('sessionsInput'); - nodes.activeSpaceTitle = document.getElementById('activeSpaceTitle'); - nodes.activeTabTitle = document.getElementById('activeTabTitle'); - nodes.activeTabFavicon = document.getElementById('activeTabFavicon'); - nodes.okButton = document.getElementById('moveBtn'); - nodes.cancelButton = document.getElementById('cancelBtn'); - - // nodes.home.setAttribute('href', chrome.extension.getURL('spaces.html')); - - nodes.moveForm.onsubmit = e => { - e.preventDefault(); - handleSelectAction(); - }; +/* + * MOVE VIEW + */ - nodes.body.onkeyup = e => { - // highlight ok button when you start typing - if (nodes.moveInput.value.length > 0) { - nodes.okButton.className = 'button okBtn selected'; - } else { - nodes.okButton.className = 'button okBtn'; - } +async function renderMoveCard() { + document.getElementById( + 'popupContainer' + ).innerHTML = document.getElementById('moveTemplate').innerHTML; + + // initialise global handles to key elements (singletons) + // nodes.home = document.getElementById('spacesHome'); + nodes.body = document.querySelector('body'); + nodes.spaceEditButton = document.getElementById('spaceEdit'); + nodes.moveForm = document.getElementById('spaceSelectForm'); + nodes.moveInput = document.getElementById('sessionsInput'); + nodes.activeSpaceTitle = document.getElementById('activeSpaceTitle'); + nodes.activeTabTitle = document.getElementById('activeTabTitle'); + nodes.activeTabFavicon = document.getElementById('activeTabFavicon'); + nodes.okButton = document.getElementById('moveBtn'); + nodes.cancelButton = document.getElementById('cancelBtn'); + + // nodes.home.setAttribute('href', chrome.extension.getURL('spaces.html')); + + nodes.moveForm.onsubmit = e => { + e.preventDefault(); + handleSelectAction(); + }; - // listen for escape key - if (e.keyCode === 27) { - handleCloseAction(); - } - }; + nodes.body.onkeyup = e => { + // highlight ok button when you start typing + if (nodes.moveInput.value.length > 0) { + nodes.okButton.className = 'button okBtn selected'; + } else { + nodes.okButton.className = 'button okBtn'; + } - nodes.spaceEditButton.onclick = () => { - handleEditSpace(); - }; - nodes.okButton.onclick = () => { - handleSelectAction(); - }; - nodes.cancelButton.onclick = () => { + // listen for escape key + if (e.keyCode === 27) { handleCloseAction(); - }; + } + }; + + nodes.spaceEditButton.onclick = () => { + handleEditSpace(); + }; + nodes.okButton.onclick = () => { + handleSelectAction(); + }; + nodes.cancelButton.onclick = () => { + handleCloseAction(); + }; - // update currentSpaceDiv - // nodes.windowTitle.innerHTML = "Current space: " + (globalSessionName ? globalSessionName : 'unnamed'); - nodes.activeSpaceTitle.innerHTML = globalSessionName || '(unnamed)'; - // selectSpace(nodes.activeSpace); + // update currentSpaceDiv + // nodes.windowTitle.innerHTML = "Current space: " + (globalSessionName ? globalSessionName : 'unnamed'); + nodes.activeSpaceTitle.innerHTML = globalSessionName || '(unnamed)'; + // selectSpace(nodes.activeSpace); - await updateTabDetails(); + await updateTabDetails(); - const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); - // remove currently visible space - const filteredSpaces = spaces.filter(space => { - return `${space.windowId}` !== globalWindowId; - }); - spacesRenderer.initialise(5, false); - spacesRenderer.renderSpaces(filteredSpaces); - - const allSpaceEls = document.querySelectorAll('.space'); - for (const el of allSpaceEls) { - // eslint-disable-next-line no-param-reassign - const existingClickHandler = el.onclick; - el.onclick = e => { - existingClickHandler(e); - handleSelectAction(); - }; - } + const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' }); + // remove currently visible space + const filteredSpaces = spaces.filter(space => { + return `${space.windowId}` !== globalWindowId; + }); + spacesRenderer.initialise(5, false); + spacesRenderer.renderSpaces(filteredSpaces); + + const allSpaceEls = document.querySelectorAll('.space'); + for (const el of allSpaceEls) { + // eslint-disable-next-line no-param-reassign + const existingClickHandler = el.onclick; + el.onclick = e => { + existingClickHandler(e); + handleSelectAction(); + }; } +} async function updateTabDetails() { let faviconSrc; @@ -389,52 +389,53 @@ async function updateTabDetails() { } } - function handleSelectAction() { - const selectedSpaceEl = document.querySelector('.space.selected'); - const sessionId = selectedSpaceEl.getAttribute('data-sessionId'); - const windowId = selectedSpaceEl.getAttribute('data-windowId'); - const newSessionName = nodes.moveInput.value; - const params = {}; - - if (sessionId && sessionId !== 'false') { - params.sessionId = sessionId; - - if (globalTabId) { - params.action = 'moveTabToSession'; - params.tabId = globalTabId; - } else if (globalUrl) { - params.action = 'addLinkToSession'; - params.url = globalUrl; - } - } else if (windowId && windowId !== 'false') { - params.windowId = windowId; - - if (globalTabId) { - params.action = 'moveTabToWindow'; - params.tabId = globalTabId; - } else if (globalUrl) { - params.action = 'addLinkToWindow'; - params.url = globalUrl; - } - } else { - params.sessionName = newSessionName; - - if (globalTabId) { - params.action = 'moveTabToNewSession'; - params.tabId = globalTabId; - } else if (globalUrl) { - params.action = 'addLinkToNewSession'; - params.url = globalUrl; - } +function handleSelectAction() { + const selectedSpaceEl = document.querySelector('.space.selected'); + const sessionId = selectedSpaceEl.getAttribute('data-sessionId'); + const windowId = selectedSpaceEl.getAttribute('data-windowId'); + const newSessionName = nodes.moveInput.value; + const params = {}; + + if (sessionId && sessionId !== 'false') { + params.sessionId = sessionId; + + if (globalTabId) { + params.action = 'moveTabToSession'; + params.tabId = globalTabId; + } else if (globalUrl) { + params.action = 'addLinkToSession'; + params.url = globalUrl; + } + } else if (windowId && windowId !== 'false') { + params.windowId = windowId; + + if (globalTabId) { + params.action = 'moveTabToWindow'; + params.tabId = globalTabId; + } else if (globalUrl) { + params.action = 'addLinkToWindow'; + params.url = globalUrl; + } + } else { + params.sessionName = newSessionName; + + if (globalTabId) { + params.action = 'moveTabToNewSession'; + params.tabId = globalTabId; + } else if (globalUrl) { + params.action = 'addLinkToNewSession'; + params.url = globalUrl; } - - chrome.runtime.sendMessage(params); - // this window will be closed by background script - } - function handleEditSpace() { - chrome.runtime.sendMessage({ - action: 'requestShowSpaces', - windowId: globalWindowId, - edit: 'true', - }); } + + chrome.runtime.sendMessage(params); + // this window will be closed by background script +} + +function handleEditSpace() { + chrome.runtime.sendMessage({ + action: 'requestShowSpaces', + windowId: globalWindowId, + edit: 'true', + }); +} diff --git a/js/spaces.js b/js/spaces.js index a8107ff..bbc9d2e 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -26,195 +26,195 @@ function renderSpacesList(spaces) { }); } - function renderSpaceListEl(space) { - let hash; +function renderSpaceListEl(space) { + let hash; - const listEl = document.createElement('li'); - const linkEl = document.createElement('a'); + const listEl = document.createElement('li'); + const linkEl = document.createElement('a'); - if (space.sessionId) { - hash = `#sessionId=${space.sessionId}`; - } else if (space.windowId) { - hash = `#windowId=${space.windowId}`; - } - linkEl.setAttribute('href', hash); - - if (space.name) { - linkEl.innerHTML = space.name; - } else { - linkEl.innerHTML = UNSAVED_SESSION; - } + if (space.sessionId) { + hash = `#sessionId=${space.sessionId}`; + } else if (space.windowId) { + hash = `#windowId=${space.windowId}`; + } + linkEl.setAttribute('href', hash); - if ( - globalSelectedSpace && - ((space.windowId && - globalSelectedSpace.windowId === space.windowId) || - (space.sessionId && - globalSelectedSpace.sessionId === space.sessionId)) - ) { - linkEl.className = 'selected'; - } + if (space.name) { + linkEl.innerHTML = space.name; + } else { + linkEl.innerHTML = UNSAVED_SESSION; + } - // if (space && !space.windowId) { - // iconEl.className = 'icon fa fa-external-link'; - // iconEl.setAttribute('title', 'Load this space'); - // } else { - // iconEl.className = 'icon fa fa-arrow-circle-right'; - // iconEl.setAttribute('title', 'Switch to this space'); - // } - // listEl.appendChild(iconEl); - - // //add event listener for each load/switch icon - // iconEl.addEventListener("click", () => { - // handleLoadSpace(space.sessionId, space.windowId); - // }); - - listEl.appendChild(linkEl); - return listEl; + if ( + globalSelectedSpace && + ((space.windowId && + globalSelectedSpace.windowId === space.windowId) || + (space.sessionId && + globalSelectedSpace.sessionId === space.sessionId)) + ) { + linkEl.className = 'selected'; } - // METHODS FOR RENDERING MAIN CONTENT (space detail) + // if (space && !space.windowId) { + // iconEl.className = 'icon fa fa-external-link'; + // iconEl.setAttribute('title', 'Load this space'); + // } else { + // iconEl.className = 'icon fa fa-arrow-circle-right'; + // iconEl.setAttribute('title', 'Switch to this space'); + // } + // listEl.appendChild(iconEl); + + // //add event listener for each load/switch icon + // iconEl.addEventListener("click", () => { + // handleLoadSpace(space.sessionId, space.windowId); + // }); + + listEl.appendChild(linkEl); + return listEl; +} - function renderSpaceDetail(space, editMode) { - updateNameForm(space); - toggleNameEditMode(editMode); - updateButtons(space); - renderTabs(space); - } +// METHODS FOR RENDERING MAIN CONTENT (space detail) - function updateNameForm(space) { - if (space && space.name) { - nodes.nameFormInput.value = space.name; - nodes.nameFormDisplay.innerHTML = space.name; - } else { - nodes.nameFormInput.value = ''; - if (space) { - nodes.nameFormDisplay.innerHTML = UNSAVED_SESSION; - } else { - nodes.nameFormDisplay.innerHTML = ''; - } - } - } +function renderSpaceDetail(space, editMode) { + updateNameForm(space); + toggleNameEditMode(editMode); + updateButtons(space); + renderTabs(space); +} - function toggleNameEditMode(visible) { - if (visible) { - nodes.nameFormDisplay.style.display = 'none'; - nodes.nameFormInput.style.display = 'inline'; - nodes.nameFormInput.focus(); +function updateNameForm(space) { + if (space && space.name) { + nodes.nameFormInput.value = space.name; + nodes.nameFormDisplay.innerHTML = space.name; + } else { + nodes.nameFormInput.value = ''; + if (space) { + nodes.nameFormDisplay.innerHTML = UNSAVED_SESSION; } else { - nodes.nameFormDisplay.style.display = 'inline'; - nodes.nameFormInput.style.display = 'none'; + nodes.nameFormDisplay.innerHTML = ''; } } +} - function updateButtons(space) { - const sessionId = space && space.sessionId ? space.sessionId : false; - const windowId = space && space.windowId ? space.windowId : false; - - nodes.actionSwitch.style.display = windowId ? 'inline-block' : 'none'; - nodes.actionOpen.style.display = - space && !windowId ? 'inline-block' : 'none'; - nodes.actionEdit.style.display = - sessionId || windowId ? 'inline-block' : 'none'; - nodes.actionExport.style.display = - sessionId || windowId ? 'inline-block' : 'none'; - nodes.actionDelete.style.display = - !windowId && sessionId ? 'inline-block' : 'none'; +function toggleNameEditMode(visible) { + if (visible) { + nodes.nameFormDisplay.style.display = 'none'; + nodes.nameFormInput.style.display = 'inline'; + nodes.nameFormInput.focus(); + } else { + nodes.nameFormDisplay.style.display = 'inline'; + nodes.nameFormInput.style.display = 'none'; } +} - function renderTabs(space) { - nodes.activeTabs.innerHTML = ''; - nodes.historicalTabs.innerHTML = ''; +function updateButtons(space) { + const sessionId = space && space.sessionId ? space.sessionId : false; + const windowId = space && space.windowId ? space.windowId : false; + + nodes.actionSwitch.style.display = windowId ? 'inline-block' : 'none'; + nodes.actionOpen.style.display = + space && !windowId ? 'inline-block' : 'none'; + nodes.actionEdit.style.display = + sessionId || windowId ? 'inline-block' : 'none'; + nodes.actionExport.style.display = + sessionId || windowId ? 'inline-block' : 'none'; + nodes.actionDelete.style.display = + !windowId && sessionId ? 'inline-block' : 'none'; +} - if (!space) { - nodes.spaceDetailContainer.style.display = 'none'; - } else { - nodes.spaceDetailContainer.style.display = 'block'; +function renderTabs(space) { + nodes.activeTabs.innerHTML = ''; + nodes.historicalTabs.innerHTML = ''; + + if (!space) { + nodes.spaceDetailContainer.style.display = 'none'; + } else { + nodes.spaceDetailContainer.style.display = 'block'; - space.tabs.forEach(tab => { - nodes.activeTabs.appendChild(renderTabListEl(tab, space)); + space.tabs.forEach(tab => { + nodes.activeTabs.appendChild(renderTabListEl(tab, space)); + }); + if (space.history) { + space.history.forEach(tab => { + nodes.historicalTabs.appendChild( + renderTabListEl(tab, space) + ); }); - if (space.history) { - space.history.forEach(tab => { - nodes.historicalTabs.appendChild( - renderTabListEl(tab, space) - ); - }); - } else { - // TODO: hide historical tabs section - } + } else { + // TODO: hide historical tabs section } } +} - function renderTabListEl(tab, space) { - let faviconSrc; - - const listEl = document.createElement('li'); - const linkEl = document.createElement('a'); - const faviconEl = document.createElement('img'); +function renderTabListEl(tab, space) { + let faviconSrc; - // try to get best favicon url path - if (tab.favIconUrl && tab.favIconUrl.indexOf('chrome://theme') < 0) { - faviconSrc = tab.favIconUrl; - } else { - // TODO(codedread): Fix this, it errors. - //faviconSrc = `chrome://favicon/${tab.url}`; - } - faviconEl.setAttribute('src', faviconSrc); + const listEl = document.createElement('li'); + const linkEl = document.createElement('a'); + const faviconEl = document.createElement('img'); - linkEl.innerHTML = tab.title ? tab.title : tab.url; - linkEl.setAttribute('href', tab.url); - linkEl.setAttribute('target', '_blank'); + // try to get best favicon url path + if (tab.favIconUrl && tab.favIconUrl.indexOf('chrome://theme') < 0) { + faviconSrc = tab.favIconUrl; + } else { + // TODO(codedread): Fix this, it errors. + //faviconSrc = `chrome://favicon/${tab.url}`; + } + faviconEl.setAttribute('src', faviconSrc); - // add event listener for each tab link - linkEl.addEventListener('click', e => { - e.preventDefault(); - handleLoadTab(space.sessionId, space.windowId, tab.url); - }); + linkEl.innerHTML = tab.title ? tab.title : tab.url; + linkEl.setAttribute('href', tab.url); + linkEl.setAttribute('target', '_blank'); - if (tab.duplicate) { - linkEl.className = 'duplicate'; - } + // add event listener for each tab link + linkEl.addEventListener('click', e => { + e.preventDefault(); + handleLoadTab(space.sessionId, space.windowId, tab.url); + }); - listEl.appendChild(faviconEl); - listEl.appendChild(linkEl); - return listEl; + if (tab.duplicate) { + linkEl.className = 'duplicate'; } - function initialiseBanner(spaces) { - let savedSpacesExist = false; + listEl.appendChild(faviconEl); + listEl.appendChild(linkEl); + return listEl; +} + +function initialiseBanner(spaces) { + let savedSpacesExist = false; - savedSpacesExist = spaces.some(space => { - if (space.name) return true; - return false; - }); + savedSpacesExist = spaces.some(space => { + if (space.name) return true; + return false; + }); - if (!savedSpacesExist) { - setBannerState(1); - } + if (!savedSpacesExist) { + setBannerState(1); } +} - async function setBannerState(state) { - const lessonOneEl = document.getElementById('lessonOne'); - const lessonTwoEl = document.getElementById('lessonTwo'); - - if (state !== bannerState) { - bannerState = state; - - await toggleBanner(false); - if (state > 0) { - nodes.banner.style.display = 'block'; - if (state === 1) { - lessonOneEl.style.display = 'block'; - lessonTwoEl.style.display = 'none'; - } else if (state === 2) { - lessonOneEl.style.display = 'none'; - lessonTwoEl.style.display = 'block'; - } - await toggleBanner(true); +async function setBannerState(state) { + const lessonOneEl = document.getElementById('lessonOne'); + const lessonTwoEl = document.getElementById('lessonTwo'); + + if (state !== bannerState) { + bannerState = state; + + await toggleBanner(false); + if (state > 0) { + nodes.banner.style.display = 'block'; + if (state === 1) { + lessonOneEl.style.display = 'block'; + lessonTwoEl.style.display = 'none'; + } else if (state === 2) { + lessonOneEl.style.display = 'none'; + lessonTwoEl.style.display = 'block'; } + await toggleBanner(true); } } +} async function toggleBanner(visible) { return new Promise(resolve => { @@ -225,220 +225,220 @@ async function toggleBanner(visible) { }); } - function toggleModal(visible) { - nodes.modalBlocker.style.display = visible ? 'block' : 'none'; - nodes.modalContainer.style.display = visible ? 'block' : 'none'; +function toggleModal(visible) { + nodes.modalBlocker.style.display = visible ? 'block' : 'none'; + nodes.modalContainer.style.display = visible ? 'block' : 'none'; - if (visible) { - nodes.modalInput.value = ''; - nodes.modalInput.focus(); - } + if (visible) { + nodes.modalInput.value = ''; + nodes.modalInput.focus(); } +} - // ACTION HANDLERS +// ACTION HANDLERS - async function handleLoadSpace(sessionId, windowId) { - if (sessionId) { - await performLoadSession(sessionId); - reroute(sessionId, false, false); - } else if (windowId) { - await performLoadWindow(windowId); - reroute(false, windowId, false); - } +async function handleLoadSpace(sessionId, windowId) { + if (sessionId) { + await performLoadSession(sessionId); + reroute(sessionId, false, false); + } else if (windowId) { + await performLoadWindow(windowId); + reroute(false, windowId, false); } +} - async function handleLoadTab(sessionId, windowId, tabUrl) { - const noop = () => {}; +async function handleLoadTab(sessionId, windowId, tabUrl) { + const noop = () => {}; - if (sessionId) { - await performLoadTabInSession(sessionId, tabUrl, noop); - } else if (windowId) { - await performLoadTabInWindow(windowId, tabUrl, noop); - } + if (sessionId) { + await performLoadTabInSession(sessionId, tabUrl, noop); + } else if (windowId) { + await performLoadTabInWindow(windowId, tabUrl, noop); } +} - // if background page requests this page update, then assume we need to do a full page update - function handleAutoUpdateRequest(spaces) { - let matchingSpaces; - let selectedSpace; - - // re-render main spaces list - updateSpacesList(spaces); - - // if we are currently viewing a space detail then update this object from returned spaces list - if (globalSelectedSpace) { - // look for currently selected space by sessionId - if (globalSelectedSpace.sessionId) { - matchingSpaces = spaces.filter(curSpace => { - return curSpace.sessionId === globalSelectedSpace.sessionId; - }); - if (matchingSpaces.length === 1) { - [selectedSpace] = matchingSpaces; - } - - // else look for currently selected space by windowId - } else if (globalSelectedSpace.windowId) { - matchingSpaces = spaces.filter(curSpace => { - return curSpace.windowId === globalSelectedSpace.windowId; - }); - if (matchingSpaces.length === 1) { - [selectedSpace] = matchingSpaces; - } +// if background page requests this page update, then assume we need to do a full page update +function handleAutoUpdateRequest(spaces) { + let matchingSpaces; + let selectedSpace; + + // re-render main spaces list + updateSpacesList(spaces); + + // if we are currently viewing a space detail then update this object from returned spaces list + if (globalSelectedSpace) { + // look for currently selected space by sessionId + if (globalSelectedSpace.sessionId) { + matchingSpaces = spaces.filter(curSpace => { + return curSpace.sessionId === globalSelectedSpace.sessionId; + }); + if (matchingSpaces.length === 1) { + [selectedSpace] = matchingSpaces; } - // update cache and re-render space detail view - if (selectedSpace) { - globalSelectedSpace = selectedSpace; - updateSpaceDetail(true); - } else { - reroute(false, false, true); + // else look for currently selected space by windowId + } else if (globalSelectedSpace.windowId) { + matchingSpaces = spaces.filter(curSpace => { + return curSpace.windowId === globalSelectedSpace.windowId; + }); + if (matchingSpaces.length === 1) { + [selectedSpace] = matchingSpaces; } } - } - - async function handleNameSave() { - const newName = nodes.nameFormInput.value; - const oldName = globalSelectedSpace.name; - const { sessionId } = globalSelectedSpace; - const { windowId } = globalSelectedSpace; - // if invalid name set then revert back to non-edit mode - if (newName === oldName || newName.trim() === '') { - updateNameForm(globalSelectedSpace); - toggleNameEditMode(false); - return; + // update cache and re-render space detail view + if (selectedSpace) { + globalSelectedSpace = selectedSpace; + updateSpaceDetail(true); + } else { + reroute(false, false, true); } + } +} - const canOverwrite = await utils.checkSessionOverwrite(newName); - if (!canOverwrite) { - return; - } +async function handleNameSave() { + const newName = nodes.nameFormInput.value; + const oldName = globalSelectedSpace.name; + const { sessionId } = globalSelectedSpace; + const { windowId } = globalSelectedSpace; + + // if invalid name set then revert back to non-edit mode + if (newName === oldName || newName.trim() === '') { + updateNameForm(globalSelectedSpace); + toggleNameEditMode(false); + return; + } - // otherwise call the save service - if (sessionId) { - const session = await performSessionUpdate(newName, sessionId); - if (session) reroute(session.id, false, true); - } else if (windowId) { - const session = await performNewSessionSave(newName, windowId); - if (session) reroute(session.id, false, true); - } + const canOverwrite = await utils.checkSessionOverwrite(newName); + if (!canOverwrite) { + return; + } - // handle banner - if (bannerState === 1) { - setBannerState(2); - } + // otherwise call the save service + if (sessionId) { + const session = await performSessionUpdate(newName, sessionId); + if (session) reroute(session.id, false, true); + } else if (windowId) { + const session = await performNewSessionSave(newName, windowId); + if (session) reroute(session.id, false, true); } - async function handleDelete() { - const { sessionId } = globalSelectedSpace; - if (sessionId) { - const session = await fetchSpaceDetail(sessionId, false); - if (!session) { - console.error( - `handleDelete: No session found with id ${sessionId}` - ); - return; - } - const sessionName = session.name || UNSAVED_SESSION_NAME; - const confirm = window.confirm( - `Are you sure you want to delete the space: ${sessionName}?` + // handle banner + if (bannerState === 1) { + setBannerState(2); + } +} + +async function handleDelete() { + const { sessionId } = globalSelectedSpace; + if (sessionId) { + const session = await fetchSpaceDetail(sessionId, false); + if (!session) { + console.error( + `handleDelete: No session found with id ${sessionId}` ); + return; + } + const sessionName = session.name || UNSAVED_SESSION_NAME; + const confirm = window.confirm( + `Are you sure you want to delete the space: ${sessionName}?` + ); - if (confirm) { - await performDelete(sessionId); - updateSpacesList(); - reroute(false, false, true); - } + if (confirm) { + await performDelete(sessionId); + updateSpacesList(); + reroute(false, false, true); } } +} - // import accepts either a newline separated list of urls or a json backup object - async function handleImport() { - let urlList; - let spaces; +// import accepts either a newline separated list of urls or a json backup object +async function handleImport() { + let urlList; + let spaces; - const rawInput = nodes.modalInput.value; + const rawInput = nodes.modalInput.value; - // check for json object - try { - spaces = JSON.parse(rawInput); - await performRestoreFromBackup(spaces); - updateSpacesList(); - } catch (e) { - // otherwise treat as a list of newline separated urls - if (rawInput.trim().length > 0) { - urlList = rawInput.split('\n'); - - // filter out bad urls - urlList = urlList.filter(url => { - if (url.trim().length > 0 && url.indexOf('://') > 0) - return true; - return false; - }); - - if (urlList.length > 0) { - const session = await performSessionImport(urlList); - if (session) reroute(session.id, false, true); - } + // check for json object + try { + spaces = JSON.parse(rawInput); + await performRestoreFromBackup(spaces); + updateSpacesList(); + } catch (e) { + // otherwise treat as a list of newline separated urls + if (rawInput.trim().length > 0) { + urlList = rawInput.split('\n'); + + // filter out bad urls + urlList = urlList.filter(url => { + if (url.trim().length > 0 && url.indexOf('://') > 0) + return true; + return false; + }); + + if (urlList.length > 0) { + const session = await performSessionImport(urlList); + if (session) reroute(session.id, false, true); } } } +} - async function handleBackup() { - // strip out unnessary content from each space - const leanSpaces = (await fetchAllSpaces()).map(space => { - return { - name: space.name, - tabs: space.tabs.map(curTab => { - return { - title: curTab.title, - url: normaliseTabUrl(curTab.url), - favIconUrl: curTab.favIconUrl, - }; - }), - }; - }); +async function handleBackup() { + // strip out unnessary content from each space + const leanSpaces = (await fetchAllSpaces()).map(space => { + return { + name: space.name, + tabs: space.tabs.map(curTab => { + return { + title: curTab.title, + url: normaliseTabUrl(curTab.url), + favIconUrl: curTab.favIconUrl, + }; + }), + }; + }); - const blob = new Blob([JSON.stringify(leanSpaces)], { - type: 'application/json', - }); - const blobUrl = URL.createObjectURL(blob); - const filename = 'spaces-backup.json'; - const link = document.createElement('a'); - link.setAttribute('href', blobUrl); - link.setAttribute('download', filename); - link.click(); - } + const blob = new Blob([JSON.stringify(leanSpaces)], { + type: 'application/json', + }); + const blobUrl = URL.createObjectURL(blob); + const filename = 'spaces-backup.json'; + const link = document.createElement('a'); + link.setAttribute('href', blobUrl); + link.setAttribute('download', filename); + link.click(); +} - async function handleExport() { - const { sessionId } = globalSelectedSpace; - const { windowId } = globalSelectedSpace; - let csvContent = ''; - let dataString = ''; +async function handleExport() { + const { sessionId } = globalSelectedSpace; + const { windowId } = globalSelectedSpace; + let csvContent = ''; + let dataString = ''; - const space = await fetchSpaceDetail(sessionId, windowId); - space.tabs.forEach(curTab => { - const url = normaliseTabUrl(curTab.url); - dataString += `${url}\n`; - }); - csvContent += dataString; - - const blob = new Blob([csvContent], { type: 'text/plain' }); - const blobUrl = URL.createObjectURL(blob); - const filename = `${space.name || 'untitled'}.txt`; - const link = document.createElement('a'); - link.setAttribute('href', blobUrl); - link.setAttribute('download', filename); - link.click(); - } + const space = await fetchSpaceDetail(sessionId, windowId); + space.tabs.forEach(curTab => { + const url = normaliseTabUrl(curTab.url); + dataString += `${url}\n`; + }); + csvContent += dataString; + + const blob = new Blob([csvContent], { type: 'text/plain' }); + const blobUrl = URL.createObjectURL(blob); + const filename = `${space.name || 'untitled'}.txt`; + const link = document.createElement('a'); + link.setAttribute('href', blobUrl); + link.setAttribute('download', filename); + link.click(); +} - function normaliseTabUrl(url) { - let normalisedUrl = url; - if (url.indexOf('suspended.html') > 0 && url.indexOf('uri=') > 0) { - normalisedUrl = url.substring(url.indexOf('uri=') + 4, url.length); - } - return normalisedUrl; +function normaliseTabUrl(url) { + let normalisedUrl = url; + if (url.indexOf('suspended.html') > 0 && url.indexOf('uri=') > 0) { + normalisedUrl = url.substring(url.indexOf('uri=') + 4, url.length); } + return normalisedUrl; +} // SERVICES @@ -544,210 +544,210 @@ async function performRestoreFromBackup(spaces) { } } - // EVENT LISTENERS FOR STATIC DOM ELEMENTS - - function addEventListeners() { - // register hashchange listener - window.onhashchange = () => { - updateSpacesList(); - updateSpaceDetail(); - }; - - // register incoming events listener - chrome.runtime.onMessage.addListener(request => { - if (request.action === 'updateSpaces' && request.spaces) { - handleAutoUpdateRequest(request.spaces); - } - }); +// EVENT LISTENERS FOR STATIC DOM ELEMENTS - // register dom listeners - nodes.nameFormDisplay.addEventListener('click', () => { - toggleNameEditMode(true); - }); - nodes.nameFormInput.addEventListener('blur', () => { - handleNameSave(); - }); - nodes.nameForm.addEventListener('submit', e => { - e.preventDefault(); - handleNameSave(); - }); - nodes.actionSwitch.addEventListener('click', () => { - handleLoadSpace( - globalSelectedSpace.sessionId, - globalSelectedSpace.windowId - ); - }); - nodes.actionOpen.addEventListener('click', () => { - handleLoadSpace(globalSelectedSpace.sessionId, false); - }); - nodes.actionEdit.addEventListener('click', () => { - toggleNameEditMode(true); - }); - nodes.actionExport.addEventListener('click', () => { - handleExport(); - }); - nodes.actionBackup.addEventListener('click', () => { - handleBackup(); - }); - nodes.actionDelete.addEventListener('click', () => { - handleDelete(); - }); - nodes.actionImport.addEventListener('click', e => { - e.preventDefault(); - toggleModal(true); - }); - nodes.modalBlocker.addEventListener('click', () => { - toggleModal(false); - }); - nodes.modalButton.addEventListener('click', () => { - handleImport(); - toggleModal(false); - }); - } +function addEventListeners() { + // register hashchange listener + window.onhashchange = () => { + updateSpacesList(); + updateSpaceDetail(); + }; - // ROUTING + // register incoming events listener + chrome.runtime.onMessage.addListener(request => { + if (request.action === 'updateSpaces' && request.spaces) { + handleAutoUpdateRequest(request.spaces); + } + }); - // update the hash with new ids (can trigger page re-render) - function reroute(sessionId, windowId, forceRerender) { - let hash; + // register dom listeners + nodes.nameFormDisplay.addEventListener('click', () => { + toggleNameEditMode(true); + }); + nodes.nameFormInput.addEventListener('blur', () => { + handleNameSave(); + }); + nodes.nameForm.addEventListener('submit', e => { + e.preventDefault(); + handleNameSave(); + }); + nodes.actionSwitch.addEventListener('click', () => { + handleLoadSpace( + globalSelectedSpace.sessionId, + globalSelectedSpace.windowId + ); + }); + nodes.actionOpen.addEventListener('click', () => { + handleLoadSpace(globalSelectedSpace.sessionId, false); + }); + nodes.actionEdit.addEventListener('click', () => { + toggleNameEditMode(true); + }); + nodes.actionExport.addEventListener('click', () => { + handleExport(); + }); + nodes.actionBackup.addEventListener('click', () => { + handleBackup(); + }); + nodes.actionDelete.addEventListener('click', () => { + handleDelete(); + }); + nodes.actionImport.addEventListener('click', e => { + e.preventDefault(); + toggleModal(true); + }); + nodes.modalBlocker.addEventListener('click', () => { + toggleModal(false); + }); + nodes.modalButton.addEventListener('click', () => { + handleImport(); + toggleModal(false); + }); +} - hash = '#'; - if (sessionId) { - hash += `sessionId=${sessionId}`; - } else if (windowId) { - hash += `windowId=${sessionId}`; - } +// ROUTING - // if hash hasn't changed page will not trigger onhashchange event - if (window.location.hash === hash) { - if (forceRerender) { - updateSpacesList(); - updateSpaceDetail(); - } +// update the hash with new ids (can trigger page re-render) +function reroute(sessionId, windowId, forceRerender) { + let hash; - // otherwise set new hash and let the change listener call routeHash - } else { - window.location.hash = hash; - } + hash = '#'; + if (sessionId) { + hash += `sessionId=${sessionId}`; + } else if (windowId) { + hash += `windowId=${sessionId}`; } - function getVariableFromHash(key) { - if (window.location.hash.length > 0) { - const hash = window.location.hash.substr( - 1, - window.location.hash.length - ); - const pairs = hash.split('&'); + // if hash hasn't changed page will not trigger onhashchange event + if (window.location.hash === hash) { + if (forceRerender) { + updateSpacesList(); + updateSpaceDetail(); + } - let matchedVal; - const match = pairs.some(curPair => { - const [curKey, curVal] = curPair.split('='); - if (curKey === key) { - matchedVal = curVal; - return true; - } - return false; - }); + // otherwise set new hash and let the change listener call routeHash + } else { + window.location.hash = hash; + } +} - if (match) { - return matchedVal; +function getVariableFromHash(key) { + if (window.location.hash.length > 0) { + const hash = window.location.hash.substr( + 1, + window.location.hash.length + ); + const pairs = hash.split('&'); + + let matchedVal; + const match = pairs.some(curPair => { + const [curKey, curVal] = curPair.split('='); + if (curKey === key) { + matchedVal = curVal; + return true; } + return false; + }); + + if (match) { + return matchedVal; } - return false; } + return false; +} - async function updateSpacesList(spaces) { - // if spaces passed in then re-render immediately - if (spaces) { - renderSpacesList(spaces); +async function updateSpacesList(spaces) { + // if spaces passed in then re-render immediately + if (spaces) { + renderSpacesList(spaces); - // otherwise do a fetch of spaces first - } else { - const newSpaces = await fetchAllSpaces(); - renderSpacesList(newSpaces); + // otherwise do a fetch of spaces first + } else { + const newSpaces = await fetchAllSpaces(); + renderSpacesList(newSpaces); - // determine if welcome banner should show - initialiseBanner(newSpaces); - } + // determine if welcome banner should show + initialiseBanner(newSpaces); } +} - async function updateSpaceDetail(useCachedSpace) { - const sessionId = getVariableFromHash('sessionId'); - const windowId = getVariableFromHash('windowId'); - const editMode = getVariableFromHash('editMode'); +async function updateSpaceDetail(useCachedSpace) { + const sessionId = getVariableFromHash('sessionId'); + const windowId = getVariableFromHash('windowId'); + const editMode = getVariableFromHash('editMode'); - // use cached currently selected space - if (useCachedSpace) { - addDuplicateMetadata(globalSelectedSpace); - renderSpaceDetail(globalSelectedSpace, editMode); + // use cached currently selected space + if (useCachedSpace) { + addDuplicateMetadata(globalSelectedSpace); + renderSpaceDetail(globalSelectedSpace, editMode); - // otherwise refetch space based on hashvars - } else if (sessionId || windowId) { - const space = await fetchSpaceDetail(sessionId, windowId); - addDuplicateMetadata(space); + // otherwise refetch space based on hashvars + } else if (sessionId || windowId) { + const space = await fetchSpaceDetail(sessionId, windowId); + addDuplicateMetadata(space); - // cache current selected space - globalSelectedSpace = space; - renderSpaceDetail(space, editMode); + // cache current selected space + globalSelectedSpace = space; + renderSpaceDetail(space, editMode); - // otherwise hide space detail view - } else { - // clear cache - globalSelectedSpace = false; - renderSpaceDetail(false, false); - } + // otherwise hide space detail view + } else { + // clear cache + globalSelectedSpace = false; + renderSpaceDetail(false, false); } +} - function addDuplicateMetadata(space) { - const dupeCounts = {}; +function addDuplicateMetadata(space) { + const dupeCounts = {}; - space.tabs.forEach(tab => { - // eslint-disable-next-line no-param-reassign - tab.title = tab.title || tab.url; - dupeCounts[tab.title] = dupeCounts[tab.title] - ? dupeCounts[tab.title] + 1 - : 1; - }); - space.tabs.forEach(tab => { - // eslint-disable-next-line no-param-reassign - tab.duplicate = dupeCounts[tab.title] > 1; - }); - } - - window.onload = () => { - // initialise global handles to key elements (singletons) - nodes.home = document.getElementById('spacesHome'); - nodes.openSpaces = document.getElementById('openSpaces'); - nodes.closedSpaces = document.getElementById('closedSpaces'); - nodes.activeTabs = document.getElementById('activeTabs'); - nodes.historicalTabs = document.getElementById('historicalTabs'); - nodes.spaceDetailContainer = document.querySelector( - '.content .contentBody' - ); - nodes.nameForm = document.querySelector('#nameForm'); - nodes.nameFormDisplay = document.querySelector('#nameForm span'); - nodes.nameFormInput = document.querySelector('#nameForm input'); - nodes.actionSwitch = document.getElementById('actionSwitch'); - nodes.actionOpen = document.getElementById('actionOpen'); - nodes.actionEdit = document.getElementById('actionEdit'); - nodes.actionExport = document.getElementById('actionExport'); - nodes.actionBackup = document.getElementById('actionBackup'); - nodes.actionDelete = document.getElementById('actionDelete'); - nodes.actionImport = document.getElementById('actionImport'); - nodes.banner = document.getElementById('banner'); - nodes.modalBlocker = document.querySelector('.blocker'); - nodes.modalContainer = document.querySelector('.modal'); - nodes.modalInput = document.getElementById('importTextArea'); - nodes.modalButton = document.getElementById('importBtn'); - - nodes.home.setAttribute('href', chrome.runtime.getURL('spaces.html')); - - // initialise event listeners for static elements - addEventListeners(); - - // render side nav - updateSpacesList(); + space.tabs.forEach(tab => { + // eslint-disable-next-line no-param-reassign + tab.title = tab.title || tab.url; + dupeCounts[tab.title] = dupeCounts[tab.title] + ? dupeCounts[tab.title] + 1 + : 1; + }); + space.tabs.forEach(tab => { + // eslint-disable-next-line no-param-reassign + tab.duplicate = dupeCounts[tab.title] > 1; + }); +} - // render main content - updateSpaceDetail(); - }; +window.onload = () => { + // initialise global handles to key elements (singletons) + nodes.home = document.getElementById('spacesHome'); + nodes.openSpaces = document.getElementById('openSpaces'); + nodes.closedSpaces = document.getElementById('closedSpaces'); + nodes.activeTabs = document.getElementById('activeTabs'); + nodes.historicalTabs = document.getElementById('historicalTabs'); + nodes.spaceDetailContainer = document.querySelector( + '.content .contentBody' + ); + nodes.nameForm = document.querySelector('#nameForm'); + nodes.nameFormDisplay = document.querySelector('#nameForm span'); + nodes.nameFormInput = document.querySelector('#nameForm input'); + nodes.actionSwitch = document.getElementById('actionSwitch'); + nodes.actionOpen = document.getElementById('actionOpen'); + nodes.actionEdit = document.getElementById('actionEdit'); + nodes.actionExport = document.getElementById('actionExport'); + nodes.actionBackup = document.getElementById('actionBackup'); + nodes.actionDelete = document.getElementById('actionDelete'); + nodes.actionImport = document.getElementById('actionImport'); + nodes.banner = document.getElementById('banner'); + nodes.modalBlocker = document.querySelector('.blocker'); + nodes.modalContainer = document.querySelector('.modal'); + nodes.modalInput = document.getElementById('importTextArea'); + nodes.modalButton = document.getElementById('importBtn'); + + nodes.home.setAttribute('href', chrome.runtime.getURL('spaces.html')); + + // initialise event listeners for static elements + addEventListeners(); + + // render side nav + updateSpacesList(); + + // render main content + updateSpaceDetail(); +}; From e8dcbab66218a082e10639b18349b4a0c6161ff5 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:10:52 -0700 Subject: [PATCH 48/92] Convert processMessage() into an async function in a continued effort to eliminate callback fun. --- js/popup.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/js/popup.js b/js/popup.js index 1644ce9..f859b65 100644 --- a/js/popup.js +++ b/js/popup.js @@ -13,9 +13,9 @@ let globalUrl; let globalWindowId; let globalSessionName; -/* - * POPUP INIT - */ +/** + * POPUP INIT + */ document.addEventListener('DOMContentLoaded', async () => { const url = utils.getHashVariable('url', window.location.href); @@ -53,9 +53,9 @@ function routeView(action) { } } -/* - * COMMON - */ +/** + * COMMON + */ function renderCommon() { document.getElementById( @@ -104,9 +104,9 @@ function handleCloseAction() { } } -/* - * MAIN POPUP VIEW - */ +/** + * MAIN POPUP VIEW + */ async function renderMainCard() { const hotkeys = await requestHotkeys(); @@ -219,7 +219,7 @@ async function handleNameSave() { } } -/* +/** * SWITCHER VIEW */ @@ -261,9 +261,9 @@ async function handleSwitchAction(selectedSpaceEl) { window.close(); } -/* - * MOVE VIEW - */ +/** + * MOVE VIEW + */ async function renderMoveCard() { document.getElementById( From 4e49e1997b305b0c2bbac1f26d4fdd8534d429ce Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:11:06 -0700 Subject: [PATCH 49/92] Woops, missed this on last commit --- js/background/background.js | 139 ++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index eada299..c015451 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -177,8 +177,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } // Ensure spacesService is initialized before processing any message - spacesService.ensureInitialized().then(() => { - const result = processMessage(request, sender, sendResponse); + spacesService.ensureInitialized().then(async () => { + const result = await processMessage(request, sender, sendResponse); // If processMessage returns false, we need to handle that by not sending a response // But since we're in an async context, we can't change the outer return value // The key is that processMessage will call sendResponse() for true cases @@ -190,7 +190,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; }); -function processMessage(request, sender, sendResponse) { +async function processMessage(request, sender, sendResponse) { let sessionId; let windowId; let tabId; @@ -198,35 +198,32 @@ function processMessage(request, sender, sendResponse) { // endpoints called by spaces.js switch (request.action) { case 'requestSessionPresence': - const sessionPresence = requestSessionPresence(request.sessionName); + const sessionPresence = await requestSessionPresence(request.sessionName); sendResponse(sessionPresence); return true; case 'requestSpaceFromWindowId': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); if (windowId) { - requestSpaceFromWindowId(windowId).then(space => { - sendResponse(space); - }); + const space = await requestSpaceFromWindowId(windowId); + sendResponse(space); } return true; case 'requestCurrentSpace': - requestCurrentSpace().then(space => { - sendResponse(space); - }); + const currentSpace = await requestCurrentSpace(); + sendResponse(currentSpace); return true; case 'generatePopupParams': - generatePopupParams(request.action, request.tabUrl).then(params => { - sendResponse(params); - }); + const params = await generatePopupParams(request.action, request.tabUrl); + sendResponse(params); return true; case 'loadSession': - sessionId = _cleanParameter(request.sessionId); + sessionId = cleanParameter(request.sessionId); if (sessionId) { - handleLoadSession(sessionId); + await handleLoadSession(sessionId); sendResponse(true); } // close the requesting tab (should be spaces.html) @@ -235,9 +232,9 @@ function processMessage(request, sender, sendResponse) { return true; case 'loadWindow': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); if (windowId) { - handleLoadWindow(windowId); + await handleLoadWindow(windowId); sendResponse(true); } // close the requesting tab (should be spaces.html) @@ -246,9 +243,9 @@ function processMessage(request, sender, sendResponse) { return true; case 'loadTabInSession': - sessionId = _cleanParameter(request.sessionId); + sessionId = cleanParameter(request.sessionId); if (sessionId && request.tabUrl) { - handleLoadSession(sessionId, request.tabUrl); + await handleLoadSession(sessionId, request.tabUrl); sendResponse(true); } // close the requesting tab (should be spaces.html) @@ -257,9 +254,9 @@ function processMessage(request, sender, sendResponse) { return true; case 'loadTabInWindow': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); if (windowId && request.tabUrl) { - handleLoadWindow(windowId, request.tabUrl); + await handleLoadWindow(windowId, request.tabUrl); sendResponse(true); } // close the requesting tab (should be spaces.html) @@ -268,7 +265,7 @@ function processMessage(request, sender, sendResponse) { return true; case 'saveNewSession': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); if (windowId && request.sessionName) { handleSaveNewSession( windowId, @@ -292,14 +289,14 @@ function processMessage(request, sender, sendResponse) { return true; // allow async response case 'deleteSession': - sessionId = _cleanParameter(request.sessionId); + sessionId = cleanParameter(request.sessionId); if (sessionId) { handleDeleteSession(sessionId, sendResponse); } return true; case 'updateSessionName': - sessionId = _cleanParameter(request.sessionId); + sessionId = cleanParameter(request.sessionId); if (sessionId && request.sessionName) { handleUpdateSessionName( sessionId, @@ -311,52 +308,50 @@ function processMessage(request, sender, sendResponse) { return true; case 'requestSpaceDetail': - windowId = _cleanParameter(request.windowId); - sessionId = _cleanParameter(request.sessionId); + windowId = cleanParameter(request.windowId); + sessionId = cleanParameter(request.sessionId); if (windowId) { if (checkInternalSpacesWindows(windowId, false)) { sendResponse(false); } else { - requestSpaceFromWindowId(windowId).then(space => { - sendResponse(space); - }); + const space = await requestSpaceFromWindowId(windowId); + sendResponse(space); } } else if (sessionId) { - requestSpaceFromSessionId(sessionId, sendResponse); + await requestSpaceFromSessionId(sessionId, sendResponse); } return true; // end points called by tag.js and switcher.js // note: some of these endpoints will close the requesting tab case 'requestAllSpaces': - requestAllSpaces(allSpaces => { + await requestAllSpaces(allSpaces => { sendResponse(allSpaces); }); return true; case 'requestTabDetail': - tabId = _cleanParameter(request.tabId); + tabId = cleanParameter(request.tabId); if (tabId) { - requestTabDetail(tabId).then(tab => { - if (tab) { - sendResponse(tab); - } else { - // close the requesting tab (should be tab.html) - closePopupWindow(); - } - }); + const tab = await requestTabDetail(tabId); + if (tab) { + sendResponse(tab); + } else { + // close the requesting tab (should be tab.html) + await closePopupWindow(); + } } return true; case 'requestShowSpaces': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); // show the spaces tab in edit mode for the passed in windowId if (windowId) { - showSpacesOpenWindow(windowId, request.edit); + await showSpacesOpenWindow(windowId, request.edit); } else { - showSpacesOpenWindow(); + await showSpacesOpenWindow(); } return false; @@ -374,28 +369,25 @@ function processMessage(request, sender, sendResponse) { case 'requestClose': // close the requesting tab (should be tab.html) - closePopupWindow(); + await closePopupWindow(); return false; case 'switchToSpace': - windowId = _cleanParameter(request.windowId); - sessionId = _cleanParameter(request.sessionId); - - (async () => { - if (windowId) { - await handleLoadWindow(windowId); - } else if (sessionId) { - await handleLoadSession(sessionId); - } - sendResponse(true); - })(); + windowId = cleanParameter(request.windowId); + sessionId = cleanParameter(request.sessionId); + if (windowId) { + await handleLoadWindow(windowId); + } else if (sessionId) { + await handleLoadSession(sessionId); + } + sendResponse(true); return true; case 'addLinkToNewSession': - tabId = _cleanParameter(request.tabId); + tabId = cleanParameter(request.tabId); if (request.sessionName && request.url) { - handleAddLinkToNewSession( + await handleAddLinkToNewSession( request.url, request.sessionName, result => { @@ -410,9 +402,9 @@ function processMessage(request, sender, sendResponse) { return false; case 'moveTabToNewSession': - tabId = _cleanParameter(request.tabId); + tabId = cleanParameter(request.tabId); if (request.sessionName && tabId) { - handleMoveTabToNewSession( + await handleMoveTabToNewSession( tabId, request.sessionName, result => { @@ -427,10 +419,10 @@ function processMessage(request, sender, sendResponse) { return false; case 'addLinkToSession': - sessionId = _cleanParameter(request.sessionId); + sessionId = cleanParameter(request.sessionId); if (sessionId && request.url) { - handleAddLinkToSession(request.url, sessionId, result => { + await handleAddLinkToSession(request.url, sessionId, result => { if (result) updateSpacesWindow('addLinkToSession'); // close the requesting tab (should be tab.html) @@ -440,11 +432,11 @@ function processMessage(request, sender, sendResponse) { return false; case 'moveTabToSession': - sessionId = _cleanParameter(request.sessionId); - tabId = _cleanParameter(request.tabId); + sessionId = cleanParameter(request.sessionId); + tabId = cleanParameter(request.tabId); if (sessionId && tabId) { - handleMoveTabToSession(tabId, sessionId, result => { + await handleMoveTabToSession(tabId, sessionId, result => { if (result) updateSpacesWindow('moveTabToSession'); // close the requesting tab (should be tab.html) @@ -454,10 +446,10 @@ function processMessage(request, sender, sendResponse) { return false; case 'addLinkToWindow': - windowId = _cleanParameter(request.windowId); + windowId = cleanParameter(request.windowId); if (windowId && request.url) { - handleAddLinkToWindow(request.url, windowId, result => { + await handleAddLinkToWindow(request.url, windowId, result => { if (result) updateSpacesWindow('addLinkToWindow'); // close the requesting tab (should be tab.html) @@ -467,11 +459,11 @@ function processMessage(request, sender, sendResponse) { return false; case 'moveTabToWindow': - windowId = _cleanParameter(request.windowId); - tabId = _cleanParameter(request.tabId); + windowId = cleanParameter(request.windowId); + tabId = cleanParameter(request.tabId); if (windowId && tabId) { - handleMoveTabToWindow(tabId, windowId, result => { + await handleMoveTabToWindow(tabId, windowId, result => { if (result) updateSpacesWindow('moveTabToWindow'); // close the requesting tab (should be tab.html) @@ -485,7 +477,12 @@ function processMessage(request, sender, sendResponse) { } } -function _cleanParameter(param) { +/** + * Ensures the parameter is a number. + * @param {string|number} param - The parameter to clean. + * @returns {number} - The cleaned parameter. + */ +function cleanParameter(param) { if (typeof param === 'number') { return param; } From 6b0f4c135b57a9262c91ec4df028a6ae19c46345 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:19:48 -0700 Subject: [PATCH 50/92] Convert saveNewSession() to async instead of using a callback --- js/background/background.js | 24 +++++++++++++----------- js/background/spacesService.js | 23 +++++++++++++++++------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index c015451..6b981e7 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -948,12 +948,12 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) } handleDeleteSession(existingSession.id, noop); } - spacesService.saveNewSession( + const result = await spacesService.saveNewSession( sessionName, curWindow.tabs, - curWindow.id, - callback + curWindow.id ); + callback(result); } async function handleRestoreFromBackup(space, deleteOld, callback) { @@ -975,12 +975,12 @@ async function handleRestoreFromBackup(space, deleteOld, callback) { handleDeleteSession(existingSession.id, noop); } - spacesService.saveNewSession( + const result = await spacesService.saveNewSession( space.name, space.tabs, - false, - callback + false ); + callback(result); } async function handleImportNewSession(urlList, callback) { @@ -998,7 +998,8 @@ async function handleImportNewSession(urlList, callback) { }); // save session to database - spacesService.saveNewSession(tempName, tabList, false, callback); + const result = await spacesService.saveNewSession(tempName, tabList, false); + callback(result); } async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { @@ -1043,7 +1044,8 @@ async function handleAddLinkToNewSession(url, sessionName, callback) { // else create a new session with this name containing this url } else { - spacesService.saveNewSession(sessionName, newTabs, false, callback); + const result = await spacesService.saveNewSession(sessionName, newTabs, false); + callback(result); } } @@ -1067,12 +1069,12 @@ async function handleMoveTabToNewSession(tabId, sessionName, callback) { chrome.tabs.remove(tab.id); // save session to database - spacesService.saveNewSession( + const result = await spacesService.saveNewSession( sessionName, [tab], - false, - callback + false ); + callback(result); } } diff --git a/js/background/spacesService.js b/js/background/spacesService.js index af4bc14..4107694 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -675,12 +675,23 @@ class SpacesService { } } - async saveNewSession(sessionName, tabs, windowId, callback = noop) { + /** + * Creates a new session with the provided name, tabs, and window association. + * If a temporary session exists for the given windowId, it will be converted to a permanent session. + * Otherwise, a new session is created and added to the sessions cache. + * + * @param {string} sessionName - The name for the new session + * @param {Array} tabs - Array of tab objects containing URL and other tab properties + * @param {number|false} windowId - The window ID to associate with this session, or false for no association + * @returns {Promise} Promise that resolves to: + * - Session object with id property if successfully created + * - null if session creation failed or no tabs were provided + */ + async saveNewSession(sessionName, tabs, windowId) { await this.ensureInitialized(); if (!tabs) { - callback(); - return; + return null; } const sessionHash = generateSessionHash(tabs); @@ -712,14 +723,14 @@ class SpacesService { if (savedSession) { // update sessionId in cache session.id = savedSession.id; - callback(savedSession); + return savedSession; } else { console.error('Failed to create session'); - callback(false); + return null; } } catch (error) { console.error('Error creating session:', error); - callback(false); + return null; } } From fe83bd1e896765d8f2397517e045fd25b0f96772 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:28:38 -0700 Subject: [PATCH 51/92] Update spacesService saveExistingSession() to not take a callback --- js/background/spacesService.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 4107694..48f355f 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -4,6 +4,8 @@ * Copyright (C) 2015 Dean Oemcke */ +/** @typedef {import('./dbService.js').Session} Session */ + import { dbService } from './dbService.js'; // Module-level properties @@ -559,7 +561,7 @@ class SpacesService { // if it is a saved session then update db if (session.id) { - this.saveExistingSession(session, callback); + await this.saveExistingSession(session); } } @@ -655,23 +657,33 @@ class SpacesService { session.tabs = tabs; session.sessionHash = generateSessionHash(session.tabs); - this.saveExistingSession(session, callback); + const result = await this.saveExistingSession(session); + callback(result); } async updateSessionName(sessionId, sessionName, callback = noop) { const session = await dbService.fetchSessionById(sessionId); session.name = sessionName; - this.saveExistingSession(session, callback); + await this.saveExistingSession(session); + callback(); } - async saveExistingSession(session, callback = noop) { + /** + * Updates an existing session in the database. + * + * @param {Session} session - The session object to update + * @returns {Promise} Promise that resolves to: + * - Updated session object if successfully saved + * - null if session update failed + */ + async saveExistingSession(session) { try { const updatedSession = await dbService.updateSession(session); - callback(updatedSession); + return updatedSession || null; } catch (error) { console.error('Error saving existing session:', error); - callback(null); + return null; } } @@ -683,7 +695,7 @@ class SpacesService { * @param {string} sessionName - The name for the new session * @param {Array} tabs - Array of tab objects containing URL and other tab properties * @param {number|false} windowId - The window ID to associate with this session, or false for no association - * @returns {Promise} Promise that resolves to: + * @returns {Promise} Promise that resolves to: * - Session object with id property if successfully created * - null if session creation failed or no tabs were provided */ From 43c8938aa7189c01c08799da542019ab67fa0174 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:49:04 -0700 Subject: [PATCH 52/92] Change SpacesService updateSessionName() and updateSessionTabs() to async and remove callback param --- js/background/background.js | 12 +++++++----- js/background/spacesService.js | 28 ++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 6b981e7..de20b26 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -1019,7 +1019,8 @@ async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callba } handleDeleteSession(existingSession.id, noop); } - spacesService.updateSessionName(sessionId, sessionName, callback); + const result = await spacesService.updateSessionName(sessionId, sessionName); + callback(result); } async function handleDeleteSession(sessionId, callback) { @@ -1096,7 +1097,8 @@ async function handleAddLinkToSession(url, sessionId, callback) { } else { // update session in db session.tabs = session.tabs.concat(newTabs); - spacesService.updateSessionTabs(session.id, session.tabs, callback); + const result = await spacesService.updateSessionTabs(session.id, session.tabs); + callback(result); } } @@ -1137,11 +1139,11 @@ async function handleMoveTabToSession(tabId, sessionId, callback) { // update session in db session.tabs = session.tabs.concat(newTabs); - spacesService.updateSessionTabs( + const result = await spacesService.updateSessionTabs( session.id, - session.tabs, - callback + session.tabs ); + callback(result); } } diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 48f355f..33b5c5d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -650,23 +650,39 @@ class SpacesService { // Database actions - async updateSessionTabs(sessionId, tabs, callback = noop) { + /** + * Updates the tabs of an existing session in the database. + * + * @param {number} sessionId - The ID of the session to update + * @param {Array} tabs - Array of tab objects containing URL and other tab properties + * @returns {Promise} Promise that resolves to: + * - Updated session object if successfully saved + * - null if session update failed + */ + async updateSessionTabs(sessionId, tabs) { const session = await dbService.fetchSessionById(sessionId); // update tabs in session session.tabs = tabs; session.sessionHash = generateSessionHash(session.tabs); - const result = await this.saveExistingSession(session); - callback(result); + return this.saveExistingSession(session); } - async updateSessionName(sessionId, sessionName, callback = noop) { + /** + * Updates the name of an existing session in the database. + * + * @param {number} sessionId - The ID of the session to update + * @param {string} sessionName - The new name for the session + * @returns {Promise} Promise that resolves to: + * - Updated session object if successfully saved + * - null if session update failed + */ + async updateSessionName(sessionId, sessionName) { const session = await dbService.fetchSessionById(sessionId); session.name = sessionName; - await this.saveExistingSession(session); - callback(); + return this.saveExistingSession(session); } /** From a81ba324d8cf224c62f9ba1c46a3ed926a160970 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:53:19 -0700 Subject: [PATCH 53/92] Change SpacesService deleteSession() to async and remove callback param --- js/background/background.js | 3 ++- js/background/spacesService.js | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index de20b26..9af7fc2 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -1031,7 +1031,8 @@ async function handleDeleteSession(sessionId, callback) { return; } - spacesService.deleteSession(sessionId, callback); + const result = await spacesService.deleteSession(sessionId); + callback(result); } async function handleAddLinkToNewSession(url, sessionName, callback) { diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 33b5c5d..0286c38 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -762,7 +762,15 @@ class SpacesService { } } - async deleteSession(sessionId, callback = noop) { + /** + * Deletes a session from the database and removes it from the cache. + * + * @param {number} sessionId - The ID of the session to delete + * @returns {Promise} Promise that resolves to: + * - true if session was successfully deleted + * - false if session deletion failed + */ + async deleteSession(sessionId) { try { const success = await dbService.removeSession(sessionId); if (success) { @@ -775,10 +783,10 @@ class SpacesService { return false; }); } - callback(success); + return success; } catch (error) { console.error('Error deleting session:', error); - callback(false); + return false; } } } From e7cd278292c182524f24dc541524c04b0f333fe3 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 14:58:23 -0700 Subject: [PATCH 54/92] Update background's handleDeleteSession() to not use a callback --- js/background/background.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 9af7fc2..390a40f 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -291,7 +291,8 @@ async function processMessage(request, sender, sendResponse) { case 'deleteSession': sessionId = cleanParameter(request.sessionId); if (sessionId) { - handleDeleteSession(sessionId, sendResponse); + const result = await handleDeleteSession(sessionId); + sendResponse(result); } return true; @@ -946,7 +947,7 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) // if we choose to overwrite, delete the existing session } - handleDeleteSession(existingSession.id, noop); + await handleDeleteSession(existingSession.id); } const result = await spacesService.saveNewSession( sessionName, @@ -972,7 +973,7 @@ async function handleRestoreFromBackup(space, deleteOld, callback) { // if we choose to overwrite, delete the existing session } - handleDeleteSession(existingSession.id, noop); + await handleDeleteSession(existingSession.id); } const result = await spacesService.saveNewSession( @@ -1017,22 +1018,28 @@ async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callba // if we choose to override, then delete the existing session } - handleDeleteSession(existingSession.id, noop); + await handleDeleteSession(existingSession.id); } const result = await spacesService.updateSessionName(sessionId, sessionName); callback(result); } -async function handleDeleteSession(sessionId, callback) { +/** + * Deletes a session from the database and removes it from the cache. + * + * @param {number} sessionId + * @returns {Promise} Promise that resolves to: + * - true if session was successfully deleted + * - false if session deletion failed or session not found + */ +async function handleDeleteSession(sessionId) { const session = await dbService.fetchSessionById(sessionId); if (!session) { console.error(`handleDeleteSession: No session found with id ${sessionId}`); - callback(false); - return; + return false; } - const result = await spacesService.deleteSession(sessionId); - callback(result); + return spacesService.deleteSession(sessionId); } async function handleAddLinkToNewSession(url, sessionName, callback) { From 7ef2b8d0ca9eda5e44dfb2e64a1c79f840d7d94f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 15:02:10 -0700 Subject: [PATCH 55/92] Update handleAddLinkToNewSession() to not use a callback --- js/background/background.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 390a40f..a65aa6c 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -388,17 +388,16 @@ async function processMessage(request, sender, sendResponse) { case 'addLinkToNewSession': tabId = cleanParameter(request.tabId); if (request.sessionName && request.url) { - await handleAddLinkToNewSession( + const result = await handleAddLinkToNewSession( request.url, - request.sessionName, - result => { - if (result) - updateSpacesWindow('addLinkToNewSession'); - - // close the requesting tab (should be tab.html) - closePopupWindow(); - } + request.sessionName ); + if (result) { + updateSpacesWindow('addLinkToNewSession'); + } + + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -1042,19 +1041,25 @@ async function handleDeleteSession(sessionId) { return spacesService.deleteSession(sessionId); } -async function handleAddLinkToNewSession(url, sessionName, callback) { +/** + * @param {string} url - The URL to add to the new session + * @param {string} sessionName - The name for the new session + * @returns {Promise} Promise that resolves to: + * - Session object if the session was successfully created + * - null if a session with that name already exists or creation failed + */ +async function handleAddLinkToNewSession(url, sessionName) { const session = await dbService.fetchSessionByName(sessionName); const newTabs = [{ url }]; // if we found a session matching this name then return as an error as we are // supposed to be creating a new session with this name if (session) { - callback(false); + return null; // else create a new session with this name containing this url } else { - const result = await spacesService.saveNewSession(sessionName, newTabs, false); - callback(result); + return spacesService.saveNewSession(sessionName, newTabs, false); } } From 50494257524a0ac7864b91855334008c3716118d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 15:04:58 -0700 Subject: [PATCH 56/92] Update handleMoveTabToNewSession() to not use a callback --- js/background/background.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index a65aa6c..835f186 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -404,17 +404,16 @@ async function processMessage(request, sender, sendResponse) { case 'moveTabToNewSession': tabId = cleanParameter(request.tabId); if (request.sessionName && tabId) { - await handleMoveTabToNewSession( + const result = await handleMoveTabToNewSession( tabId, - request.sessionName, - result => { - if (result) - updateSpacesWindow('moveTabToNewSession'); - - // close the requesting tab (should be tab.html) - closePopupWindow(); - } + request.sessionName ); + if (result) { + updateSpacesWindow('moveTabToNewSession'); + } + + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -1063,11 +1062,17 @@ async function handleAddLinkToNewSession(url, sessionName) { } } -async function handleMoveTabToNewSession(tabId, sessionName, callback) { +/** + * @param {number} tabId - The ID of the tab to move to the new session + * @param {string} sessionName - The name for the new session + * @returns {Promise} Promise that resolves to: + * - Session object if the session was successfully created + * - null if a session with that name already exists or creation failed + */ +async function handleMoveTabToNewSession(tabId, sessionName) { const tab = await requestTabDetail(tabId); if (!tab) { - callback(false); - return; + return null; } const session = await dbService.fetchSessionByName(sessionName); @@ -1075,7 +1080,7 @@ async function handleMoveTabToNewSession(tabId, sessionName, callback) { // if we found a session matching this name then return as an error as we are // supposed to be creating a new session with this name if (session) { - callback(false); + return null; // else create a new session with this name containing this tab } else { @@ -1083,12 +1088,11 @@ async function handleMoveTabToNewSession(tabId, sessionName, callback) { chrome.tabs.remove(tab.id); // save session to database - const result = await spacesService.saveNewSession( + return spacesService.saveNewSession( sessionName, [tab], false ); - callback(result); } } From b135e8570a62c36d30816bc3d35c4b7ced348f7c Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 15:12:55 -0700 Subject: [PATCH 57/92] Update handleMoveTabToWindow() and moveTabToWindow() to not use callbacks --- js/background/background.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 835f186..7b2bf2b 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -462,12 +462,13 @@ async function processMessage(request, sender, sendResponse) { tabId = cleanParameter(request.tabId); if (windowId && tabId) { - await handleMoveTabToWindow(tabId, windowId, result => { - if (result) updateSpacesWindow('moveTabToWindow'); + const result = await handleMoveTabToWindow(tabId, windowId); + if (result) { + updateSpacesWindow('moveTabToWindow'); + } - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -1146,7 +1147,8 @@ async function handleMoveTabToSession(tabId, sessionId, callback) { } else { // if session is currently open then move it directly if (session.windowId) { - moveTabToWindow(tab, session.windowId, callback); + moveTabToWindow(tab, session.windowId); + callback(true); return; } @@ -1164,24 +1166,33 @@ async function handleMoveTabToSession(tabId, sessionId, callback) { } } -async function handleMoveTabToWindow(tabId, windowId, callback) { +/** + * @param {number} tabId + * @param {number} windowId + * @returns {Promise} Promise that resolves to: + * - true if the tab was successfully moved + * - false if the tab was not found or move failed + */ +async function handleMoveTabToWindow(tabId, windowId) { const tab = await requestTabDetail(tabId); if (!tab) { - callback(false); - return; + return false; } - moveTabToWindow(tab, windowId, callback); + moveTabToWindow(tab, windowId); + return true; } -function moveTabToWindow(tab, windowId, callback) { +/** + * @param {chrome.tabs.Tab} tab + * @param {number} windowId The ID of the destination window. + */ +function moveTabToWindow(tab, windowId) { chrome.tabs.move(tab.id, { windowId, index: -1 }); // NOTE: this move does not seem to trigger any tab event listeners // so we need to update sessions manually spacesService.queueWindowEvent(tab.windowId); spacesService.queueWindowEvent(windowId); - - callback(true); } console.log(`Initializing spacesService...`); From 32d59d068180dcab787862082818f53691e828ab Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 15:22:53 -0700 Subject: [PATCH 58/92] Update requestAllSpaces(), handleAddLinkToSession() and handleAddLinkToWindow() to not take callbacks --- js/background/background.js | 90 ++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 7b2bf2b..ba6cec2 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -327,9 +327,8 @@ async function processMessage(request, sender, sendResponse) { // end points called by tag.js and switcher.js // note: some of these endpoints will close the requesting tab case 'requestAllSpaces': - await requestAllSpaces(allSpaces => { - sendResponse(allSpaces); - }); + const allSpaces = await requestAllSpaces(); + sendResponse(allSpaces); return true; case 'requestTabDetail': @@ -392,9 +391,7 @@ async function processMessage(request, sender, sendResponse) { request.url, request.sessionName ); - if (result) { - updateSpacesWindow('addLinkToNewSession'); - } + if (result) updateSpacesWindow('addLinkToNewSession'); // close the requesting tab (should be tab.html) closePopupWindow(); @@ -408,9 +405,7 @@ async function processMessage(request, sender, sendResponse) { tabId, request.sessionName ); - if (result) { - updateSpacesWindow('moveTabToNewSession'); - } + if (result) updateSpacesWindow('moveTabToNewSession'); // close the requesting tab (should be tab.html) closePopupWindow(); @@ -421,12 +416,11 @@ async function processMessage(request, sender, sendResponse) { sessionId = cleanParameter(request.sessionId); if (sessionId && request.url) { - await handleAddLinkToSession(request.url, sessionId, result => { - if (result) updateSpacesWindow('addLinkToSession'); + const result = await handleAddLinkToSession(request.url, sessionId); + if (result) updateSpacesWindow('addLinkToSession'); - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -448,12 +442,11 @@ async function processMessage(request, sender, sendResponse) { windowId = cleanParameter(request.windowId); if (windowId && request.url) { - await handleAddLinkToWindow(request.url, windowId, result => { - if (result) updateSpacesWindow('addLinkToWindow'); + handleAddLinkToWindow(request.url, windowId); + updateSpacesWindow('addLinkToWindow'); - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -688,17 +681,16 @@ async function updateSpacesWindow(source) { return; } - requestAllSpaces(allSpaces => { - try { - chrome.runtime.sendMessage({ - action: 'updateSpaces', - spaces: allSpaces, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`updateSpacesWindow: Error updating spaces window: ${err}`); - } - }); + try { + const allSpaces = await requestAllSpaces(); + chrome.runtime.sendMessage({ + action: 'updateSpaces', + spaces: allSpaces, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`updateSpacesWindow: Error updating spaces window: ${err}`); + } } } @@ -800,7 +792,12 @@ async function requestSpaceFromSessionId(sessionId, callback) { }); } -async function requestAllSpaces(callback) { +/** + * Requests all spaces (sessions) from the database. + * + * @returns {Promise} Promise that resolves to an array of Space objects + */ +async function requestAllSpaces() { const sessions = await dbService.fetchAllSessions(); const allSpaces = sessions .map(session => { @@ -813,7 +810,7 @@ async function requestAllSpaces(callback) { // sort results allSpaces.sort(spaceDateCompare); - callback(allSpaces); + return allSpaces; } function spaceDateCompare(a, b) { @@ -1097,37 +1094,50 @@ async function handleMoveTabToNewSession(tabId, sessionName) { } } -async function handleAddLinkToSession(url, sessionId, callback) { +/** + * Adds a link to an existing session. + * + * @param {string} url - The URL to add to the session + * @param {number} sessionId - The ID of the session to add the link to + * @returns {Promise} Promise that resolves to: + * - true if the link was successfully added + * - false if the session was not found or addition failed + */ +async function handleAddLinkToSession(url, sessionId) { const session = await dbService.fetchSessionById(sessionId); const newTabs = [{ url }]; // if we have not found a session matching this name then return as an error as we are // supposed to be adding the tab to an existing session if (!session) { - callback(false); - return; + return false; } // if session is currently open then add link directly if (session.windowId) { - handleAddLinkToWindow(url, session.windowId, callback); + handleAddLinkToWindow(url, session.windowId); + return true; // else add tab to saved session in database } else { // update session in db session.tabs = session.tabs.concat(newTabs); const result = await spacesService.updateSessionTabs(session.id, session.tabs); - callback(result); + return !!result; } } -function handleAddLinkToWindow(url, windowId, callback) { +/** + * Adds a link to a window by creating a new tab. + * + * @param {string} url - The URL to create a tab for + * @param {number} windowId - The ID of the window to add the tab to + */ +function handleAddLinkToWindow(url, windowId) { chrome.tabs.create({ windowId, url, active: false }); // NOTE: this move does not seem to trigger any tab event listeners // so we need to update sessions manually spacesService.queueWindowEvent(windowId); - - callback(true); } async function handleMoveTabToSession(tabId, sessionId, callback) { From d95f7dc3ed232d263ab7ffeccb28332a75584a3c Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 15:58:46 -0700 Subject: [PATCH 59/92] Update handleWindowEvent() to not use a callback --- js/background/db.js | 2 +- js/background/spacesService.js | 33 +++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/js/background/db.js b/js/background/db.js index cfbb258..43b3faa 100644 --- a/js/background/db.js +++ b/js/background/db.js @@ -2,7 +2,7 @@ //Copyright (c) 2012 Aaron Powell /** * Changes in 2025 by codedread: - * - Removed unused CallbackList class. + * - Removed unused code. * - Modernized code style. * - Made into an ES module. */ diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 0286c38..98b49c7 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -447,18 +447,31 @@ class SpacesService { // Set a timeout so that multiple tabs all opened at once (like when restoring a session) // only trigger this function once (as per the timeout set by the last tab event) // This will cause multiple triggers if time between tab openings is longer than 1 sec - queueWindowEvent(windowId, eventId, callback) { + queueWindowEvent(windowId, eventId, callback = noop) { clearTimeout(this.sessionUpdateTimers[windowId]); this.eventQueueCount += 1; - this.sessionUpdateTimers[windowId] = setTimeout(() => { - this.handleWindowEvent(windowId, eventId, callback); + this.sessionUpdateTimers[windowId] = setTimeout(async () => { + const shouldCallback = await this.handleWindowEvent(windowId, eventId); + if (shouldCallback) callback(); }, 1000); } - // careful here as this function gets called A LOT - async handleWindowEvent(windowId, eventId, callback = noop) { + /** + * Handles window events by updating session data when tabs change within a window. + * This function processes batched tab events and updates the corresponding session + * in the database. + * + * NOTE: Careful here as this function gets called A LOT + * + * @param {number} windowId - The ID of the window that triggered the event + * @param {number} eventId - The unique event identifier for tracking/debugging purposes + * @returns {Promise} Promise that resolves to: + * - true if the window event was successfully processed + * - false if the event was ignored (invalid window, internal window, closed window, etc.) + */ + async handleWindowEvent(windowId, eventId) { if (debug) { // eslint-disable-next-line no-console console.log('------------------------------------------------'); @@ -477,7 +490,7 @@ class SpacesService { `received an event for windowId: ${windowId} which is obviously wrong` ); } - return; + return false; } let curWindow; @@ -496,11 +509,11 @@ class SpacesService { windowId, false ); - return; + return false; } if (!curWindow || this.filterInternalWindows(curWindow)) { - return; + return false; } // don't allow event if it pertains to a closed window id @@ -511,7 +524,7 @@ class SpacesService { `ignoring event as it pertains to a closed windowId: ${windowId}` ); } - return; + return false; } // if window is associated with an open session then update session @@ -575,7 +588,7 @@ class SpacesService { } this.checkForSessionMatch(curWindow); } - callback(); + return true; } // PUBLIC FUNCTIONS From 8685ada4184f0062cf08c3bd7b605088ca95fe4c Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 16:10:39 -0700 Subject: [PATCH 60/92] Update SpacesService.handleWindowRemoved() by removing the callback --- js/background/background.js | 5 +++-- js/background/spacesService.js | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index ba6cec2..dbe48db 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -123,9 +123,10 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { chrome.windows.onRemoved.addListener(async (windowId) => { if (checkInternalSpacesWindows(windowId, true)) return; - spacesService.handleWindowRemoved(windowId, true, () => { + const wasProcessed = await spacesService.handleWindowRemoved(windowId, true); + if (wasProcessed) { updateSpacesWindow('windows.onRemoved'); - }); + } // if this was the last window open and the spaces window is stil open // then close the spaces window also so that chrome exits fully diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 98b49c7..4a40c64 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -377,12 +377,22 @@ class SpacesService { } } - async handleWindowRemoved(windowId, markAsClosed, callback = noop) { + /** + * Handles window removal events by cleaning up session data and managing window state. + * Updates session associations and removes temporary sessions when windows are closed. + * + * @param {number} windowId - The ID of the window that was removed + * @param {boolean} markAsClosed - Whether to mark this window as permanently closed + * @returns {Promise} Promise that resolves to: + * - true if the window removal was successfully processed + * - false if the removal was ignored (duplicate event for same windowId) + */ + async handleWindowRemoved(windowId, markAsClosed) { await this.ensureInitialized(); // ignore subsequent windowRemoved events for the same windowId (each closing tab will try to call this) if (this.closedWindowIds[windowId]) { - callback(); + return true; } if (debug) { @@ -422,7 +432,7 @@ class SpacesService { } } - callback(); + return true; } async handleWindowFocussed(windowId) { From 08605e74f6d003a37f79e7e817a47f748249d958 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 20:52:51 -0700 Subject: [PATCH 61/92] Update requestSpaceFromSessionId() to remove the callback param. Also properly export the Space @typedef from common.js. --- js/background/background.js | 28 ++++++++++++++++++++-------- js/background/dbService.js | 2 -- js/common.js | 11 +++++++---- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index dbe48db..9f2aba5 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -8,9 +8,8 @@ import { dbService } from './dbService.js'; import { spacesService } from './spacesService.js'; - -/** @typedef {import('./common.js').SessionPresence} SessionPresence */ -/** @typedef {import('./common.js').Space} Space */ +import * as common from '../common.js'; +/** @typedef {common.Space} Space */ // eslint-disable-next-line no-unused-vars, no-var let spacesPopupWindowId = false; @@ -321,7 +320,8 @@ async function processMessage(request, sender, sendResponse) { sendResponse(space); } } else if (sessionId) { - await requestSpaceFromSessionId(sessionId, sendResponse); + const space = await requestSpaceFromSessionId(sessionId); + sendResponse(space); } return true; @@ -781,16 +781,28 @@ async function requestSpaceFromWindowId(windowId) { } } -async function requestSpaceFromSessionId(sessionId, callback) { +/** + * Requests space details for a specific session ID. + * + * @param {number} sessionId + * @returns {Promise} Promise that resolves to: + * - Space object if session exists + * - null if session not found + */ +async function requestSpaceFromSessionId(sessionId) { const session = await dbService.fetchSessionById(sessionId); - - callback({ + + if (!session) { + return null; + } + + return { sessionId: session.id, windowId: session.windowId, name: session.name, tabs: session.tabs, history: session.history, - }); + }; } /** diff --git a/js/background/dbService.js b/js/background/dbService.js index 7488560..3d899e7 100644 --- a/js/background/dbService.js +++ b/js/background/dbService.js @@ -2,8 +2,6 @@ import { db, Server } from './db.js'; -/** @typedef {import('./common.js').Space} Space */ - /** * @typedef Session * @property {number} id Auto-generated indexedDb object id diff --git a/js/common.js b/js/common.js index 9ab5f7a..3b7af57 100644 --- a/js/common.js +++ b/js/common.js @@ -6,12 +6,13 @@ /** Common types shared between background and client code. */ -// TODO(codedread): Fill out the rest of the properties. /** * @typedef Space - * @property {string} id The unique identifier for the space. - * @property {string} name The name of the space. - * @property {string?} windowId The ID of the window associated with the space, if any. + * @property {number|false} sessionId The unique identifier for the session, or false if not saved. + * @property {number|false} windowId The ID of the window associated with the space, or false if not open. + * @property {string|false} name The name of the space, or false if not named. + * @property {Array} tabs Array of tab objects containing URL and other tab properties. + * @property {Array|false} history Array of tab history objects, or false if no history. */ /** @@ -19,3 +20,5 @@ * @property {boolean} exists A session with this name exists in the database. * @property {boolean} isOpen The session is currently open in a window. */ + +export {} From 1856204b7b9ec7dce777edb600485b8e5c592b9d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 20:57:09 -0700 Subject: [PATCH 62/92] Remove callback from handleImportNewSession() --- js/background/background.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 9f2aba5..5f510c1 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -278,9 +278,10 @@ async function processMessage(request, sender, sendResponse) { case 'importNewSession': if (request.urlList) { - handleImportNewSession(request.urlList, sendResponse); + const result = await handleImportNewSession(request.urlList); + sendResponse(result); } - return true; // allow async response + return true; case 'restoreFromBackup': if (request.space) { @@ -993,7 +994,15 @@ async function handleRestoreFromBackup(space, deleteOld, callback) { callback(result); } -async function handleImportNewSession(urlList, callback) { +/** + * Imports a list of URLs as a new session with an auto-generated name. + * + * @param {string[]} urlList - Array of URLs to import as tabs + * @returns {Promise} Promise that resolves to: + * - Session object if successfully created + * - null if session creation failed + */ +async function handleImportNewSession(urlList) { let tempName = 'Imported space: '; let count = 1; @@ -1008,8 +1017,7 @@ async function handleImportNewSession(urlList, callback) { }); // save session to database - const result = await spacesService.saveNewSession(tempName, tabList, false); - callback(result); + return spacesService.saveNewSession(tempName, tabList, false); } async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { From 766a06f73485d1606162714d164732ed1dd43c54 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 20:58:10 -0700 Subject: [PATCH 63/92] Remove an unnecessary "return await" --- js/background/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/background/background.js b/js/background/background.js index 5f510c1..319f1a5 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -741,7 +741,7 @@ async function requestTabDetail(tabId) { */ async function requestCurrentSpace() { const window = await chrome.windows.getCurrent(); - return await requestSpaceFromWindowId(window.id); + return requestSpaceFromWindowId(window.id); } /** From 8adaf0ee8d240540bd7919feccb1dc5116e81352 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 21:00:57 -0700 Subject: [PATCH 64/92] Remove callback from handleMoveTabToSession() --- js/background/background.js | 56 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 319f1a5..c6b2b5f 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -431,12 +431,11 @@ async function processMessage(request, sender, sendResponse) { tabId = cleanParameter(request.tabId); if (sessionId && tabId) { - await handleMoveTabToSession(tabId, sessionId, result => { - if (result) updateSpacesWindow('moveTabToSession'); + const result = await handleMoveTabToSession(tabId, sessionId); + if (result) updateSpacesWindow('moveTabToSession'); - // close the requesting tab (should be tab.html) - closePopupWindow(); - }); + // close the requesting tab (should be tab.html) + closePopupWindow(); } return false; @@ -1161,11 +1160,19 @@ function handleAddLinkToWindow(url, windowId) { spacesService.queueWindowEvent(windowId); } -async function handleMoveTabToSession(tabId, sessionId, callback) { +/** + * Moves a tab to an existing session. + * + * @param {number} tabId - The ID of the tab to move + * @param {number} sessionId - The ID of the session to move the tab to + * @returns {Promise} Promise that resolves to: + * - true if the tab was successfully moved + * - false if the tab or session was not found or move failed + */ +async function handleMoveTabToSession(tabId, sessionId) { const tab = await requestTabDetail(tabId); if (!tab) { - callback(false); - return; + return false; } const session = await dbService.fetchSessionById(sessionId); @@ -1174,27 +1181,22 @@ async function handleMoveTabToSession(tabId, sessionId, callback) { // if we have not found a session matching this name then return as an error as we are // supposed to be adding the tab to an existing session if (!session) { - callback(false); - } else { - // if session is currently open then move it directly - if (session.windowId) { - moveTabToWindow(tab, session.windowId); - callback(true); - return; - } + return false; + } + + // if session is currently open then move it directly + if (session.windowId) { + moveTabToWindow(tab, session.windowId); + return true; + } - // else add tab to saved session in database - // remove tab from current window - chrome.tabs.remove(tab.id); + // else add tab to saved session in database + // remove tab from current window + chrome.tabs.remove(tab.id); - // update session in db - session.tabs = session.tabs.concat(newTabs); - const result = await spacesService.updateSessionTabs( - session.id, - session.tabs - ); - callback(result); - } + // update session in db + session.tabs = session.tabs.concat(newTabs); + return !!spacesService.updateSessionTabs(session.id, session.tabs); } /** From 90967a416b47b79116a05b9705f871191ead8079 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 21:04:11 -0700 Subject: [PATCH 65/92] Remove callback from handleRestoreFromBackup() --- js/background/background.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index c6b2b5f..f483746 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -285,9 +285,10 @@ async function processMessage(request, sender, sendResponse) { case 'restoreFromBackup': if (request.space) { - handleRestoreFromBackup(request.space, !!request.deleteOld, sendResponse); + const result = await handleRestoreFromBackup(request.space, !!request.deleteOld); + sendResponse(result); } - return true; // allow async response + return true; case 'deleteSession': sessionId = cleanParameter(request.sessionId); @@ -966,7 +967,16 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) callback(result); } -async function handleRestoreFromBackup(space, deleteOld, callback) { +/** + * Restores a session from backup data. + * + * @param {Space} space - The space/session data to restore + * @param {boolean} deleteOld - Whether to delete existing session with same name + * @returns {Promise} Promise that resolves to: + * - Session object if successfully restored + * - null if session restoration failed or name conflict without deleteOld + */ +async function handleRestoreFromBackup(space, deleteOld) { const existingSession = space.name ? await dbService.fetchSessionByName(space.name) : false; @@ -977,20 +987,14 @@ async function handleRestoreFromBackup(space, deleteOld, callback) { console.error( `handleRestoreFromBackup: Session with name "${space.name}" already exists and deleteOld was not true.` ); - callback(false); - return; - - // if we choose to overwrite, delete the existing session + return null; } + + // if we choose to overwrite, delete the existing session await handleDeleteSession(existingSession.id); } - const result = await spacesService.saveNewSession( - space.name, - space.tabs, - false - ); - callback(result); + return spacesService.saveNewSession(space.name, space.tabs, false); } /** From 3bedc588cd09798608b54a745f91e1faf5595aed Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 21:09:58 -0700 Subject: [PATCH 66/92] Remove callback from handleUpdateSessionName() --- js/background/background.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index f483746..e5ec235 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -301,12 +301,12 @@ async function processMessage(request, sender, sendResponse) { case 'updateSessionName': sessionId = cleanParameter(request.sessionId); if (sessionId && request.sessionName) { - handleUpdateSessionName( + const result = await handleUpdateSessionName( sessionId, request.sessionName, - !!request.deleteOld, - sendResponse + !!request.deleteOld ); + sendResponse(result); } return true; @@ -1023,7 +1023,17 @@ async function handleImportNewSession(urlList) { return spacesService.saveNewSession(tempName, tabList, false); } -async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callback) { +/** + * Updates the name of an existing session. + * + * @param {number} sessionId - The ID of the session to rename + * @param {string} sessionName - The new name for the session + * @param {boolean} deleteOld - Whether to delete existing session with same name + * @returns {Promise} Promise that resolves to: + * - Session object if successfully updated + * - false if session update failed or name conflict without deleteOld + */ +async function handleUpdateSessionName(sessionId, sessionName, deleteOld) { // check to make sure session name doesn't already exist const existingSession = await dbService.fetchSessionByName(sessionName); @@ -1033,15 +1043,14 @@ async function handleUpdateSessionName(sessionId, sessionName, deleteOld, callba console.error( `handleUpdateSessionName: Session with name "${sessionName}" already exists and deleteOld was not true.` ); - callback(false); - return; - - // if we choose to override, then delete the existing session + return false; } + + // if we choose to override, then delete the existing session await handleDeleteSession(existingSession.id); } - const result = await spacesService.updateSessionName(sessionId, sessionName); - callback(result); + + return spacesService.updateSessionName(sessionId, sessionName) ?? false; } /** From 60d2665c001bc7185393de8729eb9806d3c172ce Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 21:11:17 -0700 Subject: [PATCH 67/92] Remove callback from handleSaveNewSession(). Done callback refactor in background.js! --- js/background/background.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index e5ec235..10d46bc 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -267,14 +267,14 @@ async function processMessage(request, sender, sendResponse) { case 'saveNewSession': windowId = cleanParameter(request.windowId); if (windowId && request.sessionName) { - handleSaveNewSession( + const result = await handleSaveNewSession( windowId, request.sessionName, - !!request.deleteOld, - sendResponse + !!request.deleteOld ); + sendResponse(result); } - return true; // allow async response + return true; case 'importNewSession': if (request.urlList) { @@ -942,7 +942,17 @@ async function focusOrLoadTabInWindow(window, tabUrl) { } } -async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) { +/** + * Saves a new session from the specified window. + * + * @param {number} windowId - The ID of the window to save as a session + * @param {string} sessionName - The name for the new session + * @param {boolean} deleteOld - Whether to delete existing session with same name + * @returns {Promise} Promise that resolves to: + * - Session object if successfully saved + * - false if session save failed or name conflict without deleteOld + */ +async function handleSaveNewSession(windowId, sessionName, deleteOld) { const curWindow = await chrome.windows.get(windowId, { populate: true }); const existingSession = await dbService.fetchSessionByName(sessionName); @@ -952,8 +962,7 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) console.error( `handleSaveNewSession: Session with name "${sessionName}" already exists and deleteOld was not true.` ); - callback(false); - return; + return false; // if we choose to overwrite, delete the existing session } @@ -964,7 +973,7 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld, callback) curWindow.tabs, curWindow.id ); - callback(result); + return result ?? false; } /** From edccd10d1e2f6cb4bc65570dd3469fe66fe1246f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 30 Aug 2025 21:32:28 -0700 Subject: [PATCH 68/92] Eliminate the last callback in background.js: the "sendResponse" parameter in processMessage() --- js/background/background.js | 148 ++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index 10d46bc..bc8911a 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -176,21 +176,42 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log(`listener fired: ${JSON.stringify(request)}`); } - // Ensure spacesService is initialized before processing any message - spacesService.ensureInitialized().then(async () => { - const result = await processMessage(request, sender, sendResponse); - // If processMessage returns false, we need to handle that by not sending a response - // But since we're in an async context, we can't change the outer return value - // The key is that processMessage will call sendResponse() for true cases - // and won't call it for false cases, which is the correct behavior - }); + // Handle async processing + (async () => { + try { + // Ensure spacesService is initialized before processing any message + await spacesService.ensureInitialized(); + + const response = await processMessage(request, sender); + if (response !== undefined) { + sendResponse(response); + } + } catch (error) { + console.error('Error processing message:', error); + sendResponse(false); + } + })(); - // We have to return true here because we're handling everything asynchronously - // The actual response sending is controlled by whether processMessage calls sendResponse() + // We must return true synchronously to keep the message port open + // for our async sendResponse() calls return true; }); -async function processMessage(request, sender, sendResponse) { +/** + * Processes incoming messages from extension pages and returns appropriate responses. + * + * This function handles all message types sent from popup.html, spaces.html, and other + * extension pages. It performs the requested action and returns data that will be + * sent back to the requesting page via sendResponse(). + * + * @param {Object} request The message request object containing action and parameters. + * It must have an action string property. + * @param {chrome.runtime.MessageSender} sender + * @returns {Promise} Promise that resolves to: + * - Response data (any type) that will be sent to the caller + * - undefined when no response should be sent to the caller + */ +async function processMessage(request, sender) { let sessionId; let windowId; let tabId; @@ -198,117 +219,101 @@ async function processMessage(request, sender, sendResponse) { // endpoints called by spaces.js switch (request.action) { case 'requestSessionPresence': - const sessionPresence = await requestSessionPresence(request.sessionName); - sendResponse(sessionPresence); - return true; + return requestSessionPresence(request.sessionName); case 'requestSpaceFromWindowId': windowId = cleanParameter(request.windowId); if (windowId) { - const space = await requestSpaceFromWindowId(windowId); - sendResponse(space); + return requestSpaceFromWindowId(windowId); } - return true; + return undefined; case 'requestCurrentSpace': - const currentSpace = await requestCurrentSpace(); - sendResponse(currentSpace); - return true; + return requestCurrentSpace(); case 'generatePopupParams': - const params = await generatePopupParams(request.action, request.tabUrl); - sendResponse(params); - return true; + return generatePopupParams(request.action, request.tabUrl); case 'loadSession': sessionId = cleanParameter(request.sessionId); if (sessionId) { await handleLoadSession(sessionId); - sendResponse(true); + return true; } // close the requesting tab (should be spaces.html) // if (!debug) closeChromeTab(sender.tab.id); - - return true; + return undefined; case 'loadWindow': windowId = cleanParameter(request.windowId); if (windowId) { await handleLoadWindow(windowId); - sendResponse(true); + return true; } // close the requesting tab (should be spaces.html) // if (!debug) closeChromeTab(sender.tab.id); - - return true; + return undefined; case 'loadTabInSession': sessionId = cleanParameter(request.sessionId); if (sessionId && request.tabUrl) { await handleLoadSession(sessionId, request.tabUrl); - sendResponse(true); + return true; } // close the requesting tab (should be spaces.html) // if (!debug) closeChromeTab(sender.tab.id); - - return true; + return undefined; case 'loadTabInWindow': windowId = cleanParameter(request.windowId); if (windowId && request.tabUrl) { await handleLoadWindow(windowId, request.tabUrl); - sendResponse(true); + return true; } // close the requesting tab (should be spaces.html) // if (!debug) closeChromeTab(sender.tab.id); - - return true; + return undefined; case 'saveNewSession': windowId = cleanParameter(request.windowId); if (windowId && request.sessionName) { - const result = await handleSaveNewSession( + return handleSaveNewSession( windowId, request.sessionName, !!request.deleteOld ); - sendResponse(result); } - return true; + return undefined; case 'importNewSession': if (request.urlList) { - const result = await handleImportNewSession(request.urlList); - sendResponse(result); + return handleImportNewSession(request.urlList); } - return true; + return undefined; case 'restoreFromBackup': if (request.space) { - const result = await handleRestoreFromBackup(request.space, !!request.deleteOld); - sendResponse(result); + return handleRestoreFromBackup(request.space, !!request.deleteOld); } - return true; + return undefined; case 'deleteSession': sessionId = cleanParameter(request.sessionId); if (sessionId) { - const result = await handleDeleteSession(sessionId); - sendResponse(result); + return handleDeleteSession(sessionId); } - return true; + return undefined; case 'updateSessionName': sessionId = cleanParameter(request.sessionId); if (sessionId && request.sessionName) { - const result = await handleUpdateSessionName( + return handleUpdateSessionName( sessionId, request.sessionName, !!request.deleteOld ); - sendResponse(result); } - return true; + return undefined; case 'requestSpaceDetail': windowId = cleanParameter(request.windowId); @@ -316,36 +321,32 @@ async function processMessage(request, sender, sendResponse) { if (windowId) { if (checkInternalSpacesWindows(windowId, false)) { - sendResponse(false); + return false; } else { - const space = await requestSpaceFromWindowId(windowId); - sendResponse(space); + return requestSpaceFromWindowId(windowId); } } else if (sessionId) { - const space = await requestSpaceFromSessionId(sessionId); - sendResponse(space); + return requestSpaceFromSessionId(sessionId); } - return true; + return undefined; // end points called by tag.js and switcher.js // note: some of these endpoints will close the requesting tab case 'requestAllSpaces': - const allSpaces = await requestAllSpaces(); - sendResponse(allSpaces); - return true; + return requestAllSpaces(); case 'requestTabDetail': tabId = cleanParameter(request.tabId); if (tabId) { const tab = await requestTabDetail(tabId); if (tab) { - sendResponse(tab); + return tab; } else { // close the requesting tab (should be tab.html) await closePopupWindow(); } } - return true; + return undefined; case 'requestShowSpaces': windowId = cleanParameter(request.windowId); @@ -356,24 +357,24 @@ async function processMessage(request, sender, sendResponse) { } else { await showSpacesOpenWindow(); } - return false; + return undefined; case 'requestShowSwitcher': showSpacesSwitchWindow(); - return false; + return undefined; case 'requestShowMover': showSpacesMoveWindow(); - return false; + return undefined; case 'requestShowKeyboardShortcuts': createShortcutsWindow(); - return false; + return undefined; case 'requestClose': // close the requesting tab (should be tab.html) await closePopupWindow(); - return false; + return undefined; case 'switchToSpace': windowId = cleanParameter(request.windowId); @@ -384,7 +385,6 @@ async function processMessage(request, sender, sendResponse) { } else if (sessionId) { await handleLoadSession(sessionId); } - sendResponse(true); return true; case 'addLinkToNewSession': @@ -399,7 +399,7 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; case 'moveTabToNewSession': tabId = cleanParameter(request.tabId); @@ -413,7 +413,7 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; case 'addLinkToSession': sessionId = cleanParameter(request.sessionId); @@ -425,7 +425,7 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; case 'moveTabToSession': sessionId = cleanParameter(request.sessionId); @@ -438,7 +438,7 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; case 'addLinkToWindow': windowId = cleanParameter(request.windowId); @@ -450,7 +450,7 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; case 'moveTabToWindow': windowId = cleanParameter(request.windowId); @@ -465,10 +465,10 @@ async function processMessage(request, sender, sendResponse) { // close the requesting tab (should be tab.html) closePopupWindow(); } - return false; + return undefined; default: - return false; + return undefined; } } From 204ca7f3614c608fb3b306ecf36581f3a24f8263 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 31 Aug 2025 08:15:07 -0700 Subject: [PATCH 69/92] Add first unit test for cleanUrl() and removed yarn.lock. --- .gitignore | 2 + js/background/spacesService.js | 3 + package-lock.json | 7284 ++++++++++++++++++++++++++++++++ package.json | 11 + test/README.md | 26 + test/cleanUrl.test.js | 132 + yarn.lock | 1291 ------ 7 files changed, 7458 insertions(+), 1291 deletions(-) create mode 100644 package-lock.json create mode 100644 test/README.md create mode 100644 test/cleanUrl.test.js delete mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index bc5a7f6..6e743e6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /.idea/* /.debris/* .DS_Store +coverage/ node_modules +yarn.lock diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 4a40c64..a84b696 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -907,3 +907,6 @@ function generateSessionHash(tabs) { // Export an instance of the SpacesService class export const spacesService = new SpacesService(); + +// Export the cleanUrl function for testing +export { cleanUrl }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3dfd00f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7284 @@ +{ + "name": "spaces", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "eslint": "^6.7.1", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.5.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^10.0.0", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "jest": "^29.7.0", + "prettier": "^1.19.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ansi-escapes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.5.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= sha512-mRVEsI0s5MycUKtZtn8i5co54WKxL5gH3gAcCjtUbECNwdDL2gsBwjLqswM3c6fjcuWFQ9hoS4C+EhjxQmEyHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= sha512-EJLbKSuvHTrVRynOXCYFTbQKZOFXWNe3/6DN1yrEH3TuuZT1x4dMQnCHnfCrBUUiGjO63enEIfaB17VaRl2d4A==", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= sha512-GtxAN4HvBachZzm4OnWqc45ESpUCMwkYcsjnsPs23FwJbsO+k4t0k9bQCgOmzIlpHO28+WPK/KRbRk0DDHuuDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.1.tgz", + "integrity": "sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", + "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confusing-browser-globals": "^1.0.7", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.1.0", + "eslint-plugin-import": "^2.18.2" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", + "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-config-standard": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", + "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=6.2.2", + "eslint-plugin-import": ">=2.18.0", + "eslint-plugin-node": ">=9.1.0", + "eslint-plugin-promise": ">=4.2.1", + "eslint-plugin-standard": ">=4.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-module-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", + "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz", + "integrity": "sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^1.4.2", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", + "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.0.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.11.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "2.x - 6.x" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-node": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz", + "integrity": "sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-es": "^2.0.0", + "eslint-utils": "^1.4.2", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", + "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">= 5.0.0", + "prettier": ">= 1.13.0" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-plugin-standard": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", + "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^4.0.0" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^4.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0= sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= sha512-QfcgWpH8qn5qhNMg3wfXf2FD/rSA4TwNiDDthKqXe7v6oBW0YKWcnfwMAApgWq9Lh+Yu+fQWVhHPohlD/S6uoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o= sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= sha512-P5rExV1phPi42ppoMWy7V63N3i173RY921l4JJ7zonMSxK+OWGPj76GD+cUKUb68l4vQXcJp2SsG+r/A4ABVzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= sha512-NECAi6wp6CgMesHuVUEK8JwjCvm/tvnn5pCbB42JOHp3mgUizN0nagXu4HEqQZBkieGEQ+jVcMKWqoVd6CDbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= sha512-WQgPrEkb1mPCWLSlLFuN1VziADSixANugwSkJfPRR73FNWIQQN+tR/t1zWfyES/Y9oag/XBtVsahFdfBku3Kyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.entries": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", + "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18= sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw= sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c= sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c= sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA= sha512-Fx+QT3fGtS0jk8OvKyKgAB2YHPsrmqBRcMeTC5AZ+lp4vzXKPPrFSY3iLdgvjA3HVBkIvJeM6J80LRjx8bQwhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.1.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index cae7071..8d1253b 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,14 @@ { + "type": "module", + "scripts": { + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", + "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" + }, + "jest": { + "transform": {}, + "testEnvironment": "node" + }, "devDependencies": { "eslint": "^6.7.1", "eslint-config-airbnb-base": "^14.0.0", @@ -9,6 +19,7 @@ "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", + "jest": "^29.7.0", "prettier": "^1.19.1" } } diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..1f97ad6 --- /dev/null +++ b/test/README.md @@ -0,0 +1,26 @@ +# Unit Tests for Spaces Chrome Extension + +This directory contains unit tests for the Spaces Chrome extension using **Jest**. This is the first step toward better code quality and maintainability. + +## Getting Started + +### Prerequisites + +Make sure you have Node.js installed and run: + +```bash +npm install +``` + +### Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode (re-runs when files change) +npm run test:watch + +# Run tests with coverage report +npm run test:coverage +``` diff --git a/test/cleanUrl.test.js b/test/cleanUrl.test.js new file mode 100644 index 0000000..e898549 --- /dev/null +++ b/test/cleanUrl.test.js @@ -0,0 +1,132 @@ +/** + * @jest-environment node + */ + +import { cleanUrl } from '../js/background/spacesService.js'; + +// Mock chrome.runtime.id for testing +global.chrome = { + runtime: { + id: 'test-extension-id-12345' + } +}; + +describe('cleanUrl', () => { + describe('basic functionality', () => { + test('should return empty string for null input', () => { + expect(cleanUrl(null)).toBe(''); + }); + + test('should return empty string for undefined input', () => { + expect(cleanUrl(undefined)).toBe(''); + }); + + test('should return empty string for empty string', () => { + expect(cleanUrl('')).toBe(''); + }); + + test('should return clean URL unchanged', () => { + const url = 'https://example.com/page'; + expect(cleanUrl(url)).toBe(url); + }); + }); + + describe('query parameter removal', () => { + test('should remove single query parameter', () => { + const url = 'https://example.com/page?param=value'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + + test('should remove multiple query parameters', () => { + const url = 'https://example.com/page?param=value&other=test&third=123'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + }); + + describe('hash fragment removal', () => { + test('should remove hash fragments', () => { + const url = 'https://example.com/page#section'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + + test('should remove complex hash fragments', () => { + const url = 'https://example.com/page#section-with-dashes_and_underscores'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + }); + + describe('combined query and hash removal', () => { + test('should remove both query parameters and hash fragments', () => { + const url = 'https://example.com/page?param=value#section'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + + test('should handle complex combinations', () => { + const url = 'https://example.com/page?a=1&b=2&c=3#complex-hash'; + expect(cleanUrl(url)).toBe('https://example.com/page'); + }); + }); + + describe('extension URL filtering', () => { + test('should return empty string for extension URLs', () => { + const url = `chrome-extension://${chrome.runtime.id}/popup.html`; + expect(cleanUrl(url)).toBe(''); + }); + + test('should return empty string for extension URLs with query params', () => { + const url = `chrome-extension://${chrome.runtime.id}/popup.html?param=value`; + expect(cleanUrl(url)).toBe(''); + }); + }); + + describe('new tab page filtering', () => { + test('should return empty string for new tab pages', () => { + const url = 'chrome:// newtab/'; + expect(cleanUrl(url)).toBe(''); + }); + }); + + describe('The Great Suspender support', () => { + test('should extract URI from Great Suspender URLs', () => { + const suspendedUrl = 'chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=test&pos=0&uri=https://example.com/page'; + expect(cleanUrl(suspendedUrl)).toBe('https://example.com/page'); + }); + + test('should extract URI with query params from Great Suspender URLs', () => { + const suspendedUrl = 'chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=test&pos=0&uri=https://example.com/page?param=value'; + expect(cleanUrl(suspendedUrl)).toBe('https://example.com/page'); + }); + + test('should handle malformed Great Suspender URLs gracefully', () => { + const malformedUrl = 'chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=test&pos=0'; + expect(cleanUrl(malformedUrl)).toBe('chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html'); + }); + }); + + describe('edge cases', () => { + test('should handle URLs with only hash', () => { + const url = 'https://example.com/#anchor'; + expect(cleanUrl(url)).toBe('https://example.com/'); + }); + + test('should handle URLs with only query', () => { + const url = 'https://example.com/?query=test'; + expect(cleanUrl(url)).toBe('https://example.com/'); + }); + + test('should handle URLs with trailing slash', () => { + const url = 'https://example.com/'; + expect(cleanUrl(url)).toBe('https://example.com/'); + }); + + test('should handle localhost URLs', () => { + const url = 'http://localhost:3000/page?debug=true#section'; + expect(cleanUrl(url)).toBe('http://localhost:3000/page'); + }); + + test('should handle file URLs', () => { + const url = 'file:///path/to/file.html?param=value'; + expect(cleanUrl(url)).toBe('file:///path/to/file.html'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index ed410f0..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1291 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" - integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -acorn-jsx@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" - integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== - -acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== - -ajv@^6.10.0, ajv@^6.10.2: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" - integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== - dependencies: - type-fest "^0.5.2" - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -array-includes@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" - integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" - integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -confusing-browser-globals@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" - integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== - -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.12.0, es-abstract@^1.7.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" - integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.0" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-inspect "^1.6.0" - object-keys "^1.1.1" - string.prototype.trimleft "^2.1.0" - string.prototype.trimright "^2.1.0" - -es-to-primitive@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-config-airbnb-base@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz#8a7bcb9643d13c55df4dd7444f138bf4efa61e17" - integrity sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA== - dependencies: - confusing-browser-globals "^1.0.7" - object.assign "^4.1.0" - object.entries "^1.1.0" - -eslint-config-prettier@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz#aaf9a495e2a816865e541bfdbb73a65cc162b3eb" - integrity sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ== - dependencies: - get-stdin "^6.0.0" - -eslint-config-standard@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz#b23da2b76fe5a2eba668374f246454e7058f15d4" - integrity sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA== - -eslint-import-resolver-node@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" - integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== - dependencies: - debug "^2.6.9" - resolve "^1.5.0" - -eslint-module-utils@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" - integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== - dependencies: - debug "^2.6.8" - pkg-dir "^2.0.0" - -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== - dependencies: - eslint-utils "^1.4.2" - regexpp "^3.0.0" - -eslint-plugin-import@^2.18.2: - version "2.18.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" - integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== - dependencies: - array-includes "^3.0.3" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.0" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.0" - read-pkg-up "^2.0.0" - resolve "^1.11.0" - -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== - dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-prettier@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba" - integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-plugin-promise@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" - integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== - -eslint-plugin-standard@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" - integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== - -eslint-scope@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" - integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.4.2, eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== - -eslint@^6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.1.tgz#269ccccec3ef60ab32358a44d147ac209154b919" - integrity sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^7.0.0" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.3" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" - integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== - dependencies: - acorn "^7.1.0" - acorn-jsx "^5.1.0" - eslint-visitor-keys "^1.1.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" - integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== - dependencies: - estraverse "^4.0.0" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -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" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -figures@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" - integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - -glob-parent@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" - integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" - integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== - dependencies: - type-fest "^0.8.1" - -graceful-fs@^4.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hosted-git-info@^2.1.4: - version "2.8.5" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" - integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.1: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== - -import-fresh@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inquirer@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" - integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== - dependencies: - ansi-escapes "^4.2.1" - chalk "^2.4.2" - cli-cursor "^3.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.15" - mute-stream "0.0.8" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^4.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -lodash@^4.17.14, lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -object-inspect@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.entries@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" - integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" - -object.values@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" - integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regexpp@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" - integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.5.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" - integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== - dependencies: - path-parse "^1.0.6" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= - dependencies: - is-promise "^2.1.0" - -rxjs@^6.4.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" - integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== - dependencies: - tslib "^1.9.0" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -"semver@2 || 3 || 4 || 5", semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.1.0, semver@^6.1.2: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimleft@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" - integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" - integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-json-comments@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tslib@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" - integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -v8-compile-cache@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" - integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" From bc8e39e3a6c83f80447096667e1fef1c5f201233 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 31 Aug 2025 08:27:10 -0700 Subject: [PATCH 70/92] Add unit test for generateSessionHash() --- js/background/spacesService.js | 2 +- test/generateSessionHash.test.js | 207 +++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 test/generateSessionHash.test.js diff --git a/js/background/spacesService.js b/js/background/spacesService.js index a84b696..3ce6791 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -909,4 +909,4 @@ function generateSessionHash(tabs) { export const spacesService = new SpacesService(); // Export the cleanUrl function for testing -export { cleanUrl }; +export { cleanUrl, generateSessionHash }; diff --git a/test/generateSessionHash.test.js b/test/generateSessionHash.test.js new file mode 100644 index 0000000..5380871 --- /dev/null +++ b/test/generateSessionHash.test.js @@ -0,0 +1,207 @@ +/** + * @jest-environment node + */ + +import { generateSessionHash } from '../js/background/spacesService.js'; + +// Mock chrome.runtime.id for testing +global.chrome = { + runtime: { + id: 'test-extension-id-12345' + } +}; + +describe('generateSessionHash', () => { + describe('deterministic behavior', () => { + test('should return same hash for identical input', () => { + const tabs = [ + { url: 'https://example.com' }, + { url: 'https://google.com' } + ]; + + const hash1 = generateSessionHash(tabs); + const hash2 = generateSessionHash(tabs); + + expect(hash1).toBe(hash2); + expect(typeof hash1).toBe('number'); + expect(hash1).toBeGreaterThan(0); + }); + + test('should return same hash when called multiple times with same data', () => { + const tabs = [{ url: 'https://example.com/page' }]; + + const hashes = []; + for (let i = 0; i < 5; i++) { + hashes.push(generateSessionHash(tabs)); + } + + // All hashes should be identical + expect(new Set(hashes).size).toBe(1); + }); + }); + + describe('hash uniqueness', () => { + test('should return different hashes for different tab sets', () => { + const tabs1 = [{ url: 'https://example.com' }]; + const tabs2 = [{ url: 'https://google.com' }]; + + const hash1 = generateSessionHash(tabs1); + const hash2 = generateSessionHash(tabs2); + + expect(hash1).not.toBe(hash2); + }); + + test('should return different hashes for different number of tabs', () => { + const tabs1 = [{ url: 'https://example.com' }]; + const tabs2 = [ + { url: 'https://example.com' }, + { url: 'https://google.com' } + ]; + + expect(generateSessionHash(tabs1)).not.toBe(generateSessionHash(tabs2)); + }); + + test('should return different hashes for same tabs in different order', () => { + const tabs1 = [ + { url: 'https://example.com' }, + { url: 'https://google.com' } + ]; + const tabs2 = [ + { url: 'https://google.com' }, + { url: 'https://example.com' } + ]; + + expect(generateSessionHash(tabs1)).not.toBe(generateSessionHash(tabs2)); + }); + }); + + describe('edge cases', () => { + test('should handle empty tabs array', () => { + const hash = generateSessionHash([]); + expect(hash).toBe(0); + expect(typeof hash).toBe('number'); + }); + + test('should handle tabs with no url property', () => { + const tabs = [ + { title: 'Some tab without URL' }, + { url: undefined }, + { url: null } + ]; + + // Should not crash and should return 0 since no valid URLs + expect(generateSessionHash(tabs)).toBe(0); + }); + + test('should handle tabs with empty URL strings', () => { + const tabs = [ + { url: '' }, + { url: ' ' }, + { url: 'https://example.com' } + ]; + + const hash = generateSessionHash(tabs); + expect(typeof hash).toBe('number'); + expect(hash).toBeGreaterThan(0); + }); + }); + + describe('integration with cleanUrl', () => { + test('should use cleanUrl for processing URLs', () => { + const tabs1 = [{ url: 'https://example.com?param=value#hash' }]; + const tabs2 = [{ url: 'https://example.com' }]; + + // Should be same hash because cleanUrl removes query and hash + expect(generateSessionHash(tabs1)).toBe(generateSessionHash(tabs2)); + }); + + test('should handle tabs with URLs that get filtered out by cleanUrl', () => { + const tabs = [ + { url: 'chrome-extension://test-extension-id-12345/popup.html' }, + { url: 'chrome:// newtab/' } + ]; + + // All URLs get filtered by cleanUrl, so hash should be 0 + expect(generateSessionHash(tabs)).toBe(0); + }); + + test('should handle mixed filtered and valid URLs', () => { + const tabs1 = [ + { url: 'https://example.com' }, + { url: 'chrome-extension://test-extension-id-12345/popup.html' }, + { url: 'https://google.com' } + ]; + + const tabs2 = [ + { url: 'https://example.com' }, + { url: 'https://google.com' } + ]; + + // Should be same hash since filtered URLs are ignored + expect(generateSessionHash(tabs1)).toBe(generateSessionHash(tabs2)); + }); + + test('should handle Great Suspender URLs correctly', () => { + const tabs1 = [{ + url: 'chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=test&pos=0&uri=https://example.com' + }]; + const tabs2 = [{ url: 'https://example.com' }]; + + // Should be same hash because cleanUrl extracts the real URL from Great Suspender + expect(generateSessionHash(tabs1)).toBe(generateSessionHash(tabs2)); + }); + }); + + describe('hash properties', () => { + test('should always return positive integers', () => { + const testCases = [ + [{ url: 'https://example.com' }], + [{ url: 'https://google.com' }, { url: 'https://github.com' }], + [{ url: 'http://localhost:3000' }], + [{ url: 'https://very-long-domain-name-that-might-cause-issues.com/with/long/path/segments' }] + ]; + + testCases.forEach(tabs => { + const hash = generateSessionHash(tabs); + expect(hash).toBeGreaterThan(0); + expect(Number.isInteger(hash)).toBe(true); + }); + }); + + test('should handle unicode characters in URLs', () => { + const tabs = [{ url: 'https://example.com/café/naïve' }]; + const hash = generateSessionHash(tabs); + + expect(typeof hash).toBe('number'); + expect(hash).toBeGreaterThan(0); + expect(Number.isInteger(hash)).toBe(true); + }); + + test('should be consistent across different execution contexts', () => { + // Test with a variety of URLs to ensure algorithm stability + const tabs = [ + { url: 'https://example.com' }, + { url: 'https://www.google.com/search?q=test' }, + { url: 'https://github.com/user/repo/issues/123' } + ]; + + const hash1 = generateSessionHash(tabs); + const hash2 = generateSessionHash(JSON.parse(JSON.stringify(tabs))); // Deep copy + + expect(hash1).toBe(hash2); + }); + }); + + describe('known values for regression testing', () => { + test('should generate expected hash for simple known input', () => { + // This test helps catch unintended changes to the algorithm + const tabs = [{ url: 'https://example.com' }]; + const hash = generateSessionHash(tabs); + + // Regression test - this specific input should always produce the same hash + expect(hash).toBe(632849614); + expect(typeof hash).toBe('number'); + expect(hash).toBeGreaterThan(0); + }); + }); +}); From b1732c57cfc3aa7b9689fd797a79769ed98e8022 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 31 Aug 2025 08:32:56 -0700 Subject: [PATCH 71/92] Update jest coverage for all files. --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d1253b..004b665 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" }, "jest": { - "transform": {}, + "collectCoverageFrom": [ + "js/**/*.js", + "!js/**/*.test.js", + "!**/node_modules/**" + ], "testEnvironment": "node" }, "devDependencies": { From e1d4d0f7d3c806c272628f31453a60de1531a566 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 3 Sep 2025 21:59:20 -0700 Subject: [PATCH 72/92] Add unit test for cleanParameter() --- js/background/background.js | 292 ++++++++++++++++++------------------ js/background/main.js | 3 + manifest.json | 2 +- test/cleanParameter.test.js | 104 +++++++++++++ 4 files changed, 256 insertions(+), 145 deletions(-) create mode 100644 js/background/main.js create mode 100644 test/cleanParameter.test.js diff --git a/js/background/background.js b/js/background/background.js index bc8911a..0fcd4f6 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -54,148 +54,172 @@ async function rediscoverWindowByUrl(storageKey, htmlFilename) { return false; } -// runtime extension install listener -chrome.runtime.onInstalled.addListener(details => { - console.log(`Extension installed: ${JSON.stringify(details)}`); +export function initializeServiceWorker() { + console.log(`Initializing service worker...`); - if (details.reason === 'install') { - // eslint-disable-next-line no-console - console.log('This is a first install!'); - showSpacesOpenWindow(); - } else if (details.reason === 'update') { - const thisVersion = chrome.runtime.getManifest().version; - if (details.previousVersion !== thisVersion) { + chrome.runtime.onInstalled.addListener(details => { + console.log(`Extension installed: ${JSON.stringify(details)}`); + + if (details.reason === 'install') { // eslint-disable-next-line no-console - console.log( - `Updated from ${details.previousVersion} to ${thisVersion}!` - ); + console.log('This is a first install!'); + showSpacesOpenWindow(); + } else if (details.reason === 'update') { + const thisVersion = chrome.runtime.getManifest().version; + if (details.previousVersion !== thisVersion) { + // eslint-disable-next-line no-console + console.log( + `Updated from ${details.previousVersion} to ${thisVersion}!` + ); + } } - } - chrome.contextMenus.create({ - id: 'spaces-add-link', - title: 'Add link to space...', - contexts: ['link'], + chrome.contextMenus.create({ + id: 'spaces-add-link', + title: 'Add link to space...', + contexts: ['link'], + }); }); -}); - -// Handle Chrome startup - this is when window IDs get reassigned! -chrome.runtime.onStartup.addListener(async () => { - await spacesService.clearWindowIdAssociations(); - await spacesService.initialiseSpaces(); - await rediscoverWindowIds(); -}); - -// LISTENERS - -// add listeners for session monitoring -chrome.tabs.onCreated.addListener(async (tab) => { - // this call to checkInternalSpacesWindows actually returns false when it should return true - // due to the event being called before the globalWindowIds get set. oh well, never mind. - if (checkInternalSpacesWindows(tab.windowId, false)) return; - // don't need this listener as the tabUpdated listener also fires when a new tab is created - // spacesService.handleTabCreated(tab); - updateSpacesWindow('tabs.onCreated'); -}); - -chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { - if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; - spacesService.handleTabRemoved(tabId, removeInfo, () => { - updateSpacesWindow('tabs.onRemoved'); + + // Handle Chrome startup - this is when window IDs get reassigned! + chrome.runtime.onStartup.addListener(async () => { + await spacesService.clearWindowIdAssociations(); + await spacesService.initialiseSpaces(); + await rediscoverWindowIds(); }); -}); -chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { - if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; - spacesService.handleTabMoved(tabId, moveInfo, () => { - updateSpacesWindow('tabs.onMoved'); + // LISTENERS + + // add listeners for session monitoring + chrome.tabs.onCreated.addListener(async (tab) => { + // this call to checkInternalSpacesWindows actually returns false when it should return true + // due to the event being called before the globalWindowIds get set. oh well, never mind. + if (checkInternalSpacesWindows(tab.windowId, false)) return; + // don't need this listener as the tabUpdated listener also fires when a new tab is created + // spacesService.handleTabCreated(tab); + updateSpacesWindow('tabs.onCreated'); }); -}); -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (checkInternalSpacesWindows(tab.windowId, false)) return; + chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => { + if (checkInternalSpacesWindows(removeInfo.windowId, false)) return; + spacesService.handleTabRemoved(tabId, removeInfo, () => { + updateSpacesWindow('tabs.onRemoved'); + }); + }); - spacesService.handleTabUpdated(tab, changeInfo, () => { - updateSpacesWindow('tabs.onUpdated'); + chrome.tabs.onMoved.addListener(async (tabId, moveInfo) => { + if (checkInternalSpacesWindows(moveInfo.windowId, false)) return; + spacesService.handleTabMoved(tabId, moveInfo, () => { + updateSpacesWindow('tabs.onMoved'); + }); }); -}); -chrome.windows.onRemoved.addListener(async (windowId) => { - if (checkInternalSpacesWindows(windowId, true)) return; - const wasProcessed = await spacesService.handleWindowRemoved(windowId, true); - if (wasProcessed) { - updateSpacesWindow('windows.onRemoved'); - } + chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (checkInternalSpacesWindows(tab.windowId, false)) return; - // if this was the last window open and the spaces window is stil open - // then close the spaces window also so that chrome exits fully - // NOTE: this is a workaround for an issue with the chrome 'restore previous session' option - // if the spaces window is the only window open and you try to use it to open a space, - // when that space loads, it also loads all the windows from the window that was last closed - const windows = await chrome.windows.getAll({}); - if (windows.length === 1 && spacesOpenWindowId) { - await chrome.windows.remove(spacesOpenWindowId); - spacesOpenWindowId = false; - await chrome.storage.local.remove('spacesOpenWindowId'); - } -}); - -// don't need this listener as the tabUpdated listener also fires when a new window is created -// chrome.windows.onCreated.addListener(function (window) { - -// if (checkInternalSpacesWindows(window.id, false)) return; -// spacesService.handleWindowCreated(window); -// }); - -// add listeners for tab and window focus changes -// when a tab or window is changed, close the move tab popup if it is open -chrome.windows.onFocusChanged.addListener(async (windowId) => { - // Prevent a click in the popup on Ubunto or ChroneOS from closing the - // popup prematurely. - if ( - windowId === chrome.windows.WINDOW_ID_NONE || - windowId === spacesPopupWindowId - ) { - return; - } + spacesService.handleTabUpdated(tab, changeInfo, () => { + updateSpacesWindow('tabs.onUpdated'); + }); + }); - if (!debug && spacesPopupWindowId) { - if (spacesPopupWindowId) { - await closePopupWindow(); + chrome.windows.onRemoved.addListener(async (windowId) => { + if (checkInternalSpacesWindows(windowId, true)) return; + const wasProcessed = await spacesService.handleWindowRemoved(windowId, true); + if (wasProcessed) { + updateSpacesWindow('windows.onRemoved'); } - } - - spacesService.handleWindowFocussed(windowId); -}); -// add listeners for message requests from other extension pages (spaces.html & tab.html) + // if this was the last window open and the spaces window is stil open + // then close the spaces window also so that chrome exits fully + // NOTE: this is a workaround for an issue with the chrome 'restore previous session' option + // if the spaces window is the only window open and you try to use it to open a space, + // when that space loads, it also loads all the windows from the window that was last closed + const windows = await chrome.windows.getAll({}); + if (windows.length === 1 && spacesOpenWindowId) { + await chrome.windows.remove(spacesOpenWindowId); + spacesOpenWindowId = false; + await chrome.storage.local.remove('spacesOpenWindowId'); + } + }); -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (debug) { - // eslint-disable-next-line no-console - console.log(`listener fired: ${JSON.stringify(request)}`); - } + // don't need this listener as the tabUpdated listener also fires when a new window is created + // chrome.windows.onCreated.addListener(function (window) { + + // if (checkInternalSpacesWindows(window.id, false)) return; + // spacesService.handleWindowCreated(window); + // }); + + // add listeners for tab and window focus changes + // when a tab or window is changed, close the move tab popup if it is open + chrome.windows.onFocusChanged.addListener(async (windowId) => { + // Prevent a click in the popup on Ubunto or ChroneOS from closing the + // popup prematurely. + if ( + windowId === chrome.windows.WINDOW_ID_NONE || + windowId === spacesPopupWindowId + ) { + return; + } - // Handle async processing - (async () => { - try { - // Ensure spacesService is initialized before processing any message - await spacesService.ensureInitialized(); - - const response = await processMessage(request, sender); - if (response !== undefined) { - sendResponse(response); + if (!debug && spacesPopupWindowId) { + if (spacesPopupWindowId) { + await closePopupWindow(); } - } catch (error) { - console.error('Error processing message:', error); - sendResponse(false); } - })(); - - // We must return true synchronously to keep the message port open - // for our async sendResponse() calls - return true; -}); + + spacesService.handleWindowFocussed(windowId); + }); + + // add listeners for message requests from other extension pages (spaces.html & tab.html) + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (debug) { + // eslint-disable-next-line no-console + console.log(`listener fired: ${JSON.stringify(request)}`); + } + + // Handle async processing + (async () => { + try { + // Ensure spacesService is initialized before processing any message + await spacesService.ensureInitialized(); + + const response = await processMessage(request, sender); + if (response !== undefined) { + sendResponse(response); + } + } catch (error) { + console.error('Error processing message:', error); + sendResponse(false); + } + })(); + + // We must return true synchronously to keep the message port open + // for our async sendResponse() calls + return true; + }); + + chrome.commands.onCommand.addListener(command => { + // handle showing the move tab popup (tab.html) + if (command === 'spaces-move') { + showSpacesMoveWindow(); + + // handle showing the switcher tab popup (switcher.html) + } else if (command === 'spaces-switch') { + showSpacesSwitchWindow(); + } + }); + + chrome.contextMenus.onClicked.addListener(info => { + // handle showing the move tab popup (tab.html) + if (info.menuItemId === 'spaces-add-link') { + showSpacesMoveWindow(info.linkUrl); + } + }); + + console.log(`Initializing spacesService...`); + spacesService.initialiseSpaces(); +} /** * Processes incoming messages from extension pages and returns appropriate responses. @@ -490,26 +514,6 @@ function cleanParameter(param) { return parseInt(param, 10); } -// add listeners for keyboard commands - -chrome.commands.onCommand.addListener(command => { - // handle showing the move tab popup (tab.html) - if (command === 'spaces-move') { - showSpacesMoveWindow(); - - // handle showing the switcher tab popup (switcher.html) - } else if (command === 'spaces-switch') { - showSpacesSwitchWindow(); - } -}); - -chrome.contextMenus.onClicked.addListener(info => { - // handle showing the move tab popup (tab.html) - if (info.menuItemId === 'spaces-add-link') { - showSpacesMoveWindow(info.linkUrl); - } -}); - function createShortcutsWindow() { chrome.tabs.create({ url: 'chrome://extensions/configureCommands' }); } @@ -1250,5 +1254,5 @@ function moveTabToWindow(tab, windowId) { spacesService.queueWindowEvent(windowId); } -console.log(`Initializing spacesService...`); -spacesService.initialiseSpaces(); +// Exports for testing. +export { cleanParameter }; \ No newline at end of file diff --git a/js/background/main.js b/js/background/main.js new file mode 100644 index 0000000..3c1e66b --- /dev/null +++ b/js/background/main.js @@ -0,0 +1,3 @@ +import { initializeServiceWorker } from './background.js'; + +initializeServiceWorker(); diff --git a/manifest.json b/manifest.json index 8d4bec1..995897d 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,7 @@ "unlimitedStorage" ], "background": { - "service_worker": "js/background/background.js", + "service_worker": "js/background/main.js", "type": "module" }, "action": { diff --git a/test/cleanParameter.test.js b/test/cleanParameter.test.js new file mode 100644 index 0000000..2445808 --- /dev/null +++ b/test/cleanParameter.test.js @@ -0,0 +1,104 @@ +/** + * @jest-environment node + */ + +import { cleanParameter } from '../js/background/background.js'; + +describe('cleanParameter', () => { + describe('number inputs', () => { + test('should return numbers unchanged', () => { + expect(cleanParameter(42)).toBe(42); + expect(cleanParameter(0)).toBe(0); + expect(cleanParameter(-5)).toBe(-5); + expect(cleanParameter(3.14)).toBe(3.14); + }); + }); + + describe('boolean string inputs', () => { + test('should convert boolean strings to boolean values', () => { + expect(cleanParameter('false')).toBe(false); + expect(cleanParameter('true')).toBe(true); + }); + + test('should be case-sensitive for boolean strings', () => { + // These should not be treated as booleans + expect(cleanParameter('False')).toBe(NaN); + }); + }); + + describe('numeric string inputs', () => { + test('should parse numeric strings to integers', () => { + expect(cleanParameter('123')).toBe(123); + expect(cleanParameter('0')).toBe(0); + expect(cleanParameter('-5')).toBe(-5); + }); + + test('should parse decimal strings as integers', () => { + // parseInt truncates decimals + expect(cleanParameter('3.14')).toBe(3); + }); + + test('should handle numeric strings with whitespace', () => { + expect(cleanParameter(' 42 ')).toBe(42); + }); + }); + + describe('edge cases and invalid inputs', () => { + test('should handle null and undefined', () => { + expect(cleanParameter(null)).toBe(NaN); + }); + + test('should handle empty string', () => { + expect(cleanParameter('')).toBe(NaN); + }); + + test('should handle non-numeric strings', () => { + expect(cleanParameter('abc')).toBe(NaN); + }); + + test('should handle strings that start with numbers', () => { + // parseInt parses until it hits non-numeric character + expect(cleanParameter('123abc')).toBe(123); + }); + + test('should handle special values', () => { + expect(cleanParameter('Infinity')).toBe(NaN); + }); + + test('should handle objects and arrays', () => { + expect(cleanParameter({})).toBe(NaN); + expect(cleanParameter([])).toBe(NaN); + }); + }); + + describe('type consistency', () => { + test('should return correct types based on input', () => { + // Numbers should return numbers + expect(typeof cleanParameter(42)).toBe('number'); + + // Boolean strings should return booleans + expect(typeof cleanParameter('true')).toBe('boolean'); + + // Invalid inputs should return numbers (NaN) + expect(typeof cleanParameter('abc')).toBe('number'); + }); + + test('should handle NaN consistently', () => { + const result = cleanParameter('invalid'); + expect(Number.isNaN(result)).toBe(true); + expect(typeof result).toBe('number'); + }); + }); + + describe('parameter validation use cases', () => { + test('should handle window IDs correctly', () => { + // Typical window ID scenarios + expect(cleanParameter('1234567890')).toBe(1234567890); + }); + + test('should handle edge cases from Chrome extension context', () => { + // Chrome might pass these edge cases + expect(cleanParameter('-1')).toBe(-1); // WINDOW_ID_NONE + }); + }); +}); From 060e8b211b7b513915ff658b37a0d8b933fdda2f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 3 Sep 2025 21:59:32 -0700 Subject: [PATCH 73/92] Tweak to README --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 8bd4230..6f95cf3 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,8 @@ Please note that the webstore version may be behind the latest version here. ### Install as an extension from source -1. Download the **[latest available version](https://github.com/deanoemcke/spaces/archive/v1.1.1.zip)** +1. Download the **[latest available version](https://github.com/codedread/spaces/archive/v1.1.4.zip)** 2. Unarchive to your preferred location (e.g., `Downloads`). 2. In **Google Chrome**, navigate to [chrome://extensions/](chrome://extensions/) and enable Developer mode in the upper right corner. 3. Click on the LOAD UNPACKED button. 4. Browse to the _root directory_ of the unarchived download, and click OPEN. - -> **TODO** — add more sections -> - [ ] Build from github -> - [ ] License (currently unspecified) From 4fef9c710be9d6ca1ae1e0546abb4f67505021a3 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 3 Sep 2025 22:01:26 -0700 Subject: [PATCH 74/92] Clean up noop in spaces.js --- js/background/background.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/background/background.js b/js/background/background.js index 0fcd4f6..a30f3ed 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -14,7 +14,6 @@ import * as common from '../common.js'; // eslint-disable-next-line no-unused-vars, no-var let spacesPopupWindowId = false; let spacesOpenWindowId = false; -const noop = () => {}; const debug = false; async function rediscoverWindowIds() { From c7a38fb885d1eee374b390eb5f3df3ea55945d17 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 3 Sep 2025 22:01:37 -0700 Subject: [PATCH 75/92] No, really. Clean it up --- js/spaces.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/js/spaces.js b/js/spaces.js index bbc9d2e..143be8e 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -248,12 +248,10 @@ async function handleLoadSpace(sessionId, windowId) { } async function handleLoadTab(sessionId, windowId, tabUrl) { - const noop = () => {}; - if (sessionId) { - await performLoadTabInSession(sessionId, tabUrl, noop); + await performLoadTabInSession(sessionId, tabUrl); } else if (windowId) { - await performLoadTabInWindow(windowId, tabUrl, noop); + await performLoadTabInWindow(windowId, tabUrl); } } From 2529bbb0fdcdf9a9230de01a8cb9bd5ade88a375 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 11:06:28 -0700 Subject: [PATCH 76/92] Add unit tests for utils.getHashVariable() --- test/getHashVariable.test.js | 163 +++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 test/getHashVariable.test.js diff --git a/test/getHashVariable.test.js b/test/getHashVariable.test.js new file mode 100644 index 0000000..7cbb67a --- /dev/null +++ b/test/getHashVariable.test.js @@ -0,0 +1,163 @@ +/** + * @jest-environment node + */ + +import { utils } from '../js/utils.js'; + +describe('utils.getHashVariable', () => { + describe('basic functionality', () => { + test('should return false for invalid inputs', () => { + expect(utils.getHashVariable('key', null)).toBe(false); + expect(utils.getHashVariable('key', undefined)).toBe(false); + expect(utils.getHashVariable('key', '')).toBe(false); + }); + + test('should return false for URL without hash', () => { + expect(utils.getHashVariable('key', 'https://example.com')).toBe(false); + }); + + test('should return false for URL with empty hash', () => { + expect(utils.getHashVariable('key', 'https://example.com#')).toBe(false); + }); + }); + + describe('single key-value pairs', () => { + test('should extract single key-value pair', () => { + const url = 'https://example.com#key=value'; + expect(utils.getHashVariable('key', url)).toBe('value'); + }); + + test('should return false for non-existent key', () => { + const url = 'https://example.com#key=value'; + expect(utils.getHashVariable('missing', url)).toBe(false); + }); + + test('should handle numeric values', () => { + const url = 'https://example.com#id=123'; + expect(utils.getHashVariable('id', url)).toBe('123'); + }); + + test('should return false for empty values', () => { + const url = 'https://example.com#key='; + // The regex /^(.+)=(.+)/ requires at least one character after = + expect(utils.getHashVariable('key', url)).toBe(false); + }); + }); + + describe('multiple key-value pairs', () => { + test('should extract keys from any position in multiple pairs', () => { + const url = 'https://example.com#first=value1&second=value2&third=value3'; + expect(utils.getHashVariable('first', url)).toBe('value1'); // first position + expect(utils.getHashVariable('second', url)).toBe('value2'); // middle position + expect(utils.getHashVariable('third', url)).toBe('value3'); // last position + }); + + test('should return false for non-existent key in multiple pairs', () => { + const url = 'https://example.com#first=value1&second=value2&third=value3'; + expect(utils.getHashVariable('fourth', url)).toBe(false); + }); + }); + + describe('special characters and encoding', () => { + test('should handle values with special characters', () => { + const url = 'https://example.com#message=hello%20world'; + expect(utils.getHashVariable('message', url)).toBe('hello%20world'); + }); + + test('should handle keys with special characters', () => { + const url = 'https://example.com#special-key=value'; + expect(utils.getHashVariable('special-key', url)).toBe('value'); + }); + + test('should return false for values with equals signs', () => { + // The regex /^(.+)=(.+)/ doesn't handle multiple = signs properly + const url = 'https://example.com#equation=x=y'; + expect(utils.getHashVariable('equation', url)).toBe(false); + }); + + test('should handle values with ampersands in query parameters before hash', () => { + const url = 'https://example.com?param1=val1¶m2=val2#key=value'; + expect(utils.getHashVariable('key', url)).toBe('value'); + }); + }); + + describe('malformed hash fragments', () => { + test('should ignore key-value pairs without equals sign', () => { + const url = 'https://example.com#validkey=validvalue&invalidpair&anotherkey=anothervalue'; + expect(utils.getHashVariable('validkey', url)).toBe('validvalue'); + expect(utils.getHashVariable('anotherkey', url)).toBe('anothervalue'); + expect(utils.getHashVariable('invalidpair', url)).toBe(false); + }); + + test('should ignore empty key-value pairs', () => { + const url = 'https://example.com#key=value&&anotherkey=anothervalue'; + expect(utils.getHashVariable('key', url)).toBe('value'); + expect(utils.getHashVariable('anotherkey', url)).toBe('anothervalue'); + }); + + test('should handle hash with only ampersands', () => { + const url = 'https://example.com#&&&'; + expect(utils.getHashVariable('key', url)).toBe(false); + }); + }); + + describe('edge cases', () => { + test('should include everything after first hash including additional hash symbols', () => { + const url = 'https://example.com#key=value#extra'; + // The regex extracts everything after the first # symbol + expect(utils.getHashVariable('key', url)).toBe('value#extra'); + }); + + test('should be case-sensitive for keys', () => { + const url = 'https://example.com#Key=value'; + expect(utils.getHashVariable('key', url)).toBe(false); + expect(utils.getHashVariable('Key', url)).toBe('value'); + }); + + test('should handle duplicate keys (last one wins behavior)', () => { + const url = 'https://example.com#key=first&key=second'; + // Based on the implementation, the last key should overwrite the first + expect(utils.getHashVariable('key', url)).toBe('second'); + }); + + test('should return false for hash at the beginning of string', () => { + const url = '#key=value'; + // The regex /^[^#]+#+(.*)/ expects at least one non-# character before # + expect(utils.getHashVariable('key', url)).toBe(false); + }); + + test('should handle very long values', () => { + const longValue = 'a'.repeat(1000); + const url = `https://example.com#data=${longValue}`; + expect(utils.getHashVariable('data', url)).toBe(longValue); + }); + }); + + describe('regex behavior documentation', () => { + test('should extract hash correctly with complex URLs', () => { + const complexUrl = 'https://user:pass@example.com:8080/path?query=value&other=test#key=hashvalue&second=pair'; + expect(utils.getHashVariable('key', complexUrl)).toBe('hashvalue'); + expect(utils.getHashVariable('second', complexUrl)).toBe('pair'); + }); + }); + + describe('real-world URL scenarios', () => { + test('should handle Chrome extension URLs', () => { + const url = 'chrome-extension://abcdef123456/spaces.html#sessionId=123&editMode=true'; + expect(utils.getHashVariable('sessionId', url)).toBe('123'); + expect(utils.getHashVariable('editMode', url)).toBe('true'); + }); + + test('should handle file URLs', () => { + const url = 'file:///path/to/file.html#section=intro&version=1.0'; + expect(utils.getHashVariable('section', url)).toBe('intro'); + expect(utils.getHashVariable('version', url)).toBe('1.0'); + }); + + test('should handle URLs with ports', () => { + const url = 'http://localhost:3000/app#tab=settings&debug=true'; + expect(utils.getHashVariable('tab', url)).toBe('settings'); + expect(utils.getHashVariable('debug', url)).toBe('true'); + }); + }); +}); From abb31817720bfcea1a8a0f557d2ff5b23124a234 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 11:23:32 -0700 Subject: [PATCH 77/92] Turn spacesService.filterInternalWindows() into a helper function and write unit tests for it. --- js/background/spacesService.js | 53 ++++++---- test/filterInternalWindows.test.js | 150 +++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 test/filterInternalWindows.test.js diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 3ce6791..7d62c3e 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -68,7 +68,7 @@ class SpacesService { // then try to match current open windows with saved sessions for (const curWindow of windows) { - if (!this.filterInternalWindows(curWindow)) { + if (!filterInternalWindows(curWindow)) { await this.checkForSessionMatchDuringInit(curWindow); } } @@ -129,22 +129,6 @@ class SpacesService { } } - filterInternalWindows(curWindow) { - // sanity check to make sure window isnt an internal spaces window - if ( - curWindow.tabs.length === 1 && - curWindow.tabs[0].url.indexOf(chrome.runtime.id) >= 0 - ) { - return true; - } - - // also filter out popup or panel window types - if (curWindow.type === 'popup' || curWindow.type === 'panel') { - return true; - } - return false; - } - async checkForSessionMatchDuringInit(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; @@ -522,7 +506,7 @@ class SpacesService { return false; } - if (!curWindow || this.filterInternalWindows(curWindow)) { + if (!curWindow || filterInternalWindows(curWindow)) { return false; } @@ -873,6 +857,35 @@ function cleanUrl(url) { return processedUrl; } +/** + * Filters out internal Chrome extension windows that should be ignored by the Spaces extension. + * This includes windows containing only the Spaces extension's own pages, as well as popup + * and panel type windows. + * + * @param {chrome.windows.Window} curWindow - The Chrome window object to check + * @returns {boolean} True if the window should be filtered out (ignored), false otherwise + * + * @example + * filterInternalWindows({ tabs: [{ url: 'chrome-extension://abc123/spaces.html' }], type: 'normal' }) // returns true + * filterInternalWindows({ tabs: [{ url: 'https://example.com' }], type: 'popup' }) // returns true + * filterInternalWindows({ tabs: [{ url: 'https://example.com' }], type: 'normal' }) // returns false + */ +function filterInternalWindows(curWindow) { + // sanity check to make sure window isnt an internal spaces window + if ( + curWindow.tabs.length === 1 && + curWindow.tabs[0].url.indexOf(chrome.runtime.id) >= 0 + ) { + return true; + } + + // also filter out popup or panel window types + if (curWindow.type === 'popup' || curWindow.type === 'panel') { + return true; + } + return false; +} + /** * Generates a unique hash for a browser session based on the URLs of its tabs. * This hash is used to match existing sessions when windows are reopened after Chrome restart. @@ -908,5 +921,5 @@ function generateSessionHash(tabs) { // Export an instance of the SpacesService class export const spacesService = new SpacesService(); -// Export the cleanUrl function for testing -export { cleanUrl, generateSessionHash }; +// Export helper functions for testing +export { cleanUrl, filterInternalWindows, generateSessionHash }; diff --git a/test/filterInternalWindows.test.js b/test/filterInternalWindows.test.js new file mode 100644 index 0000000..73fb6f3 --- /dev/null +++ b/test/filterInternalWindows.test.js @@ -0,0 +1,150 @@ +/** + * @jest-environment node + */ + +import { filterInternalWindows } from '../js/background/spacesService.js'; + +// Mock chrome.runtime.id for testing +global.chrome = { + runtime: { + id: 'test-extension-id-12345' + } +}; + +describe('filterInternalWindows', () => { + describe('normal windows (should not be filtered)', () => { + test('should not filter normal windows with regular websites', () => { + // Single tab + const tabWindow = { + tabs: [{ url: 'https://example.com' }], + type: 'normal' + }; + expect(filterInternalWindows(tabWindow)).toBe(false); + + // Multiple tabs + tabWindow.tabs.push({ url: 'https://example.com' }); + expect(filterInternalWindows(tabWindow)).toBe(false); + }); + + test('should not filter window with extension URL among other tabs', () => { + const window = { + tabs: [ + { url: 'https://example.com' }, + { url: `chrome-extension://${chrome.runtime.id}/spaces.html` } + ], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(false); + }); + }); + + describe('internal extension windows (should be filtered)', () => { + test('should filter window with single tab containing extension URL', () => { + const window = { + tabs: [{ url: `chrome-extension://${chrome.runtime.id}/spaces.html` }], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + + test('should filter window with single tab containing extension popup', () => { + const window = { + tabs: [{ url: `chrome-extension://${chrome.runtime.id}/popup.html` }], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + + test('should filter window with single tab containing any extension page', () => { + const window = { + tabs: [{ url: `chrome-extension://${chrome.runtime.id}/any-page.html?param=value` }], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + }); + + describe('popup and panel windows (should be filtered)', () => { + test('should filter popup window type', () => { + const window = { + tabs: [{ url: 'https://example.com' }], + type: 'popup' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + + test('should filter panel window type', () => { + const window = { + tabs: [{ url: 'https://example.com' }], + type: 'panel' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + + test('should filter popup window even with multiple tabs', () => { + const window = { + tabs: [ + { url: 'https://example.com' }, + { url: 'https://google.com' } + ], + type: 'popup' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + }); + + describe('edge cases', () => { + test('should not filter window with empty tabs array', () => { + const window = { + tabs: [], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(false); + }); + + test('should handle different extension IDs correctly', () => { + const window = { + tabs: [{ url: 'chrome-extension://different-extension-id/page.html' }], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(false); + }); + + test('should handle partial matches in URL (possible bug)', () => { + // NOTE: This tests current behavior but may be unintended. + // The function uses indexOf() which matches extension ID anywhere in the URL, + // not just in proper chrome-extension:// URLs + const window = { + tabs: [{ url: `https://example.com/page-${chrome.runtime.id}` }], + type: 'normal' + }; + expect(filterInternalWindows(window)).toBe(true); + }); + + test('should handle undefined window type', () => { + const window = { + tabs: [{ url: 'https://example.com' }] + // type is undefined + }; + expect(filterInternalWindows(window)).toBe(false); + }); + }); + + describe('chrome-specific window types', () => { + test('should not filter app window type', () => { + const window = { + tabs: [{ url: 'https://example.com' }], + type: 'app' + }; + expect(filterInternalWindows(window)).toBe(false); + }); + + test('should not filter devtools window type', () => { + const window = { + tabs: [{ url: 'devtools://devtools/bundled/inspector.html' }], + type: 'devtools' + }; + expect(filterInternalWindows(window)).toBe(false); + }); + }); +}); From bee1b6c3fcaf4bc0bc135e0631b12a65357d2ffe Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 11:25:56 -0700 Subject: [PATCH 78/92] Rename test folder to tests. --- {test => tests}/README.md | 0 {test => tests}/cleanParameter.test.js | 0 {test => tests}/cleanUrl.test.js | 0 {test => tests}/filterInternalWindows.test.js | 0 {test => tests}/generateSessionHash.test.js | 0 {test => tests}/getHashVariable.test.js | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {test => tests}/README.md (100%) rename {test => tests}/cleanParameter.test.js (100%) rename {test => tests}/cleanUrl.test.js (100%) rename {test => tests}/filterInternalWindows.test.js (100%) rename {test => tests}/generateSessionHash.test.js (100%) rename {test => tests}/getHashVariable.test.js (100%) diff --git a/test/README.md b/tests/README.md similarity index 100% rename from test/README.md rename to tests/README.md diff --git a/test/cleanParameter.test.js b/tests/cleanParameter.test.js similarity index 100% rename from test/cleanParameter.test.js rename to tests/cleanParameter.test.js diff --git a/test/cleanUrl.test.js b/tests/cleanUrl.test.js similarity index 100% rename from test/cleanUrl.test.js rename to tests/cleanUrl.test.js diff --git a/test/filterInternalWindows.test.js b/tests/filterInternalWindows.test.js similarity index 100% rename from test/filterInternalWindows.test.js rename to tests/filterInternalWindows.test.js diff --git a/test/generateSessionHash.test.js b/tests/generateSessionHash.test.js similarity index 100% rename from test/generateSessionHash.test.js rename to tests/generateSessionHash.test.js diff --git a/test/getHashVariable.test.js b/tests/getHashVariable.test.js similarity index 100% rename from test/getHashVariable.test.js rename to tests/getHashVariable.test.js From 14596984f6d609bebd99c249718f5bae0416ba1d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 12:30:53 -0700 Subject: [PATCH 79/92] Add unit tests for spaces.js normaliseTablUrl() --- js/spaces.js | 116 +++++++++++++++++++++------------- tests/normaliseTabUrl.test.js | 52 +++++++++++++++ 2 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 tests/normaliseTabUrl.test.js diff --git a/js/spaces.js b/js/spaces.js index 143be8e..014f235 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -430,14 +430,6 @@ async function handleExport() { link.click(); } -function normaliseTabUrl(url) { - let normalisedUrl = url; - if (url.indexOf('suspended.html') > 0 && url.indexOf('uri=') > 0) { - normalisedUrl = url.substring(url.indexOf('uri=') + 4, url.length); - } - return normalisedUrl; -} - // SERVICES /** @returns {Promise} */ @@ -712,40 +704,74 @@ function addDuplicateMetadata(space) { }); } -window.onload = () => { - // initialise global handles to key elements (singletons) - nodes.home = document.getElementById('spacesHome'); - nodes.openSpaces = document.getElementById('openSpaces'); - nodes.closedSpaces = document.getElementById('closedSpaces'); - nodes.activeTabs = document.getElementById('activeTabs'); - nodes.historicalTabs = document.getElementById('historicalTabs'); - nodes.spaceDetailContainer = document.querySelector( - '.content .contentBody' - ); - nodes.nameForm = document.querySelector('#nameForm'); - nodes.nameFormDisplay = document.querySelector('#nameForm span'); - nodes.nameFormInput = document.querySelector('#nameForm input'); - nodes.actionSwitch = document.getElementById('actionSwitch'); - nodes.actionOpen = document.getElementById('actionOpen'); - nodes.actionEdit = document.getElementById('actionEdit'); - nodes.actionExport = document.getElementById('actionExport'); - nodes.actionBackup = document.getElementById('actionBackup'); - nodes.actionDelete = document.getElementById('actionDelete'); - nodes.actionImport = document.getElementById('actionImport'); - nodes.banner = document.getElementById('banner'); - nodes.modalBlocker = document.querySelector('.blocker'); - nodes.modalContainer = document.querySelector('.modal'); - nodes.modalInput = document.getElementById('importTextArea'); - nodes.modalButton = document.getElementById('importBtn'); - - nodes.home.setAttribute('href', chrome.runtime.getURL('spaces.html')); - - // initialise event listeners for static elements - addEventListeners(); - - // render side nav - updateSpacesList(); - - // render main content - updateSpaceDetail(); -}; +/** + * Initialize the spaces window. + * This function should be called from the HTML page after the DOM is loaded. + */ +// Auto-initialize when loaded in browser context +if (typeof window !== 'undefined') { + window.onload = () => { + // initialise global handles to key elements (singletons) + nodes.home = document.getElementById('spacesHome'); + nodes.openSpaces = document.getElementById('openSpaces'); + nodes.closedSpaces = document.getElementById('closedSpaces'); + nodes.activeTabs = document.getElementById('activeTabs'); + nodes.historicalTabs = document.getElementById('historicalTabs'); + nodes.spaceDetailContainer = document.querySelector( + '.content .contentBody' + ); + nodes.nameForm = document.querySelector('#nameForm'); + nodes.nameFormDisplay = document.querySelector('#nameForm span'); + nodes.nameFormInput = document.querySelector('#nameForm input'); + nodes.actionSwitch = document.getElementById('actionSwitch'); + nodes.actionOpen = document.getElementById('actionOpen'); + nodes.actionEdit = document.getElementById('actionEdit'); + nodes.actionExport = document.getElementById('actionExport'); + nodes.actionBackup = document.getElementById('actionBackup'); + nodes.actionDelete = document.getElementById('actionDelete'); + nodes.actionImport = document.getElementById('actionImport'); + nodes.banner = document.getElementById('banner'); + nodes.modalBlocker = document.querySelector('.blocker'); + nodes.modalContainer = document.querySelector('.modal'); + nodes.modalInput = document.getElementById('importTextArea'); + nodes.modalButton = document.getElementById('importBtn'); + + nodes.home.setAttribute('href', chrome.runtime.getURL('spaces.html')); + + // initialise event listeners for static elements + addEventListeners(); + + // render side nav + updateSpacesList(); + + // render main content + updateSpaceDetail(); + }; +} + +// Module-level helper functions. + +/** + * Extracts the original URL from a Great Suspender extension suspended tab URL. + * Great Suspender URLs have the format: chrome-extension://id/suspended.html?uri=originalUrl + * + * @param {string} url - The URL to normalize (should be a string) + * @returns {string} The original URL if it's a suspended URL, otherwise returns the input unchanged + * + * @example + * normaliseTabUrl('chrome-extension://abc/suspended.html?uri=https://example.com') + * // returns: 'https://example.com' + * + * normaliseTabUrl('https://example.com') + * // returns: 'https://example.com' + */ +function normaliseTabUrl(url) { + let normalisedUrl = url; + if (url.indexOf('suspended.html') > 0 && url.indexOf('uri=') > 0) { + normalisedUrl = url.substring(url.indexOf('uri=') + 4, url.length); + } + return normalisedUrl; +} + +// Export for testing +export { normaliseTabUrl }; diff --git a/tests/normaliseTabUrl.test.js b/tests/normaliseTabUrl.test.js new file mode 100644 index 0000000..e2aca63 --- /dev/null +++ b/tests/normaliseTabUrl.test.js @@ -0,0 +1,52 @@ +/** + * @jest-environment node + */ + +import { normaliseTabUrl } from '../js/spaces.js'; + +describe('normaliseTabUrl', () => { + describe('normal URLs (should remain unchanged)', () => { + test('should return regular URLs unchanged', () => { + expect(normaliseTabUrl('https://example.com')).toBe('https://example.com'); + expect(normaliseTabUrl('https://example.com#section')).toBe('https://example.com#section'); + expect(normaliseTabUrl('chrome://settings/')).toBe('chrome://settings/'); + }); + }); + + describe('Great Suspender URLs (should be normalized)', () => { + test('should extract original URL from suspended.html with uri parameter', () => { + const suspendedUrl = 'chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#uri=https://example.com'; + expect(normaliseTabUrl(suspendedUrl)).toBe('https://example.com'); + }); + + test('should handle complex URLs and different extension IDs', () => { + const complexUrl = 'chrome-extension://different-id/suspended.html?title=Test&uri=https://docs.example.com/api/v1/users?sort=name&page=5#results'; + expect(normaliseTabUrl(complexUrl)).toBe('https://docs.example.com/api/v1/users?sort=name&page=5#results'); + }); + }); + + describe('edge cases', () => { + test('should require both suspended.html and uri parameter', () => { + expect(normaliseTabUrl('chrome-extension://abc/suspended.html#title=Something')).toBe('chrome-extension://abc/suspended.html#title=Something'); + expect(normaliseTabUrl('https://example.com/page.html#uri=https://other.com')).toBe('https://example.com/page.html#uri=https://other.com'); + }); + + test('should require suspended.html not at beginning (indexOf > 0)', () => { + expect(normaliseTabUrl('suspended.html#uri=https://example.com')).toBe('suspended.html#uri=https://example.com'); + }); + + test('should extract from first uri parameter when multiple exist', () => { + const url = 'chrome-extension://abc/suspended.html#uri=https://first.com&other=param&uri=https://second.com'; + expect(normaliseTabUrl(url)).toBe('https://first.com&other=param&uri=https://second.com'); + }); + }); + + describe('invalid inputs', () => { + test('should handle edge case inputs', () => { + expect(normaliseTabUrl('')).toBe(''); + expect(() => normaliseTabUrl(null)).toThrow(); + expect(() => normaliseTabUrl(123)).toThrow('url.indexOf is not a function'); + expect(normaliseTabUrl([])).toEqual([]); // Arrays have indexOf + }); + }); +}); From 781fc01dcca12f035a132ca0a59241876f852a55 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 12:58:57 -0700 Subject: [PATCH 80/92] Move getHashVariable() to common.js and eliminate duplicate getVariableFromHash(). --- js/common.js | 34 ++++++++++++++- js/popup.js | 11 ++--- js/spaces.js | 32 ++------------ js/utils.js | 24 ----------- tests/getHashVariable.test.js | 78 +++++++++++++++++------------------ 5 files changed, 82 insertions(+), 97 deletions(-) diff --git a/js/common.js b/js/common.js index 3b7af57..a94e5e8 100644 --- a/js/common.js +++ b/js/common.js @@ -21,4 +21,36 @@ * @property {boolean} isOpen The session is currently open in a window. */ -export {} +/** + * Extracts a parameter value from a URL's hash fragment. + * @param {string} key - The parameter name to extract + * @param {string} urlStr - The URL string to parse + * @returns {string|false} The parameter value, or false if not found + * + * @example + * getHashVariable('id', 'https://example.com#id=123&name=test') + * // returns: '123' + */ +export function getHashVariable(key, urlStr) { + const valuesByKey = {}; + const keyPairRegEx = /^(.+)=(.+)/; + + if (!urlStr || urlStr.length === 0 || urlStr.indexOf('#') === -1) { + return false; + } + + // extract hash component from url + const hashStr = urlStr.replace(/^[^#]+#+(.*)/, '$1'); + if (hashStr.length === 0) { + return false; + } + + hashStr.split('&').forEach(keyPair => { + if (keyPair && keyPair.match(keyPairRegEx)) { + valuesByKey[ + keyPair.replace(keyPairRegEx, '$1') + ] = keyPair.replace(keyPairRegEx, '$2'); + } + }); + return valuesByKey[key] || false; +} diff --git a/js/popup.js b/js/popup.js index f859b65..6290a81 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,5 +1,6 @@ /* global chrome spacesRenderer */ +import { getHashVariable } from './common.js'; import { spacesRenderer } from './spacesRenderer.js'; import { utils } from './utils.js'; @@ -18,19 +19,19 @@ let globalSessionName; */ document.addEventListener('DOMContentLoaded', async () => { - const url = utils.getHashVariable('url', window.location.href); + const url = getHashVariable('url', window.location.href); globalUrl = url !== '' ? decodeURIComponent(url) : false; const currentWindow = await chrome.windows.getCurrent({ populate: true }); const windowId = currentWindow.id; globalWindowId = windowId !== '' ? windowId : false; - globalTabId = utils.getHashVariable('tabId', window.location.href); - const sessionName = utils.getHashVariable( + globalTabId = getHashVariable('tabId', window.location.href); + const sessionName = getHashVariable( 'sessionName', window.location.href ); globalSessionName = sessionName && sessionName !== 'false' ? sessionName : false; - const action = utils.getHashVariable('action', window.location.href); + const action = getHashVariable('action', window.location.href); const requestSpacePromise = globalWindowId ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) @@ -94,7 +95,7 @@ function renderCommon() { } function handleCloseAction() { - const opener = utils.getHashVariable('opener', window.location.href); + const opener = getHashVariable('opener', window.location.href); if (opener && opener === 'bg') { chrome.runtime.sendMessage({ action: 'requestClose', diff --git a/js/spaces.js b/js/spaces.js index 014f235..0a5c401 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -1,5 +1,6 @@ /* global chrome */ +import { getHashVariable } from './common.js'; import { utils } from './utils.js'; const UNSAVED_SESSION_NAME = 'Unnamed window'; @@ -621,31 +622,6 @@ function reroute(sessionId, windowId, forceRerender) { } } -function getVariableFromHash(key) { - if (window.location.hash.length > 0) { - const hash = window.location.hash.substr( - 1, - window.location.hash.length - ); - const pairs = hash.split('&'); - - let matchedVal; - const match = pairs.some(curPair => { - const [curKey, curVal] = curPair.split('='); - if (curKey === key) { - matchedVal = curVal; - return true; - } - return false; - }); - - if (match) { - return matchedVal; - } - } - return false; -} - async function updateSpacesList(spaces) { // if spaces passed in then re-render immediately if (spaces) { @@ -662,9 +638,9 @@ async function updateSpacesList(spaces) { } async function updateSpaceDetail(useCachedSpace) { - const sessionId = getVariableFromHash('sessionId'); - const windowId = getVariableFromHash('windowId'); - const editMode = getVariableFromHash('editMode'); + const sessionId = getHashVariable('sessionId', window.location.href); + const windowId = getHashVariable('windowId', window.location.href); + const editMode = getHashVariable('editMode', window.location.href); // use cached currently selected space if (useCachedSpace) { diff --git a/js/utils.js b/js/utils.js index 58b2e40..790e3ea 100644 --- a/js/utils.js +++ b/js/utils.js @@ -35,28 +35,4 @@ export var utils = { `A session with the name '${sessionName}' already exists. Do you want to overwrite it?` ); }, - - getHashVariable(key, urlStr) { - const valuesByKey = {}; - const keyPairRegEx = /^(.+)=(.+)/; - - if (!urlStr || urlStr.length === 0 || urlStr.indexOf('#') === -1) { - return false; - } - - // extract hash component from url - const hashStr = urlStr.replace(/^[^#]+#+(.*)/, '$1'); - if (hashStr.length === 0) { - return false; - } - - hashStr.split('&').forEach(keyPair => { - if (keyPair && keyPair.match(keyPairRegEx)) { - valuesByKey[ - keyPair.replace(keyPairRegEx, '$1') - ] = keyPair.replace(keyPairRegEx, '$2'); - } - }); - return valuesByKey[key] || false; - }, }; diff --git a/tests/getHashVariable.test.js b/tests/getHashVariable.test.js index 7cbb67a..6536c91 100644 --- a/tests/getHashVariable.test.js +++ b/tests/getHashVariable.test.js @@ -2,102 +2,102 @@ * @jest-environment node */ -import { utils } from '../js/utils.js'; +import { getHashVariable } from '../js/common.js'; -describe('utils.getHashVariable', () => { +describe('getHashVariable', () => { describe('basic functionality', () => { test('should return false for invalid inputs', () => { - expect(utils.getHashVariable('key', null)).toBe(false); - expect(utils.getHashVariable('key', undefined)).toBe(false); - expect(utils.getHashVariable('key', '')).toBe(false); + expect(getHashVariable('key', null)).toBe(false); + expect(getHashVariable('key', undefined)).toBe(false); + expect(getHashVariable('key', '')).toBe(false); }); test('should return false for URL without hash', () => { - expect(utils.getHashVariable('key', 'https://example.com')).toBe(false); + expect(getHashVariable('key', 'https://example.com')).toBe(false); }); test('should return false for URL with empty hash', () => { - expect(utils.getHashVariable('key', 'https://example.com#')).toBe(false); + expect(getHashVariable('key', 'https://example.com#')).toBe(false); }); }); describe('single key-value pairs', () => { test('should extract single key-value pair', () => { const url = 'https://example.com#key=value'; - expect(utils.getHashVariable('key', url)).toBe('value'); + expect(getHashVariable('key', url)).toBe('value'); }); test('should return false for non-existent key', () => { const url = 'https://example.com#key=value'; - expect(utils.getHashVariable('missing', url)).toBe(false); + expect(getHashVariable('missing', url)).toBe(false); }); test('should handle numeric values', () => { const url = 'https://example.com#id=123'; - expect(utils.getHashVariable('id', url)).toBe('123'); + expect(getHashVariable('id', url)).toBe('123'); }); test('should return false for empty values', () => { const url = 'https://example.com#key='; // The regex /^(.+)=(.+)/ requires at least one character after = - expect(utils.getHashVariable('key', url)).toBe(false); + expect(getHashVariable('key', url)).toBe(false); }); }); describe('multiple key-value pairs', () => { test('should extract keys from any position in multiple pairs', () => { const url = 'https://example.com#first=value1&second=value2&third=value3'; - expect(utils.getHashVariable('first', url)).toBe('value1'); // first position - expect(utils.getHashVariable('second', url)).toBe('value2'); // middle position - expect(utils.getHashVariable('third', url)).toBe('value3'); // last position + expect(getHashVariable('first', url)).toBe('value1'); // first position + expect(getHashVariable('second', url)).toBe('value2'); // middle position + expect(getHashVariable('third', url)).toBe('value3'); // last position }); test('should return false for non-existent key in multiple pairs', () => { const url = 'https://example.com#first=value1&second=value2&third=value3'; - expect(utils.getHashVariable('fourth', url)).toBe(false); + expect(getHashVariable('fourth', url)).toBe(false); }); }); describe('special characters and encoding', () => { test('should handle values with special characters', () => { const url = 'https://example.com#message=hello%20world'; - expect(utils.getHashVariable('message', url)).toBe('hello%20world'); + expect(getHashVariable('message', url)).toBe('hello%20world'); }); test('should handle keys with special characters', () => { const url = 'https://example.com#special-key=value'; - expect(utils.getHashVariable('special-key', url)).toBe('value'); + expect(getHashVariable('special-key', url)).toBe('value'); }); test('should return false for values with equals signs', () => { // The regex /^(.+)=(.+)/ doesn't handle multiple = signs properly const url = 'https://example.com#equation=x=y'; - expect(utils.getHashVariable('equation', url)).toBe(false); + expect(getHashVariable('equation', url)).toBe(false); }); test('should handle values with ampersands in query parameters before hash', () => { const url = 'https://example.com?param1=val1¶m2=val2#key=value'; - expect(utils.getHashVariable('key', url)).toBe('value'); + expect(getHashVariable('key', url)).toBe('value'); }); }); describe('malformed hash fragments', () => { test('should ignore key-value pairs without equals sign', () => { const url = 'https://example.com#validkey=validvalue&invalidpair&anotherkey=anothervalue'; - expect(utils.getHashVariable('validkey', url)).toBe('validvalue'); - expect(utils.getHashVariable('anotherkey', url)).toBe('anothervalue'); - expect(utils.getHashVariable('invalidpair', url)).toBe(false); + expect(getHashVariable('validkey', url)).toBe('validvalue'); + expect(getHashVariable('anotherkey', url)).toBe('anothervalue'); + expect(getHashVariable('invalidpair', url)).toBe(false); }); test('should ignore empty key-value pairs', () => { const url = 'https://example.com#key=value&&anotherkey=anothervalue'; - expect(utils.getHashVariable('key', url)).toBe('value'); - expect(utils.getHashVariable('anotherkey', url)).toBe('anothervalue'); + expect(getHashVariable('key', url)).toBe('value'); + expect(getHashVariable('anotherkey', url)).toBe('anothervalue'); }); test('should handle hash with only ampersands', () => { const url = 'https://example.com#&&&'; - expect(utils.getHashVariable('key', url)).toBe(false); + expect(getHashVariable('key', url)).toBe(false); }); }); @@ -105,59 +105,59 @@ describe('utils.getHashVariable', () => { test('should include everything after first hash including additional hash symbols', () => { const url = 'https://example.com#key=value#extra'; // The regex extracts everything after the first # symbol - expect(utils.getHashVariable('key', url)).toBe('value#extra'); + expect(getHashVariable('key', url)).toBe('value#extra'); }); test('should be case-sensitive for keys', () => { const url = 'https://example.com#Key=value'; - expect(utils.getHashVariable('key', url)).toBe(false); - expect(utils.getHashVariable('Key', url)).toBe('value'); + expect(getHashVariable('key', url)).toBe(false); + expect(getHashVariable('Key', url)).toBe('value'); }); test('should handle duplicate keys (last one wins behavior)', () => { const url = 'https://example.com#key=first&key=second'; // Based on the implementation, the last key should overwrite the first - expect(utils.getHashVariable('key', url)).toBe('second'); + expect(getHashVariable('key', url)).toBe('second'); }); test('should return false for hash at the beginning of string', () => { const url = '#key=value'; // The regex /^[^#]+#+(.*)/ expects at least one non-# character before # - expect(utils.getHashVariable('key', url)).toBe(false); + expect(getHashVariable('key', url)).toBe(false); }); test('should handle very long values', () => { const longValue = 'a'.repeat(1000); const url = `https://example.com#data=${longValue}`; - expect(utils.getHashVariable('data', url)).toBe(longValue); + expect(getHashVariable('data', url)).toBe(longValue); }); }); describe('regex behavior documentation', () => { test('should extract hash correctly with complex URLs', () => { const complexUrl = 'https://user:pass@example.com:8080/path?query=value&other=test#key=hashvalue&second=pair'; - expect(utils.getHashVariable('key', complexUrl)).toBe('hashvalue'); - expect(utils.getHashVariable('second', complexUrl)).toBe('pair'); + expect(getHashVariable('key', complexUrl)).toBe('hashvalue'); + expect(getHashVariable('second', complexUrl)).toBe('pair'); }); }); describe('real-world URL scenarios', () => { test('should handle Chrome extension URLs', () => { const url = 'chrome-extension://abcdef123456/spaces.html#sessionId=123&editMode=true'; - expect(utils.getHashVariable('sessionId', url)).toBe('123'); - expect(utils.getHashVariable('editMode', url)).toBe('true'); + expect(getHashVariable('sessionId', url)).toBe('123'); + expect(getHashVariable('editMode', url)).toBe('true'); }); test('should handle file URLs', () => { const url = 'file:///path/to/file.html#section=intro&version=1.0'; - expect(utils.getHashVariable('section', url)).toBe('intro'); - expect(utils.getHashVariable('version', url)).toBe('1.0'); + expect(getHashVariable('section', url)).toBe('intro'); + expect(getHashVariable('version', url)).toBe('1.0'); }); test('should handle URLs with ports', () => { const url = 'http://localhost:3000/app#tab=settings&debug=true'; - expect(utils.getHashVariable('tab', url)).toBe('settings'); - expect(utils.getHashVariable('debug', url)).toBe('true'); + expect(getHashVariable('tab', url)).toBe('settings'); + expect(getHashVariable('debug', url)).toBe('true'); }); }); }); From 66ab4595587819c6216d8c4d8b981ab46d88a306 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 14:20:10 -0700 Subject: [PATCH 81/92] Fix issue #2 by ensuring temporary sessions are returned from the service worker. Also fix some issues with Service Worker async initialization. --- js/background/background.js | 5 ++- js/background/spacesService.js | 74 +++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index a30f3ed..934eaaa 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -815,8 +815,9 @@ async function requestSpaceFromSessionId(sessionId) { * @returns {Promise} Promise that resolves to an array of Space objects */ async function requestAllSpaces() { - const sessions = await dbService.fetchAllSessions(); - const allSpaces = sessions + // Get all sessions from spacesService (includes both saved and temporary open window sessions) + const allSessions = await spacesService.getAllSessions(); + const allSpaces = allSessions .map(session => { return { sessionId: session.id, ...session }; }) diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 7d62c3e..8e0abf8 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -13,10 +13,18 @@ const debug = false; const noop = () => {}; class SpacesService { + /** + * Array containing all sessions - both saved sessions from database and temporary sessions for open windows. + * Saved sessions have an `id` property, while temporary sessions have `id: false` and represent + * open windows that don't match any saved session. + * @type {Array} + * @private + */ + sessions = []; + constructor() { this.tabHistoryUrlMap = {}; this.closedWindowIds = {}; - this.sessions = []; this.sessionUpdateTimers = {}; this.historyQueue = []; this.eventQueueCount = 0; @@ -137,7 +145,7 @@ class SpacesService { // First, check if there's already a session with this windowId (service worker reactivation case) let existingSession = null; try { - existingSession = await dbService.fetchSessionByWindowId(curWindow.id); + existingSession = await this._getSessionByWindowIdInternal(curWindow.id); } catch (error) { console.error('Error fetching session by windowId:', error); } @@ -163,7 +171,7 @@ class SpacesService { } const sessionHash = generateSessionHash(curWindow.tabs); - const temporarySession = await dbService.fetchSessionByWindowId( + const temporarySession = await this._getSessionByWindowIdInternal( curWindow.id ); @@ -268,6 +276,50 @@ class SpacesService { chrome.storage.local.set({'spacesVersion': JSON.stringify(newVersion)}); } + /** + * Get all sessions (includes both saved sessions and temporary open window sessions) + * @returns {Promise} Promise that resolves to a shallow copy of all sessions + */ + async getAllSessions() { + await this.ensureInitialized(); + return [...(this.sessions || [])]; + } + + /** + * Find a session by windowId, checking both in-memory sessions and database + * @param {number} windowId - The window ID to search for + * @returns {Session|null} The session object if found, null otherwise + */ + async getSessionByWindowId(windowId) { + await this.ensureInitialized(); + return await this._getSessionByWindowIdInternal(windowId); + } + + /** + * Internal method to find a session by windowId without ensuring initialization + * Used during initialization to avoid circular dependencies + * @private + * @param {number} windowId - The window ID to search for + * @returns {Promise} The session object if found, null otherwise + */ + async _getSessionByWindowIdInternal(windowId) { + // First check in-memory sessions (includes temporary sessions) + const memorySession = this.sessions.find(session => session.windowId === windowId); + if (memorySession) { + return memorySession; + } + + // If not found in memory, check database (for saved sessions) + // During initialization, avoid potential circular dependencies + if (this.initialized) { + const dbSession = await dbService.fetchSessionByWindowId(windowId); + return dbSession || null; + } + + // During initialization, only check what's already loaded in memory + return null; + } + // event listener functions for window and tab events // (events are received and screened first in background.js) // ----------------------------------------------------------------------------------------- @@ -396,7 +448,7 @@ class SpacesService { clearTimeout(this.sessionUpdateTimers[windowId]); } - const session = await dbService.fetchSessionByWindowId(windowId); + const session = await this.getSessionByWindowId(windowId); if (session) { // if this is a saved session then just remove the windowId reference if (session.id) { @@ -431,7 +483,7 @@ class SpacesService { return; } - const session = await dbService.fetchSessionByWindowId(windowId); + const session = await this.getSessionByWindowId(windowId); if (session) { session.lastAccess = new Date(); } @@ -522,7 +574,7 @@ class SpacesService { } // if window is associated with an open session then update session - const session = await dbService.fetchSessionByWindowId(windowId); + const session = await this.getSessionByWindowId(windowId); if (session) { if (debug) { @@ -572,10 +624,10 @@ class SpacesService { } } - // if no session found, it must be a new window. - // if session found without session.id then it must be a temporary session - // check for sessionMatch - if (!session || !session.id) { + // if no session found, it must be a new window - check for sessionMatch + // Note: if session found without session.id, it's a temporary session and we should NOT + // call checkForSessionMatch as that would create duplicate temporary sessions + if (!session) { if (debug) { // eslint-disable-next-line no-console console.log('session check triggered'); @@ -734,7 +786,7 @@ class SpacesService { // check for a temporary session with this windowId if (windowId) { - session = await dbService.fetchSessionByWindowId(windowId); + session = await this.getSessionByWindowId(windowId); } // if no temporary session found with this windowId, then create one From cc44644439b3191351500a0fdfaeb16d8c605970 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 15:57:50 -0700 Subject: [PATCH 82/92] Fix issue #5: Space rename prompts stop infinitely asking --- js/popup.js | 2 ++ js/spaces.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/js/popup.js b/js/popup.js index 6290a81..13e9250 100644 --- a/js/popup.js +++ b/js/popup.js @@ -200,6 +200,8 @@ async function handleNameSave() { const canOverwrite = await utils.checkSessionOverwrite(newName); if (!canOverwrite) { + inputEl.value = globalCurrentSpace.name || UNSAVED_SESSION; + inputEl.blur(); return; } diff --git a/js/spaces.js b/js/spaces.js index 0a5c401..e5a1b61 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -310,6 +310,8 @@ async function handleNameSave() { const canOverwrite = await utils.checkSessionOverwrite(newName); if (!canOverwrite) { + updateNameForm(globalSelectedSpace); + toggleNameEditMode(false); return; } From 4202655fd2b8aef3223ea4b03c2998ffa6c55c46 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 19:04:38 -0700 Subject: [PATCH 83/92] Fix issue #6: Remove duplicate "Unnamed window" in the "open spaces" list in Spaces window. --- js/background/spacesService.js | 156 ++++++++++++++++++++++++----- tests/addSessionSafely.test.js | 140 ++++++++++++++++++++++++++ tests/getSessionByWindowId.test.js | 60 +++++++++++ 3 files changed, 332 insertions(+), 24 deletions(-) create mode 100644 tests/addSessionSafely.test.js create mode 100644 tests/getSessionByWindowId.test.js diff --git a/js/background/spacesService.js b/js/background/spacesService.js index 8e0abf8..a7bfd9d 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -77,7 +77,7 @@ class SpacesService { // then try to match current open windows with saved sessions for (const curWindow of windows) { if (!filterInternalWindows(curWindow)) { - await this.checkForSessionMatchDuringInit(curWindow); + await this._checkForSessionMatchDuringInit(curWindow); } } @@ -137,7 +137,16 @@ class SpacesService { } } - async checkForSessionMatchDuringInit(curWindow) { + /** + * Checks for session matches during initialization, handling both service worker reactivation + * and Chrome restart scenarios. First checks for existing sessions by windowId, then falls + * back to hash matching if none found. + * + * @private + * @param {chrome.windows.Window} curWindow - Chrome window object with tabs array + * @returns {Promise} Resolves when initialization matching completes + */ + async _checkForSessionMatchDuringInit(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; } @@ -162,20 +171,41 @@ class SpacesService { } // If no existing session, fall back to hash matching (Chrome restart case) - await this.checkForSessionMatch(curWindow); + await this._ensureWindowHasSession(curWindow); } - async checkForSessionMatch(curWindow) { + /** + * Ensures a window has an associated session using multiple strategies: + * 1. Checks for existing session by windowId (returns early if found) + * 2. Attempts hash-based matching with saved sessions (links if match found) + * 3. Creates temporary session as fallback (if no existing session or match) + * + * @private + * @param {chrome.windows.Window} curWindow - Chrome window object with tabs array + * @returns {Promise} Resolves when session association is complete + */ + async _ensureWindowHasSession(curWindow) { if (!curWindow.tabs || curWindow.tabs.length === 0) { return; } + // Double-check that a session doesn't already exist for this window + // This is an additional safety check to prevent race conditions + const existingSession = this.sessions.find(session => session.windowId === curWindow.id); + if (existingSession) { + if (debug) { + // eslint-disable-next-line no-console + console.log( + `ensureWindowHasSession: Session already exists for window ${curWindow.id}, skipping creation` + ); + } + return; + } + + // Generate hash from current window's tabs to find matching saved sessions const sessionHash = generateSessionHash(curWindow.tabs); - const temporarySession = await this._getSessionByWindowIdInternal( - curWindow.id - ); - // Find matching session by hash (closedOnly = true) + // Find matching session by hash (closedOnly = true - sessions with no windowId) let matchingSession = false; try { const sessions = await dbService.fetchAllSessions(); @@ -197,10 +227,7 @@ class SpacesService { } this.matchSessionToWindow(matchingSession, curWindow); - } - - // if no match found and this window does not already have a temporary session - if (!matchingSession && !temporarySession) { + } else { if (debug) { // eslint-disable-next-line no-console console.log( @@ -209,7 +236,7 @@ class SpacesService { } // create a new temporary session for this window (with no sessionId or name) - this.createTemporaryUnmatchedSession(curWindow); + this._createTemporaryUnmatchedSession(curWindow); } } @@ -239,7 +266,62 @@ class SpacesService { } } - async createTemporaryUnmatchedSession(curWindow) { + /** + * Safely adds a session to this.sessions array, preventing duplicates. + * For saved sessions (with id), prevents duplicates by id. + * For any session with windowId, prevents duplicates by windowId. + * + * @private + * @param {Session} newSession - The session to add + * @returns {boolean} True if session was added, false if duplicate was prevented + */ + _addSessionSafely(newSession) { + // For saved sessions (with id), check for ID duplicates + if (newSession.id) { + const existingSession = this.sessions.find(session => session.id === newSession.id); + if (existingSession) { + console.error( + `_addSessionSafely: Attempted to add duplicate session with id ${newSession.id}. This should not happen!` + ); + return false; + } + } + + // For any session with windowId, check for windowId duplicates + if (newSession.windowId) { + const existingSession = this.sessions.find(session => session.windowId === newSession.windowId); + if (existingSession) { + if (debug) { + // eslint-disable-next-line no-console + console.log( + `_addSessionSafely: Session already exists for window ${newSession.windowId}, skipping addition` + ); + } + return false; + } + } + + // Safe to add - no duplicate found + this.sessions.push(newSession); + return true; + } + + /** + * Creates a temporary session for an unmatched window that doesn't correspond to any saved session. + * Temporary sessions have `id: false` and represent open windows that haven't been saved as sessions yet. + * Uses centralized duplicate prevention to ensure no duplicate sessions are created for the same windowId. + * + * @private + * @param {chrome.windows.Window} curWindow - The Chrome window object to create a temporary session for + * @returns {boolean} True if the temporary session was successfully created, false if duplicate was prevented + * + * @example + * // Internal usage only + * const window = { id: 123, tabs: [{ url: 'https://example.com' }] }; + * const created = this._createTemporaryUnmatchedSession(window); + * console.log(created); // true if session created, false if duplicate prevented + */ + _createTemporaryUnmatchedSession(curWindow) { if (debug) { // eslint-disable-next-line no-console console.dir(this.sessions); @@ -251,7 +333,7 @@ class SpacesService { const sessionHash = generateSessionHash(curWindow.tabs); - this.sessions.push({ + const newSession = { id: false, windowId: curWindow.id, sessionHash, @@ -259,7 +341,10 @@ class SpacesService { tabs: curWindow.tabs, history: [], lastAccess: new Date(), - }); + }; + + // Use centralized method to prevent duplicates + return this._addSessionSafely(newSession); } // local storage getters/setters @@ -624,15 +709,15 @@ class SpacesService { } } - // if no session found, it must be a new window - check for sessionMatch + // if no session found, it must be a new window - ensure it has a session // Note: if session found without session.id, it's a temporary session and we should NOT - // call checkForSessionMatch as that would create duplicate temporary sessions + // call _ensureWindowHasSession as that would create duplicate temporary sessions if (!session) { if (debug) { // eslint-disable-next-line no-console console.log('session check triggered'); } - this.checkForSessionMatch(curWindow); + this._ensureWindowHasSession(curWindow); } return true; } @@ -767,12 +852,15 @@ class SpacesService { * If a temporary session exists for the given windowId, it will be converted to a permanent session. * Otherwise, a new session is created and added to the sessions cache. * + * IMPORTANT: This method only works with temporary sessions (id: false). It will reject any + * attempt to "create" a session that already has a saved ID to prevent data corruption. + * * @param {string} sessionName - The name for the new session * @param {Array} tabs - Array of tab objects containing URL and other tab properties * @param {number|false} windowId - The window ID to associate with this session, or false for no association * @returns {Promise} Promise that resolves to: * - Session object with id property if successfully created - * - null if session creation failed or no tabs were provided + * - null if session creation failed, no tabs were provided, or attempted on already saved session */ async saveNewSession(sessionName, tabs, windowId) { await this.ensureInitialized(); @@ -786,16 +874,36 @@ class SpacesService { // check for a temporary session with this windowId if (windowId) { - session = await this.getSessionByWindowId(windowId); + const existingSession = await this.getSessionByWindowId(windowId); + if (existingSession) { + // If it's a saved session, reject immediately to prevent data corruption + if (existingSession.id) { + console.error('Cannot create new session: window already has a saved session'); + return null; + } + // Only use the session if it's temporary (no id) + session = existingSession; + } } - // if no temporary session found with this windowId, then create one + // if no existing session found, create a new one if (!session) { session = { windowId, history: [], }; - this.sessions.push(session); + // Use centralized method to prevent duplicates (protects against race conditions) + const wasAdded = this._addSessionSafely(session); + if (!wasAdded) { + // Race condition: another async operation created a session for this windowId + // Retrieve the session that was created by the other operation + const raceConditionSession = await this.getSessionByWindowId(windowId); + if (!raceConditionSession) { + console.error('Race condition detected but failed to retrieve the competing session'); + return null; + } + session = raceConditionSession; + } } // update temporary session details @@ -804,7 +912,7 @@ class SpacesService { session.tabs = tabs; session.lastAccess = new Date(); - // save session to db + // save session to db - this should only be called on temporary sessions (id: false) try { const savedSession = await dbService.createSession(session); if (savedSession) { diff --git a/tests/addSessionSafely.test.js b/tests/addSessionSafely.test.js new file mode 100644 index 0000000..034aad8 --- /dev/null +++ b/tests/addSessionSafely.test.js @@ -0,0 +1,140 @@ +import { spacesService } from '../js/background/spacesService.js'; + +describe('_addSessionSafely', () => { + beforeEach(() => { + // Reset sessions array before each test + spacesService.sessions = []; + }); + + test('adds new temporary session successfully', () => { + const newSession = { + id: false, + windowId: 100, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const result = spacesService._addSessionSafely(newSession); + + expect(result).toBe(true); + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions[0]).toEqual(newSession); + }); + + test('prevents duplicate temporary sessions by windowId', () => { + const existingSession = { + id: false, + windowId: 100, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const duplicateSession = { + id: false, + windowId: 100, + name: 'Different name', + tabs: [{ url: 'https://different.com' }] + }; + + // Add first session + spacesService.sessions.push(existingSession); + + // Try to add duplicate + const result = spacesService._addSessionSafely(duplicateSession); + + expect(result).toBe(false); + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions[0]).toEqual(existingSession); + }); + + test('allows multiple temporary sessions with different windowIds', () => { + const session1 = { + id: false, + windowId: 100, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const session2 = { + id: false, + windowId: 200, + name: false, + tabs: [{ url: 'https://different.com' }] + }; + + const result1 = spacesService._addSessionSafely(session1); + const result2 = spacesService._addSessionSafely(session2); + + expect(result1).toBe(true); + expect(result2).toBe(true); + expect(spacesService.sessions).toHaveLength(2); + }); + + test('prevents duplicate sessions by windowId regardless of session type', () => { + const existingSession = { + id: false, + windowId: 100, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const savedSession = { + id: 1, + windowId: 100, // Same windowId as temporary session + name: 'Saved Session', + tabs: [{ url: 'https://saved.com' }] + }; + + spacesService.sessions.push(existingSession); + const result = spacesService._addSessionSafely(savedSession); + + expect(result).toBe(false); + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions[0]).toEqual(existingSession); + }); + + test('allows saved sessions with different windowIds', () => { + const existingSession = { + id: false, + windowId: 100, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const savedSession = { + id: 1, + windowId: 200, // Different windowId + name: 'Saved Session', + tabs: [{ url: 'https://saved.com' }] + }; + + spacesService.sessions.push(existingSession); + const result = spacesService._addSessionSafely(savedSession); + + expect(result).toBe(true); + expect(spacesService.sessions).toHaveLength(2); + }); + + test('allows temporary sessions without windowId', () => { + const session1 = { + id: false, + windowId: false, + name: false, + tabs: [{ url: 'https://example.com' }] + }; + + const session2 = { + id: false, + windowId: false, + name: false, + tabs: [{ url: 'https://different.com' }] + }; + + const result1 = spacesService._addSessionSafely(session1); + const result2 = spacesService._addSessionSafely(session2); + + expect(result1).toBe(true); + expect(result2).toBe(true); + expect(spacesService.sessions).toHaveLength(2); + }); +}); diff --git a/tests/getSessionByWindowId.test.js b/tests/getSessionByWindowId.test.js new file mode 100644 index 0000000..32ec3bc --- /dev/null +++ b/tests/getSessionByWindowId.test.js @@ -0,0 +1,60 @@ +import { spacesService } from '../js/background/spacesService.js'; +import { dbService } from '../js/background/dbService.js'; + +describe('getSessionByWindowId - session retrieval logic', () => { + let originalFetchSessionByWindowId; + + beforeEach(() => { + // Reset sessions array + spacesService.sessions = []; + spacesService.initialized = true; + + // Mock database method to prevent indexedDB errors in tests + originalFetchSessionByWindowId = dbService.fetchSessionByWindowId; + dbService.fetchSessionByWindowId = async () => null; + }); + + afterEach(() => { + // Restore original method + if (originalFetchSessionByWindowId) { + dbService.fetchSessionByWindowId = originalFetchSessionByWindowId; + } + }); + + test('retrieves sessions by windowId and distinguishes types', async () => { + const tempSession = { + id: false, // temporary + windowId: 100, + name: false, + tabs: [{ url: 'https://temp.com' }] + }; + + const savedSession = { + id: 123, // saved + windowId: 200, + name: 'Saved Session', + tabs: [{ url: 'https://saved.com' }] + }; + + spacesService.sessions.push(tempSession, savedSession); + + // Test retrieval and type identification + const foundTemp = await spacesService.getSessionByWindowId(100); + expect(foundTemp).toBe(tempSession); + expect(foundTemp.id).toBe(false); // temporary session + expect(typeof foundTemp.id).toBe('boolean'); + + const foundSaved = await spacesService.getSessionByWindowId(200); + expect(foundSaved).toBe(savedSession); + expect(foundSaved.id).toBe(123); // saved session + expect(typeof foundSaved.id).toBe('number'); + }); + + test('returns null when no session exists for windowId', async () => { + // No sessions exist + expect(spacesService.sessions).toHaveLength(0); + + const noSession = await spacesService.getSessionByWindowId(999); + expect(noSession).toBe(null); + }); +}); From f448aa1646d2652b6daa12b019275d6c0d8df307 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 20:08:29 -0700 Subject: [PATCH 84/92] Fix issue #3 by escaping all HTML and adding unit tests. Flatten utils.js and add jsdoc for utils.js and common.js --- js/common.js | 11 +++-- js/popup.js | 10 ++--- js/spaces.js | 12 +++--- js/spacesRenderer.js | 8 ++-- js/utils.js | 83 ++++++++++++++++++++++++-------------- test_escaping_demo.html | 44 ++++++++++++++++++++ tests/htmlEscaping.test.js | 25 ++++++++++++ 7 files changed, 144 insertions(+), 49 deletions(-) create mode 100644 test_escaping_demo.html create mode 100644 tests/htmlEscaping.test.js diff --git a/js/common.js b/js/common.js index a94e5e8..3b1a058 100644 --- a/js/common.js +++ b/js/common.js @@ -1,11 +1,14 @@ -/* - * common.js +/** + * @fileoverview Shared utilities and types for the Spaces Chrome extension. + * + * This module contains functions and type definitions that are used by both + * client-side code (popup, spaces window, etc.) and background scripts. + * Client-side only utilities should be placed in utils.js instead. + * * Licensed under the MIT License * Copyright (C) 2025 by the Contributors. */ -/** Common types shared between background and client code. */ - /** * @typedef Space * @property {number|false} sessionId The unique identifier for the session, or false if not saved. diff --git a/js/popup.js b/js/popup.js index 13e9250..d8d2c94 100644 --- a/js/popup.js +++ b/js/popup.js @@ -2,7 +2,7 @@ import { getHashVariable } from './common.js'; import { spacesRenderer } from './spacesRenderer.js'; -import { utils } from './utils.js'; +import { checkSessionOverwrite, escapeHtml } from './utils.js'; const UNSAVED_SESSION = '(unnamed window)'; const NO_HOTKEY = 'no hotkey set'; @@ -198,7 +198,7 @@ async function handleNameSave() { return; } - const canOverwrite = await utils.checkSessionOverwrite(newName); + const canOverwrite = await checkSessionOverwrite(newName); if (!canOverwrite) { inputEl.value = globalCurrentSpace.name || UNSAVED_SESSION; inputEl.blur(); @@ -318,7 +318,7 @@ async function renderMoveCard() { // update currentSpaceDiv // nodes.windowTitle.innerHTML = "Current space: " + (globalSessionName ? globalSessionName : 'unnamed'); - nodes.activeSpaceTitle.innerHTML = globalSessionName || '(unnamed)'; + nodes.activeSpaceTitle.innerHTML = escapeHtml(globalSessionName) || '(unnamed)'; // selectSpace(nodes.activeSpace); await updateTabDetails(); @@ -353,7 +353,7 @@ async function updateTabDetails() { }); if (tab) { - nodes.activeTabTitle.innerHTML = tab.title; + nodes.activeTabTitle.innerHTML = escapeHtml(tab.title); // try to get best favicon url path if ( @@ -385,7 +385,7 @@ async function updateTabDetails() { globalUrl.length ) : globalUrl; - nodes.activeTabTitle.innerHTML = cleanUrl; + nodes.activeTabTitle.innerHTML = escapeHtml(cleanUrl); nodes.activeTabFavicon.setAttribute('src', '/img/new.png'); nodes.moveInput.setAttribute('placeholder', 'Add tab to..'); diff --git a/js/spaces.js b/js/spaces.js index e5a1b61..5f6f957 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -1,7 +1,7 @@ /* global chrome */ import { getHashVariable } from './common.js'; -import { utils } from './utils.js'; +import { checkSessionOverwrite, escapeHtml } from './utils.js'; const UNSAVED_SESSION_NAME = 'Unnamed window'; const UNSAVED_SESSION = `${UNSAVED_SESSION_NAME}`; @@ -41,7 +41,7 @@ function renderSpaceListEl(space) { linkEl.setAttribute('href', hash); if (space.name) { - linkEl.innerHTML = space.name; + linkEl.innerHTML = escapeHtml(space.name); } else { linkEl.innerHTML = UNSAVED_SESSION; } @@ -86,7 +86,7 @@ function renderSpaceDetail(space, editMode) { function updateNameForm(space) { if (space && space.name) { nodes.nameFormInput.value = space.name; - nodes.nameFormDisplay.innerHTML = space.name; + nodes.nameFormDisplay.innerHTML = escapeHtml(space.name); } else { nodes.nameFormInput.value = ''; if (space) { @@ -163,7 +163,7 @@ function renderTabListEl(tab, space) { } faviconEl.setAttribute('src', faviconSrc); - linkEl.innerHTML = tab.title ? tab.title : tab.url; + linkEl.innerHTML = escapeHtml(tab.title ?? tab.url); linkEl.setAttribute('href', tab.url); linkEl.setAttribute('target', '_blank'); @@ -308,7 +308,7 @@ async function handleNameSave() { return; } - const canOverwrite = await utils.checkSessionOverwrite(newName); + const canOverwrite = await checkSessionOverwrite(newName); if (!canOverwrite) { updateNameForm(globalSelectedSpace); toggleNameEditMode(false); @@ -524,7 +524,7 @@ async function performSessionImport(urlList) { /** @returns {Promise} */ async function performRestoreFromBackup(spaces) { for (const space of spaces) { - const canOverwrite = await utils.checkSessionOverwrite(space.name); + const canOverwrite = await checkSessionOverwrite(space.name); if (!canOverwrite) { continue; } diff --git a/js/spacesRenderer.js b/js/spacesRenderer.js index 3c18c3c..d1757fe 100644 --- a/js/spacesRenderer.js +++ b/js/spacesRenderer.js @@ -1,3 +1,5 @@ +import { escapeHtml } from './utils.js'; + // eslint-disable-next-line no-var export const spacesRenderer = { nodes: {}, @@ -150,11 +152,11 @@ export const spacesRenderer = { if (!count) return ''; const firstTitle = space.tabs[0].title; if (count === 1) { - return `[${firstTitle}]`; + return `[${escapeHtml(firstTitle)}]`; } return firstTitle.length > 30 - ? `[${firstTitle.slice(0, 21)}…] +${count - 1} more` - : `[${firstTitle}] +${count - 1} more`; + ? `[${escapeHtml(firstTitle.slice(0, 21))}…] +${count - 1} more` + : `[${escapeHtml(firstTitle)}] +${count - 1} more`; }, getTabDetailsString(space) { diff --git a/js/utils.js b/js/utils.js index 790e3ea..2899241 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,38 +1,59 @@ +/** + * @fileoverview Client-side utility functions for the Spaces Chrome extension. + * + * This module contains utility functions that are only used by client-side code + * (popup, spaces window, content scripts, etc.). Functions that need to be shared + * between client-side and background scripts should be placed in common.js instead. + */ + /* global chrome */ /** @typedef {import('./common.js').SessionPresence} SessionPresence */ -// eslint-disable-next-line no-var, no-unused-vars -export var utils = { - /** - * Checks if a session with the given name can be overwritten by checking - * with the background script, alerting the user if the session is currently - * open, and confirming if the session already exists but is not open. - * @param {string} sessionName - * @returns {Promise} Returns true if the session can be safely - * overwritten. This happens if the session does not exist or if the - * user has confirmed overwriting. - */ - async checkSessionOverwrite(sessionName) { - /** @type {SessionPresence} */ - const sessionPresence = await chrome.runtime.sendMessage({ - action: 'requestSessionPresence', - sessionName, - }); +/** + * Checks if a session with the given name can be overwritten by checking + * with the background script, alerting the user if the session is currently + * open, and confirming if the session already exists but is not open. + * @param {string} sessionName + * @returns {Promise} Returns true if the session can be safely + * overwritten. This happens if the session does not exist or if the + * user has confirmed overwriting. + */ +export async function checkSessionOverwrite(sessionName) { + /** @type {SessionPresence} */ + const sessionPresence = await chrome.runtime.sendMessage({ + action: 'requestSessionPresence', + sessionName, + }); - if (!sessionPresence.exists) { - return true; - } + if (!sessionPresence.exists) { + return true; + } - if (sessionPresence.isOpen) { - // eslint-disable-next-line no-alert - alert( - `A session with the name '${sessionName}' is currently open and cannot be overwritten` - ); - return false; - } - return confirm( - `A session with the name '${sessionName}' already exists. Do you want to overwrite it?` + if (sessionPresence.isOpen) { + // eslint-disable-next-line no-alert + alert( + `A session with the name '${sessionName}' is currently open and cannot be overwritten` ); - }, -}; + return false; + } + return confirm( + `A session with the name '${sessionName}' already exists. Do you want to overwrite it?` + ); +} + +/** + * Escapes HTML characters to prevent XSS and HTML injection. + * @param {string} text - The text to escape + * @returns {string} The HTML-escaped text + */ +export function escapeHtml(text) { + if (!text) return text; + + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/test_escaping_demo.html b/test_escaping_demo.html new file mode 100644 index 0000000..5d0b3c8 --- /dev/null +++ b/test_escaping_demo.html @@ -0,0 +1,44 @@ + + + + HTML Escaping Bug Fix Demo + + +

HTML Escaping Bug Fix Demo

+ +
+

Before Fix (Vulnerable):

+
+ +

After Fix (Safe):

+
+
+ + + + diff --git a/tests/htmlEscaping.test.js b/tests/htmlEscaping.test.js new file mode 100644 index 0000000..acf2db4 --- /dev/null +++ b/tests/htmlEscaping.test.js @@ -0,0 +1,25 @@ +import { escapeHtml } from '../js/utils.js'; + +describe('HTML escaping bug fix', () => { + test('escapes HTML characters to prevent injection', () => { + // Core HTML characters + expect(escapeHtml(' element')).toBe('<input> element'); + expect(escapeHtml('AT&T')).toBe('AT&T'); + expect(escapeHtml('"quoted text"')).toBe('"quoted text"'); + expect(escapeHtml("'apostrophe'")).toBe(''apostrophe''); + + // Complex injection attempt + expect(escapeHtml('')) + .toBe('<script>alert("XSS")</script>'); + }); + + test('handles edge cases correctly', () => { + // Empty/null values + expect(escapeHtml('')).toBe(''); + expect(escapeHtml(null)).toBe(null); + expect(escapeHtml(undefined)).toBe(undefined); + + // Normal text unchanged + expect(escapeHtml('normal text')).toBe('normal text'); + }); +}); From 0e3b29eb85f8e391049d88cd2a0b764a6d722d39 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 20:49:37 -0700 Subject: [PATCH 85/92] Remove eslint and prettier (for now) --- .eslintrc.js | 19 - .prettierrc.js | 5 - package-lock.json | 5213 +++++++-------------------------------------- package.json | 12 +- 4 files changed, 796 insertions(+), 4453 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index ca0c73c..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - env: { - browser: true, - es6: true, - }, - extends: ['airbnb-base', 'plugin:prettier/recommended'], - globals: { - Atomics: 'readonly', - SharedArrayBuffer: 'readonly', - }, - parserOptions: { - ecmaVersion: 2018, - }, - ignorePatterns: ['db.js'], - rules: { - 'no-use-before-define': 0, - 'no-underscore-dangle': 0, - }, -}; diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 2b588d6..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - trailingComma: 'es5', - tabWidth: 4, - singleQuote: true, -}; diff --git a/package-lock.json b/package-lock.json index 3dfd00f..45304a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,17 +5,7 @@ "packages": { "": { "devDependencies": { - "eslint": "^6.7.1", - "eslint-config-airbnb-base": "^14.0.0", - "eslint-config-prettier": "^6.5.0", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-node": "^10.0.0", - "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", - "jest": "^29.7.0", - "prettier": "^1.19.1" + "jest": "^29.7.0" } }, "node_modules/@ampproject/remapping": { @@ -603,82 +593,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", @@ -727,105 +641,6 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -947,105 +762,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1133,176 +849,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1485,42 +1049,6 @@ "dev": true, "license": "MIT" }, - "node_modules/acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0" - } - }, - "node_modules/ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "node_modules/ansi-escapes": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", @@ -1545,26 +1073,29 @@ } }, "node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { @@ -1591,30 +1122,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= sha512-mRVEsI0s5MycUKtZtn8i5co54WKxL5gH3gAcCjtUbECNwdDL2gsBwjLqswM3c6fjcuWFQ9hoS4C+EhjxQmEyHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1637,136 +1144,60 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "color-convert": "^2.0.1" + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -1930,18 +1361,20 @@ "license": "CC-BY-4.0" }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/char-regex": { @@ -1954,13 +1387,6 @@ "node": ">=10" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1984,26 +1410,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= sha512-EJLbKSuvHTrVRynOXCYFTbQKZOFXWNe3/6DN1yrEH3TuuZT1x4dMQnCHnfCrBUUiGjO63enEIfaB17VaRl2d4A==", - "dev": true, - "license": "ISC" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2019,29 +1425,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2061,19 +1444,22 @@ "license": "MIT" }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, @@ -2084,23 +1470,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confusing-browser-globals": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", - "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2130,107 +1499,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/create-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/create-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">= 8" } }, "node_modules/debug": { @@ -2266,13 +1547,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= sha512-GtxAN4HvBachZzm4OnWqc45ESpUCMwkYcsjnsPs23FwJbsO+k4t0k9bQCgOmzIlpHO28+WPK/KRbRk0DDHuuDw==", - "dev": true, - "license": "MIT" - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2283,19 +1557,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2316,19 +1577,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.211", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", @@ -2366,2965 +1614,1049 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-abstract": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", - "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "dependencies": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", - "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, "engines": { - "node": ">=0.8.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/eslint": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.1.tgz", - "integrity": "sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "bser": "2.1.1" } }, - "node_modules/eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2" + "node": ">=8" } }, - "node_modules/eslint-config-prettier": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", - "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { - "get-stdin": "^6.0.0" - }, - "bin": { - "eslint-config-prettier-check": "bin/cli.js" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "peerDependencies": { - "eslint": ">=3.14.1" + "engines": { + "node": ">=8" } }, - "node_modules/eslint-config-standard": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", - "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=6.2.2", - "eslint-plugin-import": ">=2.18.0", - "eslint-plugin-node": ">=9.1.0", - "eslint-plugin-promise": ">=4.2.1", - "eslint-plugin-standard": ">=4.0.0" - } + "license": "ISC" }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "debug": "^2.6.9", - "resolve": "^1.5.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-import-resolver-node/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^2.6.8", - "pkg-dir": "^2.0.0" - }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/eslint-module-utils/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-es": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz", - "integrity": "sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-utils": "^1.4.2", - "regexpp": "^3.0.0" - }, "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=4.19.1" + "node": ">=8.0.0" } }, - "node_modules/eslint-plugin-es/node_modules/regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "array-includes": "^3.0.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", - "has": "^1.0.3", + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=4" + "node": "*" }, - "peerDependencies": { - "eslint": "2.x - 6.x" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, - "node_modules/eslint-plugin-node": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz", - "integrity": "sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "MIT", - "dependencies": { - "eslint-plugin-es": "^2.0.0", - "eslint-utils": "^1.4.2", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" + "node": ">=10.17.0" } }, - "node_modules/eslint-plugin-node/node_modules/ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, "engines": { - "node": ">= 4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", - "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "find-up": "^4.0.0" }, "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">= 5.0.0", - "prettier": ">= 1.13.0" + "node": ">=8" } }, - "node_modules/eslint-plugin-promise": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", - "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-plugin-standard": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", - "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=5.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^4.0.0" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^4.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0= sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= sha512-QfcgWpH8qn5qhNMg3wfXf2FD/rSA4TwNiDDthKqXe7v6oBW0YKWcnfwMAApgWq9Lh+Yu+fQWVhHPohlD/S6uoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", - "dev": true, - "license": "ISC" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o= sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inquirer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", - "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= sha512-P5rExV1phPi42ppoMWy7V63N3i173RY921l4JJ7zonMSxK+OWGPj76GD+cUKUb68l4vQXcJp2SsG+r/A4ABVzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= sha512-NECAi6wp6CgMesHuVUEK8JwjCvm/tvnn5pCbB42JOHp3mgUizN0nagXu4HEqQZBkieGEQ+jVcMKWqoVd6CDbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= sha512-WQgPrEkb1mPCWLSlLFuN1VziADSixANugwSkJfPRR73FNWIQQN+tR/t1zWfyES/Y9oag/XBtVsahFdfBku3Kyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o= sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.8.19" } }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } + "license": "ISC" }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "hasown": "^2.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6" } }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=0.12.0" } }, - "node_modules/jest-runner/node_modules/color-convert": { + "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "has-flag": "^4.0.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" } }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "color-convert": "^2.0.1" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=10" } }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "color-name": "~1.1.4" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot": { + "node_modules/jest-circus": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", + "@types/node": "*", "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", + "p-limit": "^3.1.0", "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "detect-newline": "^3.0.0" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util": { + "node_modules/jest-environment-node": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate": { + "node_modules/jest-mock": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=6" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher": { + "node_modules/jest-snapshot": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", + "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "emittery": "^0.13.1", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", - "string-length": "^4.0.1" + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=10" } }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -5343,16 +2675,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -5410,20 +2732,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5457,20 +2765,6 @@ "node": ">=6" } }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5478,45 +2772,6 @@ "dev": true, "license": "MIT" }, - "node_modules/load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -5530,13 +2785,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5618,9 +2866,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -5630,27 +2878,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5658,13 +2885,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5672,13 +2892,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5693,29 +2906,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5739,84 +2929,6 @@ "node": ">=8" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5830,45 +2942,17 @@ "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-limit": { @@ -5926,19 +3010,6 @@ "node": ">=6" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -5979,13 +3050,13 @@ } }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -5995,19 +3066,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6028,16 +3086,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw= sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -6048,127 +3096,6 @@ "node": ">= 6" } }, - "node_modules/pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c= sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -6197,16 +3124,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6221,16 +3138,6 @@ "node": ">= 6" } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -6255,118 +3162,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c= sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.5.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6421,86 +3216,15 @@ "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA= sha512-Fx+QT3fGtS0jk8OvKyKgAB2YHPsrmqBRcMeTC5AZ+lp4vzXKPPrFSY3iLdgvjA3HVBkIvJeM6J80LRjx8bQwhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-promise": "^2.1.0" - }, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/semver": { "version": "6.3.0", @@ -6513,26 +3237,26 @@ } }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/signal-exit": { @@ -6559,31 +3283,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6605,42 +3304,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6685,29 +3348,6 @@ "node": ">=10" } }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6723,17 +3363,7 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -6746,47 +3376,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -6821,16 +3410,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -6846,54 +3435,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/table/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6909,33 +3450,6 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6956,26 +3470,6 @@ "node": ">=8.0" } }, - "node_modules/tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6986,16 +3480,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -7034,23 +3518,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", - "dev": true, - "license": "MIT" - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7066,17 +3533,6 @@ "node": ">=10.12.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7088,26 +3544,19 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "license": "MIT", + "node-which": "bin/node-which" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/wrap-ansi": { @@ -7128,65 +3577,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7194,19 +3584,6 @@ "dev": true, "license": "ISC" }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", diff --git a/package.json b/package.json index 004b665..8bc8b42 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,6 @@ "testEnvironment": "node" }, "devDependencies": { - "eslint": "^6.7.1", - "eslint-config-airbnb-base": "^14.0.0", - "eslint-config-prettier": "^6.5.0", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-node": "^10.0.0", - "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", - "jest": "^29.7.0", - "prettier": "^1.19.1" + "jest": "^29.7.0" } } From 0a70dc669b480d1cb10a98fc0ea6db9e18ac06d0 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 20:49:46 -0700 Subject: [PATCH 86/92] Add starter changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6f05c60 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.1.4] - 2025-??-?? + +### Changes + +- Updated to support Chrome Extension Manifest V3. +- Updated all code to modern JavaScript and improved documentation. +- Fixed [issue #3](https://github.com/codedread/spaces/issues/3) by escaping + HTML for all extension content. +- Increased unit test coverage from 0% to 5.7%. From e5d4165b296a057228849da5fbea94a8385b0500 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 6 Sep 2025 22:51:53 -0700 Subject: [PATCH 87/92] Fix issue #4 and issue #7 by ensuring any database updates are carefully synced to the in-memory this.spaces array. Add unit test coverage. --- CHANGELOG.md | 2 +- js/background/background.js | 4 +- js/background/spacesService.js | 91 +++++++++-- tests/createSessionSync.test.js | 170 +++++++++++++++++++++ tests/helpers.js | 44 ++++++ tests/updateSessionSync.test.js | 262 ++++++++++++++++++++++++++++++++ 6 files changed, 554 insertions(+), 19 deletions(-) create mode 100644 tests/createSessionSync.test.js create mode 100644 tests/helpers.js create mode 100644 tests/updateSessionSync.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f05c60..3322000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,4 +10,4 @@ All notable changes to this project will be documented in this file. - Updated all code to modern JavaScript and improved documentation. - Fixed [issue #3](https://github.com/codedread/spaces/issues/3) by escaping HTML for all extension content. -- Increased unit test coverage from 0% to 5.7%. +- Increased unit test coverage from 0% to 6.99%. diff --git a/js/background/background.js b/js/background/background.js index 934eaaa..c77b82c 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -849,7 +849,7 @@ function spaceDateCompare(a, b) { return 0; } - async function handleLoadSession(sessionId, tabUrl) { +async function handleLoadSession(sessionId, tabUrl) { const session = await dbService.fetchSessionById(sessionId); // if space is already open, then give it focus @@ -876,7 +876,7 @@ function spaceDateCompare(a, b) { }); // force match this new window to the session - spacesService.matchSessionToWindow(session, newWindow); + await spacesService.matchSessionToWindow(session, newWindow); // after window has loaded try to pin any previously pinned tabs for (const curSessionTab of session.tabs) { diff --git a/js/background/spacesService.js b/js/background/spacesService.js index a7bfd9d..ff02273 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -98,8 +98,8 @@ class SpacesService { for (const session of sessions) { if (session.windowId) { session.windowId = false; - // Persist the cleared windowId to database - await dbService.updateSession(session); + // Persist the cleared windowId to database and update memory + await this._updateSessionSync(session); } } @@ -122,7 +122,7 @@ class SpacesService { session.sessionHash = generateSessionHash( session.tabs ); - await dbService.updateSession(session); + await this._updateSessionSync(session); } } @@ -248,8 +248,8 @@ class SpacesService { if (this.sessions[i].windowId === curWindow.id) { if (this.sessions[i].id) { this.sessions[i].windowId = false; - // Persist the cleared windowId to database - await dbService.updateSession(this.sessions[i]); + // Persist the cleared windowId to database with sync + await this._updateSessionSync(this.sessions[i]); } else { this.sessions.splice(i, 1); } @@ -260,9 +260,9 @@ class SpacesService { // eslint-disable-next-line no-param-reassign session.windowId = curWindow.id; - // Persist the new windowId association to database + // Persist the new windowId association to database with automatic sync if (session.id) { - await dbService.updateSession(session); + await this._updateSessionSync(session); } } @@ -538,8 +538,8 @@ class SpacesService { // if this is a saved session then just remove the windowId reference if (session.id) { session.windowId = false; - // Persist the cleared windowId to database - await dbService.updateSession(session); + // Persist the cleared windowId to database with sync + await this._updateSessionSync(session); // else if it is temporary session then remove the session from the cache } else { @@ -699,9 +699,7 @@ class SpacesService { // override session tabs with tabs from window session.tabs = curWindow.tabs; - session.sessionHash = generateSessionHash( - session.tabs - ); + session.sessionHash = generateSessionHash(session.tabs); // if it is a saved session then update db if (session.id) { @@ -839,7 +837,7 @@ class SpacesService { */ async saveExistingSession(session) { try { - const updatedSession = await dbService.updateSession(session); + const updatedSession = await this._updateSessionSync(session); return updatedSession || null; } catch (error) { console.error('Error saving existing session:', error); @@ -914,10 +912,8 @@ class SpacesService { // save session to db - this should only be called on temporary sessions (id: false) try { - const savedSession = await dbService.createSession(session); + const savedSession = await this._createSessionSync(session); if (savedSession) { - // update sessionId in cache - session.id = savedSession.id; return savedSession; } else { console.error('Failed to create session'); @@ -929,6 +925,69 @@ class SpacesService { } } + // ======================================== + // CENTRALIZED DATABASE OPERATIONS + // These methods handle both database operations AND memory synchronization + // ======================================== + + /** + * Creates a session in the database and ensures memory cache synchronization. + * @private + * @param {Session} session - Session object to create (must exist in this.sessions) + * @returns {Promise} The created session with ID, or null if failed + */ + async _createSessionSync(session) { + try { + const savedSession = await dbService.createSession(session); + if (savedSession) { + // Find and update the session in memory cache + const index = this.sessions.findIndex(s => s === session); + if (index !== -1) { + // Update the existing object in place to preserve references + // This is critical for UI components that hold references to session objects + Object.assign(this.sessions[index], savedSession); + return this.sessions[index]; + } else { + console.warn('Session not found in memory cache during create sync'); + return savedSession; + } + } + return null; + } catch (error) { + console.error('Error creating session with sync:', error); + return null; + } + } + + /** + * Updates a session in the database and ensures memory cache synchronization. + * @private + * @param {Session} session - Session object to update (must have valid id) + * @returns {Promise} The updated session, or null if failed + */ + async _updateSessionSync(session) { + try { + const updatedSession = await dbService.updateSession(session); + if (updatedSession) { + // Find and update the session in memory cache + const index = this.sessions.findIndex(s => s.id === session.id); + if (index !== -1) { + // Update the existing object in place to preserve references + // This is critical for UI components that hold references to session objects + Object.assign(this.sessions[index], updatedSession); + return this.sessions[index]; + } else { + console.warn('Session not found in memory cache during update sync'); + return updatedSession; + } + } + return null; + } catch (error) { + console.error('Error updating session with sync:', error); + return null; + } + } + /** * Deletes a session from the database and removes it from the cache. * diff --git a/tests/createSessionSync.test.js b/tests/createSessionSync.test.js new file mode 100644 index 0000000..33f9237 --- /dev/null +++ b/tests/createSessionSync.test.js @@ -0,0 +1,170 @@ +import { spacesService } from '../js/background/spacesService.js'; +import { dbService } from '../js/background/dbService.js'; +import { mockConsole, mockDbCreate } from './helpers.js'; + +describe('_createSessionSync - database creation with memory synchronization', () => { + let originalCreateSession; + const TEMP_SESSION = { id: false, windowId: 100, name: 'Test Session', tabs: [{ url: 'https://example.com' }], history: [] }; + const SAVED_SESSION = { id: 123, windowId: 100, name: 'Test Session', tabs: [{ url: 'https://example.com' }], history: [] }; + + beforeEach(() => { + // Reset sessions array and initialization state + spacesService.sessions = []; + spacesService.initialized = true; + + // Store original method + originalCreateSession = dbService.createSession; + }); + + afterEach(() => { + // Restore original method + if (originalCreateSession) { + dbService.createSession = originalCreateSession; + } + }); + + test('successfully creates session and updates memory cache in place', async () => { + const temporarySession = structuredClone(TEMP_SESSION); + const savedSession = { ...SAVED_SESSION, createdAt: new Date(), lastAccess: new Date() }; + + // Add session to memory first (simulating how saveNewSession works) + spacesService.sessions.push(temporarySession); + + // Mock successful database creation + let createSessionCalled = false; + let createSessionArg = null; + mockDbCreate(async (session) => { + createSessionCalled = true; + createSessionArg = session; + return savedSession; + }); + + const result = await spacesService._createSessionSync(temporarySession); + + expect(createSessionCalled).toBe(true); + expect(createSessionArg).toBe(temporarySession); + expect(result).toBe(spacesService.sessions[0]); // Returns the updated in-memory object + expect(result.id).toBe(123); // ID was assigned + expect(result.createdAt).toBeDefined(); // Database properties merged + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions).toContain(temporarySession); + + // Verify the original object was updated in place (reference preserved) + expect(spacesService.sessions).toContain(temporarySession); + expect(temporarySession.id).toBe(123); // Original object was updated + }); + + test('handles database creation failure gracefully', async () => { + const temporarySession = structuredClone(TEMP_SESSION); + + spacesService.sessions.push(temporarySession); + + // Mock database failure + mockDbCreate(null); + + const result = await spacesService._createSessionSync(temporarySession); + + expect(result).toBeNull(); + + // Memory cache should remain unchanged on failure + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions).toContain(temporarySession); + expect(spacesService.sessions[0].id).toBe(false); // Still temporary + }); + + test('handles database creation exception gracefully', async () => { + const temporarySession = structuredClone(TEMP_SESSION); + + spacesService.sessions.push(temporarySession); + + // Mock database exception + const dbError = new Error('Database connection failed'); + mockDbCreate(async () => { + throw dbError; + }); + + // Spy on console.error to verify error handling + const errorSpy = mockConsole('error'); + + const result = await spacesService._createSessionSync(temporarySession); + + expect(result).toBeNull(); + expect(errorSpy.called).toBe(true); + expect(errorSpy.args[0]).toBe('Error creating session with sync:'); + expect(errorSpy.args[1]).toBe(dbError); + + // Memory cache should remain unchanged on exception + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions).toContain(temporarySession); + expect(spacesService.sessions[0].id).toBe(false); + + errorSpy.restore(); + }); + + test('handles session not found in memory cache', async () => { + const temporarySession = structuredClone(TEMP_SESSION); + const savedSession = structuredClone(SAVED_SESSION); + + // Don't add session to memory cache (unusual edge case) + + // Mock successful database creation + mockDbCreate(savedSession); + + // Spy on console.warn to verify warning + const warnSpy = mockConsole('warn'); + + const result = await spacesService._createSessionSync(temporarySession); + + expect(result).toBe(savedSession); // Returns database result directly + expect(warnSpy.called).toBe(true); + expect(warnSpy.args[0]).toBe('Session not found in memory cache during create sync'); + expect(spacesService.sessions).toHaveLength(0); + expect(spacesService.sessions).toEqual([]); // Cache unchanged and empty + + warnSpy.restore(); + }); + + test('preserves object references for UI stability', async () => { + const temporarySession = structuredClone(TEMP_SESSION); + const savedSession = { ...SAVED_SESSION, createdAt: new Date() }; + + spacesService.sessions.push(temporarySession); + + // Store reference to original object + const originalRef = temporarySession; + const originalArrayRef = spacesService.sessions[0]; + + mockDbCreate(savedSession); + + const result = await spacesService._createSessionSync(temporarySession); + + // Verify that references are preserved (critical for UI) + expect(result).toBe(originalRef); + expect(spacesService.sessions).toContain(originalRef); + expect(spacesService.sessions).toContain(originalArrayRef); + + // But properties were updated + expect(originalRef.id).toBe(123); + expect(originalRef.createdAt).toBeDefined(); + }); + + test('handles multiple sessions in memory cache correctly', async () => { + const session1 = { id: false, windowId: 100, name: 'Session 1', tabs: [] }; + const session2 = { id: false, windowId: 200, name: 'Session 2', tabs: [] }; + const targetSession = { id: false, windowId: 300, name: 'Target', tabs: [] }; + + const savedSession = { id: 456, windowId: 300, name: 'Target', tabs: [] }; + + // Add multiple sessions + spacesService.sessions.push(session1, session2, targetSession); + + mockDbCreate(savedSession); + + const result = await spacesService._createSessionSync(targetSession); + + expect(result).toBe(targetSession); // Correct session updated + expect(spacesService.sessions).toHaveLength(3); + expect(spacesService.sessions).toEqual([session1, session2, targetSession]); // All sessions in correct order + expect(targetSession.id).toBe(456); // Properly updated + }); +}); diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100644 index 0000000..29f815f --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,44 @@ +/** + * Shared test helper functions + */ +import { dbService } from '../js/background/dbService.js'; + +/** + * Creates a mock for console methods (error, warn, log, etc.) + * @param {string} method - The console method to mock ('error', 'warn', 'log', etc.) + * @returns {Object} Spy object with properties: called, args, restore() + */ +export const mockConsole = (method) => { + const original = console[method]; + const spy = { called: false, args: null }; + console[method] = (...capturedArgs) => { + spy.called = true; + spy.args = capturedArgs; + }; + spy.restore = () => console[method] = original; + return spy; +}; + +/** + * Creates a mock for dbService.createSession + * @param {*} returnValue - Value to return (can be a function for custom behavior) + */ +export const mockDbCreate = (returnValue) => { + if (typeof returnValue === 'function') { + dbService.createSession = returnValue; + } else { + dbService.createSession = async () => returnValue; + } +}; + +/** + * Creates a mock for dbService.updateSession + * @param {*} returnValue - Value to return (can be a function for custom behavior) + */ +export const mockDbUpdate = (returnValue) => { + if (typeof returnValue === 'function') { + dbService.updateSession = returnValue; + } else { + dbService.updateSession = async () => returnValue; + } +}; diff --git a/tests/updateSessionSync.test.js b/tests/updateSessionSync.test.js new file mode 100644 index 0000000..e672d55 --- /dev/null +++ b/tests/updateSessionSync.test.js @@ -0,0 +1,262 @@ +import { spacesService } from '../js/background/spacesService.js'; +import { dbService } from '../js/background/dbService.js'; +import { mockConsole, mockDbUpdate } from './helpers.js'; + +describe('_updateSessionSync', () => { + let originalUpdateSession; + const EXISTING_SESSION = { id: 123, windowId: false, name: 'Old Name', tabs: [{ url: 'https://old.com' }], history: [] }; + const UPDATED_SESSION = { id: 123, windowId: 100, name: 'New Name', tabs: [{ url: 'https://new.com' }], history: [], lastAccess: new Date(), lastModified: new Date() }; + + beforeEach(() => { + // Reset sessions array and initialization state + spacesService.sessions = []; + spacesService.initialized = true; + + // Store original method + originalUpdateSession = dbService.updateSession; + }); + + afterEach(() => { + // Restore original method + if (originalUpdateSession) { + dbService.updateSession = originalUpdateSession; + } + }); + + test('successfully updates session and syncs memory cache in place', async () => { + const existingSession = structuredClone(EXISTING_SESSION); + const updatedSession = { ...UPDATED_SESSION, lastModified: new Date() }; + + // Add session to memory + spacesService.sessions.push(existingSession); + + // Modify the session (simulating real usage) + existingSession.windowId = 100; + existingSession.name = 'New Name'; + + // Mock successful database update + let updateSessionCalled = false; + let updateSessionArg = null; + mockDbUpdate(async (session) => { + updateSessionCalled = true; + updateSessionArg = session; + return updatedSession; + }); + + const result = await spacesService._updateSessionSync(existingSession); + + expect(updateSessionCalled).toBe(true); + expect(updateSessionArg).toBe(existingSession); + expect(result).toBe(spacesService.sessions[0]); // Returns the updated in-memory object + expect(result.windowId).toBe(100); // Property was updated + expect(result.name).toBe('New Name'); // Property was updated + expect(result.lastModified).toBeDefined(); // Database properties merged + expect(spacesService.sessions).toHaveLength(1); + + // Verify the original object was updated in place (reference preserved) + expect(spacesService.sessions[0]).toBe(existingSession); + expect(existingSession.lastModified).toBeDefined(); // Original object was updated + }); + + test('handles database update failure gracefully', async () => { + const existingSession = structuredClone(EXISTING_SESSION); + + spacesService.sessions.push(existingSession); + + // Store original values + const originalWindowId = existingSession.windowId; + const originalName = existingSession.name; + + // Mock database failure + mockDbUpdate(null); + + const result = await spacesService._updateSessionSync(existingSession); + + expect(result).toBeNull(); + + // Memory cache should remain unchanged on failure + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions[0]).toBe(existingSession); + // Original properties should remain (no sync occurred) + expect(existingSession.windowId).toBe(originalWindowId); + expect(existingSession.name).toBe(originalName); + }); + + test('handles database update exception gracefully', async () => { + const existingSession = structuredClone(EXISTING_SESSION); + + spacesService.sessions.push(existingSession); + + // Mock database exception + const dbError = new Error('Database update failed'); + mockDbUpdate(async () => { + throw dbError; + }); + + // Spy on console.error to verify error handling + const errorSpy = mockConsole('error'); + + const result = await spacesService._updateSessionSync(existingSession); + + expect(result).toBeNull(); + expect(errorSpy.called).toBe(true); + expect(errorSpy.args[0]).toBe('Error updating session with sync:'); + expect(errorSpy.args[1]).toBe(dbError); + + // Memory cache should remain unchanged on exception + expect(spacesService.sessions).toHaveLength(1); + expect(spacesService.sessions[0]).toBe(existingSession); + }); + + test('handles session not found in memory cache by ID', async () => { + const sessionToUpdate = structuredClone(UPDATED_SESSION); + sessionToUpdate.id = 999; // ID not in memory cache + + const updatedSession = structuredClone(UPDATED_SESSION); + updatedSession.id = 999; + + // Add a different session to memory cache + spacesService.sessions.push(structuredClone(EXISTING_SESSION)); + + // Mock successful database update + mockDbUpdate(updatedSession); + + // Spy on console.warn to verify warning + const warnSpy = mockConsole('warn'); + + const result = await spacesService._updateSessionSync(sessionToUpdate); + + expect(result).toBe(updatedSession); // Returns database result directly + expect(warnSpy.called).toBe(true); + expect(warnSpy.args[0]).toBe('Session not found in memory cache during update sync'); + expect(spacesService.sessions).toHaveLength(1); // Cache unchanged + }); + + test('preserves object references for UI stability', async () => { + const existingSession = structuredClone(EXISTING_SESSION); + + const updatedSession = structuredClone(UPDATED_SESSION); + + spacesService.sessions.push(existingSession); + + // Store reference to original object + const originalRef = existingSession; + const originalArrayRef = spacesService.sessions[0]; + + mockDbUpdate(updatedSession); + + const result = await spacesService._updateSessionSync(existingSession); + + // Verify that references are preserved (critical for UI) + expect(result).toBe(originalRef); + expect(spacesService.sessions[0]).toBe(originalRef); + expect(spacesService.sessions[0]).toBe(originalArrayRef); + + // But properties were updated + expect(originalRef.windowId).toBe(UPDATED_SESSION.windowId); + expect(originalRef.name).toBe(UPDATED_SESSION.name); + expect(originalRef.lastAccess).toBeDefined(); + }); + + test('finds correct session by ID in array with multiple sessions', async () => { + const session1 = structuredClone(EXISTING_SESSION); + session1.id = 111; + session1.windowId = 100; + session1.name = 'Session 1'; + + const session2 = structuredClone(EXISTING_SESSION); + session2.id = 222; + session2.windowId = 200; + session2.name = 'Session 2'; + + const targetSession = structuredClone(EXISTING_SESSION); + targetSession.id = 333; + targetSession.windowId = 300; + targetSession.name = 'Target'; + + const updatedTargetSession = structuredClone(UPDATED_SESSION); + updatedTargetSession.id = 333; + updatedTargetSession.windowId = 300; + updatedTargetSession.name = 'Updated Target'; + + // Add multiple sessions + spacesService.sessions.push(session1, session2, targetSession); + + mockDbUpdate(updatedTargetSession); + + const result = await spacesService._updateSessionSync(targetSession); + + expect(result).toBe(targetSession); // Correct session updated + expect(spacesService.sessions).toHaveLength(3); + expect(spacesService.sessions[0]).toBe(session1); // Others unchanged references + expect(spacesService.sessions[1]).toBe(session2); // Others unchanged references + expect(spacesService.sessions[2]).toBe(targetSession); // Target updated reference preserved + expect(targetSession.name).toBe('Updated Target'); // Properties updated + expect(targetSession.lastModified).toBeDefined(); // New properties added + }); + + test('handles windowId association changes correctly', async () => { + const session = structuredClone(EXISTING_SESSION); + + const updatedSession = structuredClone(UPDATED_SESSION); + + spacesService.sessions.push(session); + + // Simulate matchSessionToWindow behavior + session.windowId = UPDATED_SESSION.windowId; + + mockDbUpdate(updatedSession); + + const result = await spacesService._updateSessionSync(session); + + expect(result).toBe(session); + expect(session.windowId).toBe(100); // WindowId change preserved + expect(session.lastAccess).toBeDefined(); // Database changes synced + }); + + test('handles complex object properties correctly', async () => { + const session = { + id: 123, + windowId: 100, + name: 'Test Session', + tabs: [ + { url: 'https://example.com', title: 'Example', pinned: false }, + { url: 'https://test.com', title: 'Test', pinned: true } + ], + history: [ + { url: 'https://old.com', title: 'Old Page' } + ] + }; + + const updatedSession = { + id: 123, + windowId: 100, + name: 'Test Session', + tabs: [ + { url: 'https://example.com', title: 'Updated Example', pinned: false }, + { url: 'https://test.com', title: 'Test', pinned: true }, + { url: 'https://new.com', title: 'New Page', pinned: false } + ], + history: [ + { url: 'https://old.com', title: 'Old Page' }, + { url: 'https://recent.com', title: 'Recent Page' } + ], + sessionHash: 98765, + lastAccess: new Date() + }; + + spacesService.sessions.push(session); + + mockDbUpdate(updatedSession); + + const result = await spacesService._updateSessionSync(session); + + expect(result).toBe(session); + expect(session.tabs).toHaveLength(3); // Tabs array updated + expect(session.tabs[0].title).toBe('Updated Example'); // Nested properties updated + expect(session.tabs[2].url).toBe('https://new.com'); // New items added + expect(session.history).toHaveLength(2); // History updated + expect(session.sessionHash).toBe(98765); // Computed properties synced + expect(session.lastAccess).toBeDefined(); // Timestamps synced + }); +}); From d582d028d927292d5adcf352b0883f0c4280903d Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 7 Sep 2025 20:04:36 -0700 Subject: [PATCH 88/92] Fix issue #11 by restoring popup click handlers in popup.js and add a unit test. --- CHANGELOG.md | 2 +- js/background/background.js | 3 +- js/popup.js | 87 +++++++++++---------- tests/cleanUrl.test.js | 9 +-- tests/filterInternalWindows.test.js | 9 +-- tests/generateSessionHash.test.js | 9 +-- tests/helpers.js | 61 +++++++++++++++ tests/popupClickHandlers.test.js | 114 ++++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 tests/popupClickHandlers.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3322000..b74642b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,4 +10,4 @@ All notable changes to this project will be documented in this file. - Updated all code to modern JavaScript and improved documentation. - Fixed [issue #3](https://github.com/codedread/spaces/issues/3) by escaping HTML for all extension content. -- Increased unit test coverage from 0% to 6.99%. +- Increased unit test coverage from 0% to 7.51%. diff --git a/js/background/background.js b/js/background/background.js index c77b82c..bd4e9d1 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -255,7 +255,8 @@ async function processMessage(request, sender) { return requestCurrentSpace(); case 'generatePopupParams': - return generatePopupParams(request.action, request.tabUrl); + // TODO: Investigate if || request.action should be removed. + return generatePopupParams(request.popupAction || request.action, request.tabUrl); case 'loadSession': sessionId = cleanParameter(request.sessionId); diff --git a/js/popup.js b/js/popup.js index d8d2c94..bd8f5ec 100644 --- a/js/popup.js +++ b/js/popup.js @@ -7,6 +7,17 @@ import { checkSessionOverwrite, escapeHtml } from './utils.js'; const UNSAVED_SESSION = '(unnamed window)'; const NO_HOTKEY = 'no hotkey set'; +/** + * Handles popup menu clicks by generating popup params and reloading + * @param {string} action The popup action ('switch' or 'move') + */ +export async function handlePopupMenuClick(action) { + const params = await chrome.runtime.sendMessage({'action': 'generatePopupParams', 'popupAction': action}); + if (!params) return; + window.location.hash = params; + window.location.reload(); +} + const nodes = {}; let globalCurrentSpace; let globalTabId; @@ -14,35 +25,39 @@ let globalUrl; let globalWindowId; let globalSessionName; -/** - * POPUP INIT - */ - -document.addEventListener('DOMContentLoaded', async () => { - const url = getHashVariable('url', window.location.href); - globalUrl = url !== '' ? decodeURIComponent(url) : false; - const currentWindow = await chrome.windows.getCurrent({ populate: true }); - const windowId = currentWindow.id; - globalWindowId = windowId !== '' ? windowId : false; - globalTabId = getHashVariable('tabId', window.location.href); - const sessionName = getHashVariable( - 'sessionName', - window.location.href - ); - globalSessionName = - sessionName && sessionName !== 'false' ? sessionName : false; - const action = getHashVariable('action', window.location.href); - - const requestSpacePromise = globalWindowId - ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) - : chrome.runtime.sendMessage({ action: 'requestCurrentSpace' }); - - requestSpacePromise.then(space => { - globalCurrentSpace = space; - renderCommon(); - routeView(action); +/** Initialize the popup window. */ +function initializePopup() { + document.addEventListener('DOMContentLoaded', async () => { + const url = getHashVariable('url', window.location.href); + globalUrl = url !== '' ? decodeURIComponent(url) : false; + const currentWindow = await chrome.windows.getCurrent({ populate: true }); + const windowId = currentWindow.id; + globalWindowId = windowId !== '' ? windowId : false; + globalTabId = getHashVariable('tabId', window.location.href); + const sessionName = getHashVariable( + 'sessionName', + window.location.href + ); + globalSessionName = + sessionName && sessionName !== 'false' ? sessionName : false; + const action = getHashVariable('action', window.location.href); + + const requestSpacePromise = globalWindowId + ? chrome.runtime.sendMessage({ action: 'requestSpaceFromWindowId', windowId: globalWindowId }) + : chrome.runtime.sendMessage({ action: 'requestCurrentSpace' }); + + requestSpacePromise.then(space => { + globalCurrentSpace = space; + renderCommon(); + routeView(action); + }); }); -}); +} + +// Auto-initialize when loaded in browser context +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + initializePopup(); +} function routeView(action) { if (action === 'move') { @@ -138,22 +153,10 @@ async function renderMainCard() { }); document .querySelector('#switcherLink .optionText') - .addEventListener('click', async () => { - const params = await chrome.runtime.sendMessage({'action': 'switch'}); - if (!params) return; - window.location.hash = params; - window.location.reload(); - renderSwitchCard(); - }); + .addEventListener('click', () => handlePopupMenuClick('switch')); document .querySelector('#moverLink .optionText') - .addEventListener('click', async () => { - const params = await chrome.runtime.sendMessage({'action': 'generatePopupParams'}); - if (!params) return; - window.location.hash = params; - window.location.reload(); - // renderMoveCard() - }); + .addEventListener('click', () => handlePopupMenuClick('move')); } async function requestHotkeys() { diff --git a/tests/cleanUrl.test.js b/tests/cleanUrl.test.js index e898549..be48422 100644 --- a/tests/cleanUrl.test.js +++ b/tests/cleanUrl.test.js @@ -3,13 +3,10 @@ */ import { cleanUrl } from '../js/background/spacesService.js'; +import { setupMinimalChromeMocks } from './helpers.js'; -// Mock chrome.runtime.id for testing -global.chrome = { - runtime: { - id: 'test-extension-id-12345' - } -}; +// Setup minimal Chrome mocks for testing +setupMinimalChromeMocks(); describe('cleanUrl', () => { describe('basic functionality', () => { diff --git a/tests/filterInternalWindows.test.js b/tests/filterInternalWindows.test.js index 73fb6f3..460de20 100644 --- a/tests/filterInternalWindows.test.js +++ b/tests/filterInternalWindows.test.js @@ -3,13 +3,10 @@ */ import { filterInternalWindows } from '../js/background/spacesService.js'; +import { setupMinimalChromeMocks } from './helpers.js'; -// Mock chrome.runtime.id for testing -global.chrome = { - runtime: { - id: 'test-extension-id-12345' - } -}; +// Setup minimal Chrome mocks for testing +setupMinimalChromeMocks(); describe('filterInternalWindows', () => { describe('normal windows (should not be filtered)', () => { diff --git a/tests/generateSessionHash.test.js b/tests/generateSessionHash.test.js index 5380871..b58e4db 100644 --- a/tests/generateSessionHash.test.js +++ b/tests/generateSessionHash.test.js @@ -3,13 +3,10 @@ */ import { generateSessionHash } from '../js/background/spacesService.js'; +import { setupMinimalChromeMocks } from './helpers.js'; -// Mock chrome.runtime.id for testing -global.chrome = { - runtime: { - id: 'test-extension-id-12345' - } -}; +// Setup minimal Chrome mocks for testing +setupMinimalChromeMocks(); describe('generateSessionHash', () => { describe('deterministic behavior', () => { diff --git a/tests/helpers.js b/tests/helpers.js index 29f815f..b5a78a7 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -1,8 +1,69 @@ /** * Shared test helper functions */ +import { jest } from '@jest/globals'; import { dbService } from '../js/background/dbService.js'; +// Re-export jest for convenience +export { jest }; + +/** + * Sets up global Chrome API mocks for testing + */ +export const setupChromeMocks = () => { + global.chrome = { + runtime: { + sendMessage: jest.fn(), + }, + windows: { + getCurrent: jest.fn(), + }, + tabs: { + query: jest.fn(), + } + }; +}; + +/** + * Sets up minimal Chrome API mocks for testing (just runtime.id) + */ +export const setupMinimalChromeMocks = () => { + global.chrome = { + runtime: { + id: 'test-extension-id-12345' + } + }; +}; + +/** + * Sets up global DOM mocks for testing + */ +export const setupDOMMocks = () => { + global.document = { + addEventListener: jest.fn(), + getElementById: jest.fn(), + querySelector: jest.fn(), + querySelectorAll: jest.fn(), + }; + + global.window = { + location: { + href: 'popup.html#', + hash: '', + reload: jest.fn(), + }, + close: jest.fn(), + }; +}; + +/** + * Sets up all common test mocks (Chrome APIs and DOM) + */ +export const setupTestMocks = () => { + setupChromeMocks(); + setupDOMMocks(); +}; + /** * Creates a mock for console methods (error, warn, log, etc.) * @param {string} method - The console method to mock ('error', 'warn', 'log', etc.) diff --git a/tests/popupClickHandlers.test.js b/tests/popupClickHandlers.test.js new file mode 100644 index 0000000..4942476 --- /dev/null +++ b/tests/popupClickHandlers.test.js @@ -0,0 +1,114 @@ +/** + * Unit tests for popup.js functionality + * Tests the popup menu item click handlers that send messages to background script + */ + +import { jest, setupTestMocks } from './helpers.js'; +import { handlePopupMenuClick } from '../js/popup.js'; + +// Setup all test mocks +setupTestMocks(); + +describe('Popup Menu Click Handlers', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Reset window location + window.location.hash = ''; + window.location.reload.mockClear(); + chrome.runtime.sendMessage.mockClear(); + }); + + test('handlePopupMenuClick with switch action sends correct message and reloads popup', async () => { + // Setup: Mock successful response from background script + const mockParams = 'action=switch&windowId=123&sessionName=TestSpace&tabId=456'; + chrome.runtime.sendMessage.mockResolvedValue(mockParams); + + // Execute the click handler with switch action + await handlePopupMenuClick('switch'); + + // Verify the message was sent with correct parameters + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + 'action': 'generatePopupParams', + 'popupAction': 'switch' + }); + + // Verify popup was reloaded with correct parameters + expect(window.location.hash).toBe(mockParams); + expect(window.location.reload).toHaveBeenCalled(); + }); + + test('handlePopupMenuClick with move action sends correct message and reloads popup', async () => { + // Setup: Mock successful response from background script + const mockParams = 'action=move&windowId=123&sessionName=TestSpace&tabId=456'; + chrome.runtime.sendMessage.mockResolvedValue(mockParams); + + // Execute the click handler with move action + await handlePopupMenuClick('move'); + + // Verify the message was sent with correct parameters + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + 'action': 'generatePopupParams', + 'popupAction': 'move' + }); + + // Verify popup was reloaded with correct parameters + expect(window.location.hash).toBe(mockParams); + expect(window.location.reload).toHaveBeenCalled(); + }); + + test('handlePopupMenuClick handles empty response gracefully', async () => { + // Setup: Mock empty response from background script + chrome.runtime.sendMessage.mockResolvedValue(''); + + // Execute the click handler + await handlePopupMenuClick('test'); + + // Verify the message was sent + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + 'action': 'generatePopupParams', + 'popupAction': 'test' + }); + + // Verify popup was NOT reloaded due to empty response + expect(window.location.hash).toBe(''); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + test('handlePopupMenuClick handles null response gracefully', async () => { + // Setup: Mock null response from background script + chrome.runtime.sendMessage.mockResolvedValue(null); + + // Execute the click handler + await handlePopupMenuClick('move'); + + // Verify the message was sent + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + 'action': 'generatePopupParams', + 'popupAction': 'move' + }); + + // Verify popup was NOT reloaded due to null response + expect(window.location.hash).toBe(''); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + test('handlePopupMenuClick handles sendMessage rejection', async () => { + // Setup: Mock sendMessage rejection + const mockError = new Error('Background script error'); + chrome.runtime.sendMessage.mockRejectedValue(mockError); + + // Execute the click handler - it should throw since there's no error handling + await expect(handlePopupMenuClick('switch')).rejects.toThrow('Background script error'); + + // Verify the message was sent + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + 'action': 'generatePopupParams', + 'popupAction': 'switch' + }); + + // Verify popup was NOT reloaded due to error + expect(window.location.hash).toBe(''); + expect(window.location.reload).not.toHaveBeenCalled(); + }); +}); From d0ffdf96150d6a11caf5c37e647a392f77360879 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 7 Sep 2025 20:38:40 -0700 Subject: [PATCH 89/92] Fix issue #9 be aligning the selected space properly inside spaces.js --- js/spaces.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/js/spaces.js b/js/spaces.js index 5f6f957..4e2f98c 100644 --- a/js/spaces.js +++ b/js/spaces.js @@ -13,6 +13,9 @@ let bannerState; function renderSpacesList(spaces) { let spaceEl; + + // Clear globalSelectedSpace at the start - it will be set if we find a match + globalSelectedSpace = null; nodes.openSpaces.innerHTML = ''; nodes.closedSpaces.innerHTML = ''; @@ -46,14 +49,17 @@ function renderSpaceListEl(space) { linkEl.innerHTML = UNSAVED_SESSION; } + // Check if this space should be selected based on current hash + const currentSessionId = getHashVariable('sessionId', window.location.href); + const currentWindowId = getHashVariable('windowId', window.location.href); + if ( - globalSelectedSpace && - ((space.windowId && - globalSelectedSpace.windowId === space.windowId) || - (space.sessionId && - globalSelectedSpace.sessionId === space.sessionId)) + (currentSessionId && space.sessionId && currentSessionId == space.sessionId) || + (currentWindowId && space.windowId && currentWindowId == space.windowId) ) { linkEl.className = 'selected'; + // Also update globalSelectedSpace for the detail view + globalSelectedSpace = space; } // if (space && !space.windowId) { @@ -541,9 +547,10 @@ async function performRestoreFromBackup(spaces) { function addEventListeners() { // register hashchange listener - window.onhashchange = () => { - updateSpacesList(); - updateSpaceDetail(); + window.onhashchange = async () => { + await updateSpacesList(); + // Update the detail view using the globalSelectedSpace set by updateSpacesList + await updateSpaceDetail(true); }; // register incoming events listener @@ -653,15 +660,10 @@ async function updateSpaceDetail(useCachedSpace) { } else if (sessionId || windowId) { const space = await fetchSpaceDetail(sessionId, windowId); addDuplicateMetadata(space); - - // cache current selected space - globalSelectedSpace = space; renderSpaceDetail(space, editMode); // otherwise hide space detail view } else { - // clear cache - globalSelectedSpace = false; renderSpaceDetail(false, false); } } From 915d1808158840a234f1f351f7bf5d87fc702ae6 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 7 Sep 2025 21:17:04 -0700 Subject: [PATCH 90/92] Fix issue where new chrome windows would not appear in the Spaces window without refreshing it. --- js/background/background.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index bd4e9d1..b5c2458 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -141,12 +141,11 @@ export function initializeServiceWorker() { } }); - // don't need this listener as the tabUpdated listener also fires when a new window is created - // chrome.windows.onCreated.addListener(function (window) { - - // if (checkInternalSpacesWindows(window.id, false)) return; - // spacesService.handleWindowCreated(window); - // }); + // Add listener for window creation to ensure new windows are detected + chrome.windows.onCreated.addListener(function (window) { + if (checkInternalSpacesWindows(window.id, false)) return; + setTimeout(() => updateSpacesWindow('windows.onCreated'), 100); + }); // add listeners for tab and window focus changes // when a tab or window is changed, close the move tab popup if it is open From 7246ad85997617e6d2b10783e379cb1a9abe86dd Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 7 Sep 2025 21:39:14 -0700 Subject: [PATCH 91/92] Refactor spacesRenderer functions to be module-level exported: getTabDetailsString() and getDefaultSpaceTitle() and add unit tests for them. --- CHANGELOG.md | 2 +- js/spacesRenderer.js | 48 +++++++++++++++--------------- tests/getDefaultSpaceTitle.test.js | 33 ++++++++++++++++++++ tests/getTabDetailsString.test.js | 23 ++++++++++++++ 4 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 tests/getDefaultSpaceTitle.test.js create mode 100644 tests/getTabDetailsString.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b74642b..a6c8e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,4 +10,4 @@ All notable changes to this project will be documented in this file. - Updated all code to modern JavaScript and improved documentation. - Fixed [issue #3](https://github.com/codedread/spaces/issues/3) by escaping HTML for all extension content. -- Increased unit test coverage from 0% to 7.51%. +- Increased unit test coverage from 0% to ???%. diff --git a/js/spacesRenderer.js b/js/spacesRenderer.js index d1757fe..faeaad7 100644 --- a/js/spacesRenderer.js +++ b/js/spacesRenderer.js @@ -1,5 +1,27 @@ import { escapeHtml } from './utils.js'; +export function getDefaultSpaceTitle(space) { + const count = space.tabs && space.tabs.length; + if (!count) return ''; + const firstTitle = space.tabs[0].title; + if (count === 1) { + return `[${escapeHtml(firstTitle)}]`; + } + return firstTitle.length > 30 + ? `[${escapeHtml(firstTitle.slice(0, 21))}…] +${count - 1} more` + : `[${escapeHtml(firstTitle)}] +${count - 1} more`; +} + +export function getTabDetailsString(space) { + const count = space.tabs && space.tabs.length; + const open = space.windowId; + + if (open) { + return ''; + } + return `(${count} tab${count !== 1 ? 's' : ''})`; +} + // eslint-disable-next-line no-var export const spacesRenderer = { nodes: {}, @@ -56,8 +78,8 @@ export const spacesRenderer = { listDetail.className = 'spaceDetail'; listTitle.innerHTML = - space.name || spacesRenderer.getDefaultSpaceTitle(space); - listDetail.innerHTML = spacesRenderer.getTabDetailsString(space); + space.name || getDefaultSpaceTitle(space); + listDetail.innerHTML = getTabDetailsString(space); listContainer.appendChild(listTitle); listContainer.appendChild(listDetail); @@ -147,28 +169,6 @@ export const spacesRenderer = { } }, - getDefaultSpaceTitle(space) { - const count = space.tabs && space.tabs.length; - if (!count) return ''; - const firstTitle = space.tabs[0].title; - if (count === 1) { - return `[${escapeHtml(firstTitle)}]`; - } - return firstTitle.length > 30 - ? `[${escapeHtml(firstTitle.slice(0, 21))}…] +${count - 1} more` - : `[${escapeHtml(firstTitle)}] +${count - 1} more`; - }, - - getTabDetailsString(space) { - const count = space.tabs && space.tabs.length; - const open = space.windowId; - - if (open) { - return ''; - } - return `(${count} tab${count > 1 ? 's' : ''})`; - }, - updateSpacesList() { const query = spacesRenderer.nodes.moveInput.value; let match = false; diff --git a/tests/getDefaultSpaceTitle.test.js b/tests/getDefaultSpaceTitle.test.js new file mode 100644 index 0000000..efc2b4e --- /dev/null +++ b/tests/getDefaultSpaceTitle.test.js @@ -0,0 +1,33 @@ +import { getDefaultSpaceTitle } from '../js/spacesRenderer.js'; + +describe('getDefaultSpaceTitle', () => { + test('should return an empty string if there are no tabs', () => { + const space = { tabs: [] }; + expect(getDefaultSpaceTitle(space)).toBe(''); + }); + + test('should return the title of the single tab', () => { + const space = { tabs: [{ title: 'Test Tab' }] }; + expect(getDefaultSpaceTitle(space)).toBe('[Test Tab]'); + }); + + test('should return the title of the first tab and a count of the others', () => { + const space = { tabs: [{ title: 'Test Tab' }, { title: 'Another Tab' }] }; + expect(getDefaultSpaceTitle(space)).toBe('[Test Tab] +1 more'); + }); + + test('should truncate long titles', () => { + const space = { + tabs: [ + { title: 'This is a very long tab title that should be truncated' }, + { title: 'Another Tab' } + ] + }; + expect(getDefaultSpaceTitle(space)).toBe('[This is a very long t…] +1 more'); + }); + + test('should escape HTML in the title', () => { + const space = { tabs: [{ title: '' }] }; + expect(getDefaultSpaceTitle(space)).toBe('[<script>alert("xss")</script>]'); + }); +}); diff --git a/tests/getTabDetailsString.test.js b/tests/getTabDetailsString.test.js new file mode 100644 index 0000000..b993c49 --- /dev/null +++ b/tests/getTabDetailsString.test.js @@ -0,0 +1,23 @@ +import { getTabDetailsString } from '../js/spacesRenderer.js'; + +describe('getTabDetailsString', () => { + test('should return an empty string if the space is open', () => { + const space = { windowId: 123, tabs: [{ title: 'Test Tab' }] }; + expect(getTabDetailsString(space)).toBe(''); + }); + + test('should return (0 tabs) if the space is not open and has no tabs', () => { + const space = { windowId: false, tabs: [] }; + expect(getTabDetailsString(space)).toBe('(0 tabs)'); + }); + + test('should return (1 tab) if the space is not open and has one tab', () => { + const space = { windowId: false, tabs: [{ title: 'Test Tab' }] }; + expect(getTabDetailsString(space)).toBe('(1 tab)'); + }); + + test('should return (n tabs) if the space is not open and has multiple tabs', () => { + const space = { windowId: false, tabs: [{ title: 'Test Tab' }, { title: 'Another Tab' }] }; + expect(getTabDetailsString(space)).toBe('(2 tabs)'); + }); +}); From 307d014ebc654da67483ad51efe4726d724785a9 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 7 Sep 2025 22:03:17 -0700 Subject: [PATCH 92/92] Shuffle some functions around. --- js/background/background.js | 1 + js/background/spacesService.js | 2 +- js/spacesRenderer.js | 59 ++++++++++++++++++++++------------ 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/js/background/background.js b/js/background/background.js index b5c2458..97f468b 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -817,6 +817,7 @@ async function requestSpaceFromSessionId(sessionId) { async function requestAllSpaces() { // Get all sessions from spacesService (includes both saved and temporary open window sessions) const allSessions = await spacesService.getAllSessions(); + /** @type {Space[]} */ const allSpaces = allSessions .map(session => { return { sessionId: session.id, ...session }; diff --git a/js/background/spacesService.js b/js/background/spacesService.js index ff02273..8aae49c 100644 --- a/js/background/spacesService.js +++ b/js/background/spacesService.js @@ -363,7 +363,7 @@ class SpacesService { /** * Get all sessions (includes both saved sessions and temporary open window sessions) - * @returns {Promise} Promise that resolves to a shallow copy of all sessions + * @returns {Promise>} Promise that resolves to a shallow copy of all sessions */ async getAllSessions() { await this.ensureInitialized(); diff --git a/js/spacesRenderer.js b/js/spacesRenderer.js index faeaad7..6f703e4 100644 --- a/js/spacesRenderer.js +++ b/js/spacesRenderer.js @@ -1,26 +1,6 @@ import { escapeHtml } from './utils.js'; -export function getDefaultSpaceTitle(space) { - const count = space.tabs && space.tabs.length; - if (!count) return ''; - const firstTitle = space.tabs[0].title; - if (count === 1) { - return `[${escapeHtml(firstTitle)}]`; - } - return firstTitle.length > 30 - ? `[${escapeHtml(firstTitle.slice(0, 21))}…] +${count - 1} more` - : `[${escapeHtml(firstTitle)}] +${count - 1} more`; -} - -export function getTabDetailsString(space) { - const count = space.tabs && space.tabs.length; - const open = space.windowId; - - if (open) { - return ''; - } - return `(${count} tab${count !== 1 ? 's' : ''})`; -} +/** @typedef {import('./common.js').Space} Space */ // eslint-disable-next-line no-var export const spacesRenderer = { @@ -248,3 +228,40 @@ export const spacesRenderer = { } }, }; + +// Module-level helper functions. + +/** + * Generates a default title for a space when it hasn't been named. + * Based on the title of the first tab and the total number of tabs. + * @param {Space} space - The space object + * @returns {string} The generated title string + */ +export function getDefaultSpaceTitle(space) { + const count = space.tabs && space.tabs.length; + if (!count) return ''; + const firstTitle = space.tabs[0].title; + if (count === 1) { + return `[${escapeHtml(firstTitle)}]`; + } + return firstTitle.length > 30 + ? `[${escapeHtml(firstTitle.slice(0, 21))}…] +${count - 1} more` + : `[${escapeHtml(firstTitle)}] +${count - 1} more`; +} + +/** + * Generates a string with the number of tabs in a space. + * Returns an empty string if the space is currently open. + * @param {Space} space - The space object + * @returns {string} The generated string with the number of tabs + */ +export function getTabDetailsString(space) { + const count = space.tabs && space.tabs.length; + const open = space.windowId; + + if (open) { + return ''; + } + return `(${count} tab${count !== 1 ? 's' : ''})`; +} +