From e6b6f64513708b602af1a64f65647885dea6f611 Mon Sep 17 00:00:00 2001 From: Alperen Elhan Date: Sat, 15 Oct 2022 16:13:39 +0300 Subject: [PATCH] feat: sync db with files --- next.config.mjs | 7 +- package.json | 4 +- prisma/kaizoku.db-journal | Bin 0 -> 8720 bytes .../20221014002316_init/migration.sql | 16 ---- .../20221015131156_init/migration.sql | 32 +++++++ prisma/schema.prisma | 10 +-- src/pages/manga/[id].tsx | 4 + src/server/queue/checkChapters.ts | 73 +++++++++++---- src/server/queue/download.ts | 34 +++++-- src/server/trpc/router/manga.ts | 8 +- src/server/utils/mangal.ts | 85 +++++++++++++----- yarn.lock | 79 +++++++++++++++- 12 files changed, 271 insertions(+), 81 deletions(-) create mode 100644 prisma/kaizoku.db-journal delete mode 100644 prisma/migrations/20221014002316_init/migration.sql create mode 100644 prisma/migrations/20221015131156_init/migration.sql diff --git a/next.config.mjs b/next.config.mjs index 4243b20..6df22d0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,11 @@ +import withBundleAnalyzer from '@next/bundle-analyzer'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { env } from './src/env/server.mjs'; +const bundleAnalyzer = withBundleAnalyzer({ + enabled: process.env.ANALYZE === 'true', +}); + /** * Don't be scared of the generics here. * All they do is to give us autocompletion when using this. @@ -10,7 +15,7 @@ import { env } from './src/env/server.mjs'; * @constraint {{import('next').NextConfig}} */ function defineNextConfig(config) { - return config; + return bundleAnalyzer(config); } export default defineNextConfig({ diff --git a/package.json b/package.json index 0635e49..8464bc3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "start:server": "NODE_ENV=production node dist/server/index.js", "dev": "next dev", "build:next": "next build", - "build": "npm run build:next && npm run build:server", + "build": "yarn run build:next && yarn run build:server", + "build:analyze": "ANALYZE=true yarn run build:next && ANALYZE=false yarn run build:server", "start": "next start", "lint": "next lint", "postinstall": "prisma generate" @@ -43,6 +44,7 @@ "@mantine/notifications": "^5.5.4", "@mantine/nprogress": "^5.5.4", "@mantine/spotlight": "^5.5.4", + "@next/bundle-analyzer": "^12.3.1", "@prisma/client": "4.4.0", "@tabler/icons": "^1.101.0", "@tanstack/react-query": "^4.9.0", diff --git a/prisma/kaizoku.db-journal b/prisma/kaizoku.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..e5a5aa68403fb70dd1525e91f8a90618c4758d32 GIT binary patch literal 8720 zcmeI$Jqp4=5C!0kAfgcTB7vQq?B6v6D+}451kzYZ`rv6ih@Cg^2G$@)hUQ&$Na_53))mnBYD%p8Wocmy-PllcBY@+CMo1AW$ z6Dixr5R_DkB$ZVnZBD~^xK!8Y{o#&o|0>nKe*m$YwIq%~00Izz00bZa0SG_<0uX=z M1R(HZ0yjOrKVpX|R{#J2 literal 0 HcmV?d00001 diff --git a/prisma/migrations/20221014002316_init/migration.sql b/prisma/migrations/20221014002316_init/migration.sql deleted file mode 100644 index 545de16..0000000 --- a/prisma/migrations/20221014002316_init/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ --- CreateTable -CREATE TABLE "Library" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "path" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "Manga" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "title" TEXT NOT NULL, - "cover" TEXT NOT NULL, - "interval" TEXT NOT NULL, - "source" TEXT NOT NULL, - "libraryId" INTEGER NOT NULL, - CONSTRAINT "Manga_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); diff --git a/prisma/migrations/20221015131156_init/migration.sql b/prisma/migrations/20221015131156_init/migration.sql new file mode 100644 index 0000000..07323fe --- /dev/null +++ b/prisma/migrations/20221015131156_init/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "Library" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "path" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Manga" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "cover" TEXT NOT NULL, + "interval" TEXT NOT NULL, + "source" TEXT NOT NULL, + "libraryId" INTEGER NOT NULL, + CONSTRAINT "Manga_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Chapter" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "index" INTEGER NOT NULL, + "fileName" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "mangaId" INTEGER NOT NULL, + CONSTRAINT "Chapter_mangaId_fkey" FOREIGN KEY ("mangaId") REFERENCES "Manga" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Library_path_key" ON "Library"("path"); + +-- CreateIndex +CREATE UNIQUE INDEX "Manga_title_key" ON "Manga"("title"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c4a2b0f..65f2046 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,26 +13,26 @@ datasource db { model Library { id Int @id @default(autoincrement()) - path String + path String @unique mangas Manga[] } model Manga { id Int @id @default(autoincrement()) - title String + title String @unique cover String interval String source String - Library Library @relation(fields: [libraryId], references: [id]) + library Library @relation(fields: [libraryId], references: [id]) libraryId Int - Chapter Chapter[] + chapter Chapter[] } model Chapter { id Int @id @default(autoincrement()) index Int fileName String - size BigInt + size Int manga Manga @relation(fields: [mangaId], references: [id]) mangaId Int } diff --git a/src/pages/manga/[id].tsx b/src/pages/manga/[id].tsx index e57e035..515903e 100644 --- a/src/pages/manga/[id].tsx +++ b/src/pages/manga/[id].tsx @@ -19,5 +19,9 @@ export default function LibraryPage() { return ; } + if (mangaQuery.isError) { + router.push('/404'); + } + return {JSON.stringify(mangaQuery.data, null, 2)}; } diff --git a/src/server/queue/checkChapters.ts b/src/server/queue/checkChapters.ts index 69dc691..f63570c 100644 --- a/src/server/queue/checkChapters.ts +++ b/src/server/queue/checkChapters.ts @@ -1,9 +1,9 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, PrismaClient } from '@prisma/client'; import { Job, Queue, Worker } from 'bullmq'; import path from 'path'; import { logger } from '../../utils/logging'; import { sanitizer } from '../../utils/sanitize'; -import { findMissingChapters } from '../utils/mangal'; +import { findMissingChapterFiles, getChaptersFromLocal } from '../utils/mangal'; import { downloadQueue } from './download'; const cronMap = { @@ -13,26 +13,65 @@ const cronMap = { weekly: '0 * * * 7', }; +const prisma = new PrismaClient(); + const mangaWithLibrary = Prisma.validator()({ - include: { Library: true }, + include: { library: true }, }); export type MangaWithLibrary = Prisma.MangaGetPayload; const checkChapters = async (manga: MangaWithLibrary) => { logger.info(`Checking for new chapters: ${manga.title}`); - const mangaDir = path.resolve(manga.Library.path, sanitizer(manga.title)); - const missingChapters = await findMissingChapters(mangaDir, manga.source, manga.title); + const mangaDir = path.resolve(manga.library.path, sanitizer(manga.title)); + const missingChapterFiles = await findMissingChapterFiles(mangaDir, manga.source, manga.title); + + if (missingChapterFiles.length === 0) { + logger.info(`There are no missing chapter files for ${manga.title}`); + + const localChapters = await getChaptersFromLocal(mangaDir); + + await prisma.chapter.deleteMany({ + where: { + mangaId: manga.id, + index: { + notIn: localChapters.map((chapter) => chapter.index), + }, + fileName: { + notIn: localChapters.map((chapter) => chapter.fileName), + }, + }, + }); + + const dbChapters = await prisma.chapter.findMany({ + where: { + mangaId: manga.id, + }, + }); + + const missingDbChapters = localChapters.filter( + (localChapter) => dbChapters.findIndex((dbChapter) => dbChapter.index === localChapter.index) < 0, + ); + + await Promise.all( + missingDbChapters.map(async (chapter) => { + return prisma.chapter.create({ + data: { + ...chapter, + mangaId: manga.id, + }, + }); + }), + ); - if (missingChapters.length === 0) { - logger.info(`There are no missing chapters for ${manga.title}`); - } else { - logger.info(`There are ${missingChapters.length} new chapters for ${manga.title}`); + return; } + logger.info(`There are ${missingChapterFiles.length} new chapters for ${manga.title}`); + await Promise.all( - missingChapters.map(async (chapterIndex) => { - const job = await downloadQueue.getJob(`${sanitizer(manga.title)}_${chapterIndex - 1}_download`); + missingChapterFiles.map(async (chapterIndex) => { + const job = await downloadQueue.getJob(`${sanitizer(manga.title)}_${chapterIndex}_download`); if (job) { await job.remove(); } @@ -40,16 +79,14 @@ const checkChapters = async (manga: MangaWithLibrary) => { ); await downloadQueue.addBulk( - missingChapters.map((chapterIndex) => ({ + missingChapterFiles.map((chapterIndex) => ({ opts: { - jobId: `${sanitizer(manga.title)}_${chapterIndex - 1}_download`, + jobId: `${sanitizer(manga.title)}_${chapterIndex}_download`, }, - name: `${sanitizer(manga.title)}_chapter#${chapterIndex - 1}_download`, + name: `${sanitizer(manga.title)}_chapter#${chapterIndex}_download`, data: { - chapterIndex: chapterIndex - 1, - source: manga.source, - title: manga.title, - libraryPath: manga.Library.path, + manga, + chapterIndex, }, })), ); diff --git a/src/server/queue/download.ts b/src/server/queue/download.ts index 21c5be3..112893a 100644 --- a/src/server/queue/download.ts +++ b/src/server/queue/download.ts @@ -1,27 +1,43 @@ +import { PrismaClient } from '@prisma/client'; import { Job, Queue, Worker } from 'bullmq'; -import { downloadChapter } from '../utils/mangal'; +import { downloadChapter, getChapterFromLocal } from '../utils/mangal'; import { sendNotification } from '../utils/notification'; +import type { MangaWithLibrary } from './checkChapters'; interface IDownloadWorkerData { + manga: MangaWithLibrary; chapterIndex: number; - source: string; - query: string; - title: string; - libraryPath: string; } +const prisma = new PrismaClient(); + export const downloadWorker = new Worker( 'downloadQueue', async (job: Job) => { - const { chapterIndex, libraryPath, title, source }: IDownloadWorkerData = job.data; + const { chapterIndex, manga }: IDownloadWorkerData = job.data; try { - downloadChapter(title, source, chapterIndex, libraryPath); + const filePath = await downloadChapter(manga.title, manga.source, chapterIndex, manga.library.path); + const chapter = await getChapterFromLocal(filePath); + + await prisma.chapter.deleteMany({ + where: { + mangaId: manga.id, + index: chapterIndex, + }, + }); + + await prisma.chapter.create({ + data: { + ...chapter, + mangaId: manga.id, + }, + }); + await sendNotification(`Downloaded a new chapter #${chapterIndex + 1} for ${manga.title} from ${manga.source}`); + await job.updateProgress(100); } catch (err) { await job.log(`${err}`); throw err; } - await sendNotification(`Downloaded a new chapter #${chapterIndex + 1} for ${title} from ${source}`); - await job.updateProgress(100); }, { concurrency: 5, diff --git a/src/server/trpc/router/manga.ts b/src/server/trpc/router/manga.ts index 5f4fb03..bc77af1 100644 --- a/src/server/trpc/router/manga.ts +++ b/src/server/trpc/router/manga.ts @@ -42,7 +42,7 @@ export const mangaRouter = t.router({ ) .query(async ({ input, ctx }) => { const { id } = input; - return ctx.prisma.manga.findFirst({ where: { id } }); + return ctx.prisma.manga.findUniqueOrThrow({ include: { chapter: true, library: true }, where: { id } }); }), search: t.procedure .input( @@ -70,13 +70,13 @@ export const mangaRouter = t.router({ const { id } = input; const removed = await ctx.prisma.manga.delete({ include: { - Library: true, + library: true, }, where: { id, }, }); - const mangaPath = path.resolve(removed.Library.path, sanitizer(removed.title)); + const mangaPath = path.resolve(removed.library.path, sanitizer(removed.title)); await removeManga(mangaPath); await removeJob(removed.title); }), @@ -119,7 +119,7 @@ export const mangaRouter = t.router({ const manga = await ctx.prisma.manga.create({ include: { - Library: true, + library: true, }, data: { cover: detail.Metadata.Cover, diff --git a/src/server/utils/mangal.ts b/src/server/utils/mangal.ts index 829ef81..230a67b 100644 --- a/src/server/utils/mangal.ts +++ b/src/server/utils/mangal.ts @@ -1,12 +1,14 @@ import execa from 'execa'; import fs from 'fs/promises'; +import path from 'path'; import { logger } from '../../utils/logging'; +import { sanitizer } from '../../utils/sanitize'; -export interface IOutput { +interface IOutput { Manga: Manga[]; } -export interface Manga { +interface Manga { Name: string; URL: string; Index: number; @@ -15,7 +17,7 @@ export interface Manga { Metadata: Metadata; } -export interface Chapter { +interface Chapter { Name: string; URL: string; Index: number; @@ -23,7 +25,7 @@ export interface Chapter { Volume: string; } -export interface Metadata { +interface Metadata { Genres: string[]; Summary: string; Author: string; @@ -37,12 +39,18 @@ export interface Metadata { URLs: string[]; } -export interface MangaDate { +interface MangaDate { Year: number; Month: number; Day: number; } +interface ChapterFile { + index: number; + size: number; + fileName: string; +} + export const getAvailableSources = async () => { try { const { stdout, command } = await execa('mangal', ['sources', '-r']); @@ -55,6 +63,8 @@ export const getAvailableSources = async () => { return []; }; +export const getMangaPath = (libraryPath: string, title: string) => path.resolve(libraryPath, sanitizer(title)); + export const search = async (source: string, keyword: string): Promise => { try { const { stdout, command } = await execa('mangal', ['inline', '--source', source, '--query', keyword, '-j']); @@ -69,7 +79,7 @@ export const search = async (source: string, keyword: string): Promise }; }; -export const getChapters = async (source: string, title: string): Promise => { +export const getChaptersFromRemote = async (source: string, title: string): Promise => { try { const { stdout, command } = await execa('mangal', [ 'inline', @@ -86,7 +96,7 @@ export const getChapters = async (source: string, title: string): Promise 0) { - return result.Manga[0].Chapters.map((c) => c.Index); + return result.Manga[0].Chapters.map((c) => c.Index - 1); } } catch (err) { logger.error(err); @@ -119,7 +129,12 @@ export const getMangaDetail = async (source: string, title: string) => { return undefined; }; -export const downloadChapter = async (title: string, source: string, chapterIndex: number, libraryPath: string) => { +export const downloadChapter = async ( + title: string, + source: string, + chapterIndex: number, + libraryPath: string, +): Promise => { try { logger.info(`Downloading chapter #${chapterIndex} for ${title} from ${source}`); const { stdout, stderr, command } = await execa( @@ -138,34 +153,56 @@ export const downloadChapter = async (title: string, source: string, chapterInde } else { logger.info(`Downloaded chapter #${chapterIndex} for ${title}. Result:\n${stdout}`); } + return stdout.trim(); } catch (err) { logger.error(`Failed to download the chapter #${chapterIndex} for ${title}. Err:\n${err}`); throw err; } }; -export const findMissingChapters = async (mangaDir: string, source: string, title: string) => { +const getChapterIndexFromFile = (chapterFile: string) => { + const indexRegexp = /.*?\[(\d+)\].*/; + const match = indexRegexp.exec(path.basename(chapterFile)); + if (!match || match.length < 2 || !match[1]) { + return undefined; + } + return parseInt(match[1], 10) - 1; +}; + +const shouldIncludeFile = (chapterFile: string) => { + return path.extname(chapterFile) === '.cbz' && getChapterIndexFromFile(chapterFile) !== undefined; +}; + +export const getChapterFromLocal = async (chapterFile: string) => { + const stat = await fs.stat(chapterFile); + return { + index: getChapterIndexFromFile(chapterFile)!, + size: stat.size, + fileName: path.basename(chapterFile), + }; +}; + +export const getChaptersFromLocal = async (mangaDir: string): Promise => { + await fs.mkdir(mangaDir, { recursive: true }); + const chapters = await fs.readdir(mangaDir); + + return Promise.all( + chapters.filter(shouldIncludeFile).map((chapter) => getChapterFromLocal(path.resolve(mangaDir, chapter))), + ); +}; + +export const findMissingChapterFiles = async (mangaDir: string, source: string, title: string) => { const sources = await getAvailableSources(); if (sources.indexOf(source) < 0) { logger.error(`Specified source: ${source} is not installed.`); throw new Error(); } await fs.mkdir(mangaDir, { recursive: true }); - const titleFiles = await fs.readdir(mangaDir); - - const localChapters = titleFiles - .filter((chapter) => chapter.endsWith('cbz')) - .map((chapter) => { - const indexRegexp = /.*?\[(\d+)\].*/; - const match = indexRegexp.exec(chapter); - if (!match || match.length < 2 || !match[1]) { - return 1; - } - return parseInt(match[1], 10); - }) - .filter((index) => index !== -1); - - const remoteChapters = await getChapters(source, title); + const chapters = await fs.readdir(mangaDir); + + const localChapters = chapters.filter(shouldIncludeFile).map(getChapterIndexFromFile); + + const remoteChapters = await getChaptersFromRemote(source, title); return remoteChapters.filter((c) => !localChapters.includes(c)); }; diff --git a/yarn.lock b/yarn.lock index ff650b3..ed949bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,6 +557,13 @@ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== +"@next/bundle-analyzer@^12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.3.1.tgz#df168652c43ed0dc3e91a5108e34cdcc8fc7f05a" + integrity sha512-2f/eei0YqZZBMTs4g1+HbgHyAFH5MbI/w9wLXmE8ly9SFze2D40sRH46JcC//EFVM/TIynVBh5sxn9CVO/vtxg== + dependencies: + webpack-bundle-analyzer "4.3.0" + "@next/env@12.3.1": version "12.3.1" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260" @@ -655,6 +662,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.21" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" + integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== + "@prisma/client@4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.4.0.tgz#45f59c172dd3621ecc92d7cf9bc765d85e6c7d56" @@ -1159,7 +1171,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.1.1: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -1169,7 +1181,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.4.1, acorn@^8.8.0: +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.8.0: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== @@ -1767,6 +1779,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^9.3.0: version "9.4.1" resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" @@ -2177,6 +2194,11 @@ duplexer2@^0.1.2: dependencies: readable-stream "^2.0.2" +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3127,6 +3149,13 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -3811,7 +3840,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: +lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4024,6 +4053,11 @@ moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4320,6 +4354,11 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -5280,6 +5319,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^1.0.7: + version "1.0.19" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" + integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== + dependencies: + "@polka/url" "^1.0.0-next.20" + mrmime "^1.0.0" + totalist "^1.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -5682,6 +5730,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +totalist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" + integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -5912,6 +5965,21 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +webpack-bundle-analyzer@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz#2f3c0ca9041d5ee47fa418693cf56b4a518b578b" + integrity sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA== + dependencies: + acorn "^8.0.4" + acorn-walk "^8.0.0" + chalk "^4.1.0" + commander "^6.2.0" + gzip-size "^6.0.0" + lodash "^4.17.20" + opener "^1.5.2" + sirv "^1.0.7" + ws "^7.3.1" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -5968,6 +6036,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^7.3.1: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"