diff --git a/package.json b/package.json index eebff42..d81f031 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@fluentui/react-components": "^9.7.0", "@fluentui/react-icons": "^2.0.186", + "lzutf8": "^0.6.3", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.54.9", diff --git a/src/Models/Data/IGraphics.ts b/src/Models/Data/IGraphics.ts new file mode 100644 index 0000000..6a7173f --- /dev/null +++ b/src/Models/Data/IGraphics.ts @@ -0,0 +1,5 @@ +export default interface IGraphics +{ + Icon?: string; + Thumbnail?: string; +} diff --git a/src/Models/Data/TabModel.ts b/src/Models/Data/TabModel.ts index 70398cd..f0d31ea 100644 --- a/src/Models/Data/TabModel.ts +++ b/src/Models/Data/TabModel.ts @@ -18,16 +18,6 @@ export default class TabModel */ public ScrollPosition?: number; - /** - * Tab's thumbnail (optional) - */ - public Thumbnail?: string; - - /** - * Tab's favicon (optional) - */ - public Icon?: string; - /** * @param uri Tab's URL */ @@ -47,24 +37,12 @@ export default class TabModel * @param uri Tab's URL * @param title Tab's title * @param scrollPoisition Tab's scroll position - * @param graphics Tab's graphics data */ - constructor(uri: string, title: string, scrollPoisition: number, thumbnail: string); - constructor(uri: string, title?: string, scrollPosition?: number, thumbnail?: string) + constructor(uri: string, title: string, scrollPoisition: number); + constructor(uri: string, title?: string, scrollPosition?: number) { this.Url = uri; this.Title = title; - this.Thumbnail = thumbnail; this.ScrollPosition = scrollPosition; } - - public GetIcon(): string - { - if (this.Icon) - return this.Icon; - - let url = new URL(this.Url); - url.pathname = "/favicon.ico"; - return url.href; - } } diff --git a/src/Models/Data/index.d.ts b/src/Models/Data/index.d.ts index 29af5de..dada2cb 100644 --- a/src/Models/Data/index.d.ts +++ b/src/Models/Data/index.d.ts @@ -2,5 +2,6 @@ import CollectionModel from "./CollectionModel"; import GroupModel from "./GroupModel"; import TabModel from "./TabModel"; import SettingsModel from "./SettingsModel"; +import IGraphics from "./IGraphics"; -export { SettingsModel, CollectionModel, GroupModel, TabModel }; +export { SettingsModel, CollectionModel, GroupModel, TabModel, IGraphics }; diff --git a/src/Services/Storage/CollectionService.ts b/src/Services/Storage/CollectionService.ts deleted file mode 100644 index 336ce12..0000000 --- a/src/Services/Storage/CollectionService.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/src/Services/Storage/CollectionsRepository.ts b/src/Services/Storage/CollectionsRepository.ts new file mode 100644 index 0000000..1002feb --- /dev/null +++ b/src/Services/Storage/CollectionsRepository.ts @@ -0,0 +1,159 @@ +import { compress, decompress } from "lzutf8"; +import { Storage } from "webextension-polyfill"; +import { CollectionModel } from "../../Models/Data"; +import { ext } from "../../Utils"; +import CollectionOptimizer from "../../Utils/CollectionOptimizer"; + +/** + * Data repository that provides access to saved collections. + */ +export default class CollectionsRepository +{ + /** + * Fired when collections are changed. + */ + public ItemsChanged: (collections: CollectionModel[]) => void; + + private source: Storage.StorageArea = null; + + /** + * Generates a new instance of the class. + * @param source Storage area to be used + */ + public constructor(source: "sync" | "local") + { + this.source = source === "sync" ? ext?.storage.sync : ext?.storage.local; + ext?.storage.onChanged.addListener(this.OnStorageChanged); + } + + /** + * Gets saved collections from repository. + * @returns Saved collections + */ + public async GetCollectionsAsync(): Promise + { + if (!this.source) + return []; + + let chunks: { [key: string]: string; } = { }; + + // Setting which data to retrieve and its default value + // Saved collections are now stored in chunks. This is the most efficient way to store these. + for (let i = 0; i < 12; i++) + chunks[`chunk${i}`] = null; + + chunks = await this.source.get(chunks); + + let data: string = ""; + + for (let chunk of Object.values(chunks)) + if (chunk) + data += chunk; + + data = decompress(data, { inputEncoding: "StorageBinaryString" }); + + return CollectionOptimizer.DeserializeCollections(data); + } + + /** + * Adds new collection to repository. + * @param collection Collection to be saved + */ + public async AddCollectionAsync(collection: CollectionModel): Promise + { + if (!this.source) + return; + + let items: CollectionModel[] = await this.GetCollectionsAsync(); + items.push(collection); + + await this.SaveChangesAsync(items); + } + + /** + * Updates existing collection or adds a new one in repository. + * @param collection Collection to be updated + */ + public async UpdateCollectionAsync(collection: CollectionModel): Promise + { + if (!this.source) + return; + + let items: CollectionModel[] = await this.GetCollectionsAsync(); + let index = items.findIndex(i => i.Timestamp === collection.Timestamp); + + if (index === -1) + items.push(collection); + else + items[index] = collection; + + await this.SaveChangesAsync(items); + } + + /** + * Removes collection from repository. + * @param collection Collection to be removed + */ + public async RemoveCollectionAsync(collection: CollectionModel): Promise + { + if (!this.source) + return; + + let items: CollectionModel[] = await this.GetCollectionsAsync(); + items = items.filter(i => i.Timestamp !== collection.Timestamp); + + await this.SaveChangesAsync(items); + } + + /** + * Removes all collections from repository. + */ + public async Clear(): Promise + { + if (!this.source) + return; + + let keys: string[] = []; + + for (let i = 0; i < 12; i++) + keys.push(`chunk${i}`); + + await this.source.remove(keys); + } + + private async SaveChangesAsync(collections: CollectionModel[]): Promise + { + if (!this.source) + return; + + let data: string = CollectionOptimizer.SerializeCollections(collections); + data = compress(data, { outputEncoding: "StorageBinaryString" }); + + let chunks: string[] = CollectionOptimizer.SplitIntoChunks(data); + + let items: { [key: string]: string; } = {}; + + for (let i = 0; i < chunks.length; i++) + items[`chunk${i}`] = chunks[i]; + + let chunksToDelete: string[] = []; + + for (let i = chunks.length; i < 12; i++) + chunksToDelete.push(`chunk${i}`); + + await this.source.set(items); + await this.source.remove(chunksToDelete); + } + + private async OnStorageChanged(changes: { [key: string]: Storage.StorageChange }, areaName: string): Promise + { + if (!this.source) + return; + + if (!Object.keys(changes).some(k => k.startsWith("chunk"))) + return; + + let collections: CollectionModel[] = await this.GetCollectionsAsync(); + this.ItemsChanged?.(collections); + } +} diff --git a/src/Services/Storage/GraphicsRepository.ts b/src/Services/Storage/GraphicsRepository.ts new file mode 100644 index 0000000..c082c06 --- /dev/null +++ b/src/Services/Storage/GraphicsRepository.ts @@ -0,0 +1,63 @@ +import { IGraphics } from "../../Models/Data"; +import { ext } from "../../Utils"; + +/** + * Provides access to saved graphics (icons and thumbnails). + */ +export default class GraphicsRepository +{ + /** + * Gets saved graphics from storage. + * @returns Dictionary of IGraphics objects, where key is the URL of the graphics. + */ + public async GetGraphicsAsync(): Promise> + { + if (!ext) + return { }; + + let data: Record = await ext.storage.local.get(null); + let graphics: Record = { }; + + for (let key in data) + try + { + new URL(key); + graphics[key] = data[key] as IGraphics; + } + catch { continue; } + + return graphics; + } + + /** + * Saves graphics to storage. + * @param graphics Dictionary of IGraphics objects, where key is the URL of the graphics. + */ + public async AddOrUpdateGraphicsAsync(graphics: Record): Promise + { + if (!ext) + return; + + let data: Record = await ext.storage.local.get(Object.keys(graphics)); + + for (let key in graphics) + if (data[key] === undefined) + data[key] = graphics[key]; + else + data[key] = { ...data[key], ...graphics[key] }; + + await ext.storage.local.set(graphics); + } + + /** + * Removes graphics from storage. + * @param graphics Dictionary of IGraphics objects, where key is the URL of the graphics. + */ + public async RemoveGraphicsAsync(graphics: Record): Promise + { + if (!ext) + return; + + await ext.storage.local.remove(Object.keys(graphics)); + } +} diff --git a/src/Services/Storage/SettingsRepository.ts b/src/Services/Storage/SettingsRepository.ts new file mode 100644 index 0000000..6e391a6 --- /dev/null +++ b/src/Services/Storage/SettingsRepository.ts @@ -0,0 +1,59 @@ +import { Storage } from "webextension-polyfill"; +import { SettingsModel } from "../../Models/Data"; +import { ext } from "../../Utils"; + +/** + * Data repository that provides access to saved settings. + */ +export default class SettingsRepository +{ + /** + * Fired when settings are changed. + */ + public ItemsChanged: (changes: Partial) => void; + + public constructor() + { + ext?.storage.sync.onChanged.addListener(this.OnStorageChanged); + } + + /** + * Gets saved settings. + * @returns Saved settings + */ + public async GetSettingsAsync(): Promise + { + let fallbackOptions = new SettingsModel(); + + if (!ext) + return fallbackOptions; + + let options: Record = await ext.storage.sync.get(fallbackOptions); + + return new SettingsModel(options); + } + + /** + * Saves settings. + * @param changes Changes to be saved + */ + public async UpdateSettingsAsync(changes: Partial): Promise + { + if (ext) + await ext.storage.sync.set(changes); + else if (this.ItemsChanged) + this.ItemsChanged(changes); + } + + private OnStorageChanged(changes: { [key: string]: Storage.StorageChange }): void + { + let propsList: string[] = Object.keys(new SettingsRepository()); + let settings: { [key: string]: any; } = {}; + + Object.entries(changes) + .filter(i => propsList.includes(i[0])) + .map(i => settings[i[0]] = i[1].newValue); + + this.ItemsChanged?.(settings as Partial); + } +} diff --git a/src/Services/Storage/SettingsService.ts b/src/Services/Storage/SettingsService.ts deleted file mode 100644 index 6625549..0000000 --- a/src/Services/Storage/SettingsService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Browser, Storage } from "webextension-polyfill"; -import { SettingsModel } from "../../Models/Data"; -import { Event, ext } from "../../Utils"; - -export default class SettingsService -{ - private constructor() {} - - public static readonly Changed = new Event>(); - - public static async GetSettings(): Promise - { - let fallbackOptions = new SettingsModel(); - - if (!ext) - return fallbackOptions; - - let options: Record = await ext.storage.sync.get(fallbackOptions); - - if (!ext.storage.sync.onChanged.hasListener(SettingsService.OnStorageChanged)) - ext.storage.sync.onChanged.addListener(SettingsService.OnStorageChanged); - - return new SettingsModel(options); - } - - public static async SetSettings(changes: Partial): Promise - { - if (ext) - await ext.storage.sync.set(changes); - else - SettingsService.Changed.Invoke(null, changes); - } - - private static OnStorageChanged(changes: { [key: string]: Storage.StorageChange }): void - { - let propsList: string[] = Object.keys(new SettingsService()); - let settings: { [key: string]: any; } = {}; - - Object.entries(changes) - .filter(i => propsList.includes(i[0])) - .map(i => settings[i[0]] = i[1].newValue); - - SettingsService.Changed.Invoke(ext, settings as Partial); - } -} diff --git a/src/Services/Storage/index.d.ts b/src/Services/Storage/index.d.ts index 5e1b81b..f20e433 100644 --- a/src/Services/Storage/index.d.ts +++ b/src/Services/Storage/index.d.ts @@ -1,4 +1,4 @@ -import SettingsService from "./SettingsService"; -import CollectionService from "./CollectionService"; +import SettingsRepository from "./SettingsRepository"; +import CollectionsRepository from "./CollectionsRepository"; -export { SettingsService, CollectionService }; +export { SettingsRepository, CollectionsRepository as CollectionService }; diff --git a/src/Utils/CollectionOptimizer.ts b/src/Utils/CollectionOptimizer.ts new file mode 100644 index 0000000..2c1cf2a --- /dev/null +++ b/src/Utils/CollectionOptimizer.ts @@ -0,0 +1,205 @@ +import { CollectionModel, GroupModel, TabModel } from "../Models/Data"; +import ext from "./ext"; + +/** + * Optimizes the collection storage size by converting the collection to a more compact format. + */ +export default class CollectionOptimizer +{ + /** + * Split result data structure into chunks (default chunk size: 8183 bytes). + * @param data data to split. + * @description Default chunk size is 8183 bytes (storage.sync.QUOTA_BYTES_PER_ITEM - 9) + */ + public static SplitIntoChunks(data: string): string[]; + /** + * Split result data structure into chunks. + * @param data data to split. + * @param chunkSize max size of each chunk. + */ + public static SplitIntoChunks(data: string, chunkSize: number): string[]; + public static SplitIntoChunks(data: string, chunkSize?: number): string[] + { + let chunks: string[] = []; + + chunkSize ??= ext.storage.sync.QUOTA_BYTES_PER_ITEM - "chunkXX".length - 2; + // Remark: + // QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason). + + while (data.length > 0) + { + chunks.push(data.substring(0, chunkSize)); + data = data.substring(chunkSize); + } + + return chunks; + } + + /** + * Serializes collection data structure into a compact format string. + * @param collections collections to optimize. + * @returns optimized collection data structure. + */ + public static SerializeCollections(collections: CollectionModel[]): string + { + let data: string = ""; + + for (let collection of collections) + { + data += CollectionOptimizer.GetCollectionString(collection); + + for (let item of collection.Items) + { + if (item instanceof GroupModel) + { + data += CollectionOptimizer.GetGroupString((item as GroupModel)); + + for (let tab of (item as GroupModel).Tabs) + { + data += "\t"; + data += CollectionOptimizer.GetTabString(tab); + } + } + else + data += CollectionOptimizer.GetTabString(item as TabModel); + } + } + + return data; + } + + /** + * Deserializes collection data structure from a compact format string. + * @param data data to deserialize. + * @returns deserialized array of collection models. + */ + public static DeserializeCollections(data: string): CollectionModel[] + { + if (!data) + return []; + + let collections: CollectionModel[] = []; + + let lines: string[] = data.split("\n"); + + for (let line of lines) + { + if (line.startsWith("c")) + { + let collection: CollectionModel = CollectionOptimizer.GetCollection(line); + collections.push(collection); + } + else if (line.startsWith("\tg")) + { + let group: GroupModel = CollectionOptimizer.GetGroup(line); + collections[collections.length - 1].Items.push(group); + } + else if (line.startsWith("\t\tt")) + { + let tab: TabModel = CollectionOptimizer.GetTab(line); + + let colIndex: number = collections.length - 1; + let groupIndex: number = collections[colIndex].Items.length - 1; + + (collections[colIndex].Items[groupIndex] as GroupModel).Tabs.push(tab); + } + else if (line.startsWith("\tt")) + { + let tab: TabModel = CollectionOptimizer.GetTab(line); + collections[collections.length - 1].Items.push(tab); + } + } + + return collections; + } + + // #region Decompression functions + private static GetCollection(data: string): CollectionModel + { + return new CollectionModel( + data.match(/(?<=.*\|).*/)?.toString(), + data.match(/(?<=c\d+\/)[a-z]{1,}\b/)?.toString() as chrome.tabGroups.ColorEnum, + parseInt(data.match(/(?<=c)\d+/).toString()) + ); + } + + private static GetGroup(data: string): GroupModel + { + let isPinned: boolean = data.match(/g\/p/)?.toString() ? true : false; + + if (isPinned) + return new GroupModel(); + + return new GroupModel( + data.match(/(?<=g\/)[a-z]{1,}\b/).toString() as chrome.tabGroups.ColorEnum, + data.match(/(?<=.*\|).*/)?.toString(), + data.search(/(?<=g\/[a-z]+)\.c/) !== -1 + ); + } + + private static GetTab(data: string): TabModel + { + return new TabModel( + data.match(/(?<=t.*\|).*(?=\|)/).toString(), + data.match(/(?<=t.*\|.*\|).+/)?.toString(), + parseFloat(data.match(/(?<=t\/)[0-9,.]+(?=\|.*)/)?.toString() ?? "0") + ); + } + // #endregion + + // #region Compression functions + private static GetCollectionString(collection: CollectionModel): string + { + let data: string = `c${collection.Timestamp}`; + + if (collection.Color) + data += `/${collection.Color}`; + + if (collection.Title) + data += `|${collection.Title}`; + + data += "\n"; + + return data; + } + + private static GetGroupString(group: GroupModel): string + { + let data: string = `\tg`; + + if (group.IsPinned) + data += "/p"; + else + { + data += `/${group.Color}`; + + if (group.IsCollapsed) + data += ".c"; + + if (group.Title) + data += `|${group.Title}`; + } + + data += "\n"; + + return data; + } + + private static GetTabString(tab: TabModel): string + { + let data: string = "\tt"; + + if (tab.ScrollPosition) + data += `/${tab.ScrollPosition}`; + + data += `|${tab.Url}|`; + + if (tab.Title) + data += `${tab.Title}`; + + data += "\n"; + + return data; + } + // #endregion +} diff --git a/src/Utils/Event.ts b/src/Utils/Event.ts deleted file mode 100644 index a932ec0..0000000 --- a/src/Utils/Event.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type EventHandler = (sender: S, args: T) => void; - -export default class Event -{ - private _handlers: EventHandler[] = []; - - public AddListener(handler: EventHandler): void - { - this._handlers.push(handler); - } - - public RemoveListener(handler: EventHandler): void - { - this._handlers = this._handlers.filter(i => i !== handler); - } - - public Invoke(sender: S, args: T): void - { - this._handlers.forEach(i => i(sender, args)); - } -} diff --git a/yarn.lock b/yarn.lock index 052ddff..16eb632 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3074,6 +3074,13 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3519,6 +3526,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -3641,6 +3653,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -4911,12 +4931,17 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -5601,6 +5626,11 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -6673,6 +6703,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lzutf8@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/lzutf8/-/lzutf8-0.6.3.tgz#37a2ebe80922a8405f1e3f24c6c2b74c3e430981" + integrity sha512-CAkF9HKrM+XpB0f3DepQ2to2iUEo0zrbh+XgBqgNBc1+k8HMM3u/YSfHI3Dr4GmoTIez2Pr/If1XFl3rU26AwA== + dependencies: + readable-stream "^4.0.0" + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -7831,6 +7868,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + promise@^8.1.0: version "8.2.0" resolved "https://registry.yarnpkg.com/promise/-/promise-8.2.0.tgz#a1f6280ab67457fbfc8aad2b198c9497e9e5c806" @@ -8095,6 +8137,16 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.2.0.tgz#a7ef523d3b39e4962b0db1a1af22777b10eeca46" + integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"