diff --git a/.env.sample b/.env.sample index 45bd9c8..25f8f88 100644 --- a/.env.sample +++ b/.env.sample @@ -2,3 +2,4 @@ QIITA_DOMAIN='qiita.com' QIITA_TOKEN='' QIITA_CLI_ITEMS_ROOT="./tmp" XDG_CONFIG_HOME="./tmp" +XDG_CACHE_HOME="./tmp/.cache" diff --git a/package.json b/package.json index c45f527..2e40768 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ }, "dependencies": { "arg": "^5.0.2", + "boxen": "^7.1.1", + "chalk": "^5.3.0", "chokidar": "^3.5.3", "debug": "^4.3.4", "dotenv": "^16.0.3", diff --git a/src/commands/index.ts b/src/commands/index.ts index 320b4e6..572baa3 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,13 +1,14 @@ +import { packageUpdateNotice } from "../lib/package-update-notice"; import { help, helpText } from "./help"; import { init } from "./init"; import { login } from "./login"; import { newArticles } from "./newArticles"; import { preview } from "./preview"; import { publish } from "./publish"; -import { version } from "./version"; import { pull } from "./pull"; +import { version } from "./version"; -export const exec = (commandName: string, commandArgs: string[]) => { +export const exec = async (commandName: string, commandArgs: string[]) => { const commands = { init, login, @@ -32,5 +33,10 @@ export const exec = (commandName: string, commandArgs: string[]) => { process.exit(1); } + const updateMessage = await packageUpdateNotice(); + if (updateMessage) { + console.log(updateMessage); + } + commands[commandName](commandArgs); }; diff --git a/src/lib/config.test.ts b/src/lib/config.test.ts index 2d67841..8bb9473 100644 --- a/src/lib/config.test.ts +++ b/src/lib/config.test.ts @@ -1,5 +1,8 @@ +import process from "node:process"; import { config } from "./config"; +jest.mock("node:process"); + const initMockFs = () => { let files: { [path: string]: string } = {}; @@ -175,6 +178,40 @@ describe("config", () => { }); }); + describe("#getCacheDataDir", () => { + beforeEach(() => { + config.load({}); + }); + + it("returns default path", () => { + expect(config.getCacheDataDir()).toEqual( + "/home/test-user/.cache/qiita-cli" + ); + }); + + describe("with XDG_CACHE_HOME environment", () => { + const xdgCacheHome = "/tmp/.cache"; + + const mockProcess = process as jest.Mocked; + const env = mockProcess.env; + beforeEach(() => { + mockProcess.env = { + ...env, + XDG_CACHE_HOME: xdgCacheHome, + }; + + config.load({}); + }); + afterEach(() => { + mockProcess.env = env; + }); + + it("returns customized path", () => { + expect(config.getCacheDataDir()).toEqual(`${xdgCacheHome}/qiita-cli`); + }); + }); + }); + describe("#getUserConfig", () => { const userConfigFilePath = "/home/test-user/qiita-articles/qiita.config.json"; diff --git a/src/lib/config.ts b/src/lib/config.ts index f7a43b6..2e3460b 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -22,8 +22,12 @@ class Config { private userConfigFilePath?: string; private userConfigDir?: string; private credential?: Credential; + private cacheDataDir?: string; + private readonly packageName: string; - constructor() {} + constructor() { + this.packageName = "qiita-cli"; + } load(options: Options) { this.credentialDir = this.resolveConfigDir(options.credentialDir); @@ -32,6 +36,7 @@ class Config { this.userConfigFilePath = this.resolveUserConfigFilePath( options.userConfigDir ); + this.cacheDataDir = this.resolveCacheDataDir(); this.credential = new Credential({ credentialDir: this.credentialDir, profile: options.profile, @@ -43,6 +48,7 @@ class Config { credentialDir: this.credentialDir, itemsRootDir: this.itemsRootDir, userConfigFilePath: this.userConfigFilePath, + cacheDataDir: this.cacheDataDir, }) ); } @@ -76,6 +82,13 @@ class Config { return this.userConfigFilePath; } + getCacheDataDir() { + if (!this.cacheDataDir) { + throw new Error("cacheDataDir is undefined"); + } + return this.cacheDataDir; + } + getCredential() { if (!this.credential) { throw new Error("credential is undefined"); @@ -109,15 +122,13 @@ class Config { } private resolveConfigDir(credentialDirPath?: string) { - const packageName = "qiita-cli"; - if (process.env.XDG_CONFIG_HOME) { const credentialDir = process.env.XDG_CONFIG_HOME; - return path.join(credentialDir, packageName); + return path.join(credentialDir, this.packageName); } if (!credentialDirPath) { const homeDir = os.homedir(); - return path.join(homeDir, ".config", packageName); + return path.join(homeDir, ".config", this.packageName); } return this.resolveFullPath(credentialDirPath); @@ -150,6 +161,12 @@ class Config { return path.join(this.resolveUserConfigDirPath(dirPath), filename); } + private resolveCacheDataDir() { + const cacheHome = + process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"); + return path.join(cacheHome, this.packageName); + } + private resolveFullPath(filePath: string) { if (path.isAbsolute(filePath)) { return filePath; diff --git a/src/lib/get-latest-package-version/fetch-package-version.ts b/src/lib/get-latest-package-version/fetch-package-version.ts new file mode 100644 index 0000000..5f1498a --- /dev/null +++ b/src/lib/get-latest-package-version/fetch-package-version.ts @@ -0,0 +1,15 @@ +import { PackageSettings } from "../package-settings"; + +export const fetchLatestPackageVersion = async () => { + try { + const response = await fetch( + `https://registry.npmjs.org/${PackageSettings.name}/latest` + ); + const json = await response.json(); + const latestVersion = json.version as string; + + return latestVersion; + } catch { + return null; + } +}; diff --git a/src/lib/get-latest-package-version/get-latest-package-version.test.ts b/src/lib/get-latest-package-version/get-latest-package-version.test.ts new file mode 100644 index 0000000..bd8e11d --- /dev/null +++ b/src/lib/get-latest-package-version/get-latest-package-version.test.ts @@ -0,0 +1,90 @@ +import * as fetchPackageVersion from "./fetch-package-version"; +import { getLatestPackageVersion } from "./get-latest-package-version"; +import type { CacheData } from "./package-version-cache"; +import * as packageVersionCache from "./package-version-cache"; + +describe("getLatestPackageVersion", () => { + const mockFetchLatestPackageVersion = jest.spyOn( + fetchPackageVersion, + "fetchLatestPackageVersion" + ); + const mockGetCacheData = jest.spyOn(packageVersionCache, "getCacheData"); + const mockSetCacheData = jest.spyOn(packageVersionCache, "setCacheData"); + const mockDateNow = jest.spyOn(Date, "now"); + + beforeEach(() => { + mockFetchLatestPackageVersion.mockReset(); + mockGetCacheData.mockReset(); + mockSetCacheData.mockReset(); + }); + + describe("when cache exists and not expired", () => { + const cacheData: CacheData = { + lastCheckedAt: new Date("2023-07-13T00:00:00.000Z").getTime(), + latestVersion: "0.0.0", + }; + + beforeEach(() => { + mockGetCacheData.mockReturnValue(cacheData); + mockDateNow.mockReturnValue( + new Date("2023-07-13T11:00:00.000Z").getTime() + ); + }); + + it("returns cached version", async () => { + expect(await getLatestPackageVersion()).toEqual("0.0.0"); + expect(mockGetCacheData).toHaveBeenCalled(); + expect(mockFetchLatestPackageVersion).not.toHaveBeenCalled(); + expect(mockDateNow).toHaveBeenCalled(); + expect(mockSetCacheData).not.toHaveBeenCalled(); + }); + }); + + describe("when cache exists but expired", () => { + const cacheData: CacheData = { + lastCheckedAt: new Date("2023-07-13T00:00:00.000Z").getTime(), + latestVersion: "0.0.0", + }; + const currentTime = new Date("2023-07-13T12:00:00.000Z").getTime(); + + beforeEach(() => { + mockGetCacheData.mockReturnValue(cacheData); + mockDateNow.mockReturnValue(currentTime); + mockFetchLatestPackageVersion.mockResolvedValue("0.0.1"); + mockSetCacheData.mockReturnValue(); + }); + + it("returns latest version and updates cache", async () => { + expect(await getLatestPackageVersion()).toEqual("0.0.1"); + expect(mockGetCacheData).toBeCalled(); + expect(mockDateNow).toHaveBeenCalled(); + expect(mockFetchLatestPackageVersion).toHaveBeenCalled(); + expect(mockSetCacheData).toHaveBeenCalledWith({ + lastCheckedAt: currentTime, + latestVersion: "0.0.1", + }); + }); + }); + + describe("when cache does not exist", () => { + const currentTime = new Date("2023-07-13T12:00:00.000Z").getTime(); + + beforeEach(() => { + mockGetCacheData.mockReturnValue(null); + mockDateNow.mockReturnValue(currentTime); + mockFetchLatestPackageVersion.mockResolvedValue("0.0.1"); + mockSetCacheData.mockReturnValue(); + }); + + it("returns latest version and updates cache", async () => { + expect(await getLatestPackageVersion()).toEqual("0.0.1"); + expect(mockGetCacheData).toBeCalled(); + expect(mockDateNow).toHaveBeenCalled(); + expect(mockFetchLatestPackageVersion).toHaveBeenCalled(); + expect(mockSetCacheData).toHaveBeenCalledWith({ + lastCheckedAt: currentTime, + latestVersion: "0.0.1", + }); + }); + }); +}); diff --git a/src/lib/get-latest-package-version/get-latest-package-version.ts b/src/lib/get-latest-package-version/get-latest-package-version.ts new file mode 100644 index 0000000..63af240 --- /dev/null +++ b/src/lib/get-latest-package-version/get-latest-package-version.ts @@ -0,0 +1,27 @@ +import { fetchLatestPackageVersion } from "./fetch-package-version"; +import { getCacheData, setCacheData } from "./package-version-cache"; + +const CACHE_EXPIRE_TIME = 1000 * 60 * 60 * 12; // 12 hours + +export const getLatestPackageVersion = async () => { + const cacheData = getCacheData(); + const now = Date.now(); + + if (cacheData) { + const { lastCheckedAt, latestVersion } = cacheData; + + if (now - lastCheckedAt < CACHE_EXPIRE_TIME) { + return latestVersion; + } + } + + const latestVersion = await fetchLatestPackageVersion(); + if (latestVersion) { + setCacheData({ + lastCheckedAt: now, + latestVersion, + }); + } + + return latestVersion; +}; diff --git a/src/lib/get-latest-package-version/index.ts b/src/lib/get-latest-package-version/index.ts new file mode 100644 index 0000000..21e5adf --- /dev/null +++ b/src/lib/get-latest-package-version/index.ts @@ -0,0 +1 @@ +export { getLatestPackageVersion } from "./get-latest-package-version"; diff --git a/src/lib/get-latest-package-version/package-version-cache.ts b/src/lib/get-latest-package-version/package-version-cache.ts new file mode 100644 index 0000000..1e4d00e --- /dev/null +++ b/src/lib/get-latest-package-version/package-version-cache.ts @@ -0,0 +1,36 @@ +import fs from "node:fs"; +import path from "node:path"; +import { config } from "../config"; + +export interface CacheData { + lastCheckedAt: number; + latestVersion: string; +} + +const CACHE_FILE_NAME = "latest-package-version"; + +const cacheFilePath = () => { + const cacheDir = config.getCacheDataDir(); + return path.join(cacheDir, CACHE_FILE_NAME); +}; + +export const getCacheData = () => { + const filePath = cacheFilePath(); + if (!fs.existsSync(filePath)) { + return null; + } + + const data = fs.readFileSync(filePath, { encoding: "utf-8" }); + return JSON.parse(data) as CacheData; +}; + +export const setCacheData = (data: CacheData) => { + const cacheDir = config.getCacheDataDir(); + const filePath = cacheFilePath(); + + fs.mkdirSync(cacheDir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data), { + encoding: "utf-8", + mode: 0o600, + }); +}; diff --git a/src/lib/package-settings.ts b/src/lib/package-settings.ts index 7d39b13..773c6c0 100644 --- a/src/lib/package-settings.ts +++ b/src/lib/package-settings.ts @@ -1,6 +1,7 @@ const packageJsonData = require("../../package.json"); export const PackageSettings = { + name: packageJsonData.name, userAgentName: "QiitaCLI", version: packageJsonData.version, }; diff --git a/src/lib/package-update-notice.ts b/src/lib/package-update-notice.ts new file mode 100644 index 0000000..67e690d --- /dev/null +++ b/src/lib/package-update-notice.ts @@ -0,0 +1,27 @@ +import { getLatestPackageVersion } from "./get-latest-package-version"; +import { PackageSettings } from "./package-settings"; + +export const packageUpdateNotice = async () => { + const currentVersion = PackageSettings.version; + const latestVersion = await getLatestPackageVersion(); + + if (!latestVersion) { + return null; + } + if (currentVersion === latestVersion) { + return null; + } + + const chalk = (await import("chalk")).default; // `chalk` supports only ESM. + const boxen = (await import("boxen")).default; // `boxen` supports only ESM. + + let message = "新しいバージョンがあります! "; + message += ` ${chalk.red(currentVersion)} -> ${chalk.green(latestVersion)}`; + message += "\n"; + message += `${chalk.green(`npm install ${PackageSettings.name}@latest`)}`; + message += " でアップデートできます!"; + + message = boxen(message, { padding: 1, margin: 1, borderStyle: "round" }); + + return message; +}; diff --git a/src/server/api/items.ts b/src/server/api/items.ts index cd1a049..dd99f30 100644 --- a/src/server/api/items.ts +++ b/src/server/api/items.ts @@ -76,7 +76,6 @@ const itemsShow = async (req: Express.Request, res: Express.Response) => { return; } - // const { data, itemPath, modified, published } = ; const { itemPath, modified, published } = item; const qiitaApi = await getQiitaApiInstance(); diff --git a/tsconfig.json b/tsconfig.json index 1032db9..7aef8eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "lib": ["es2020", "DOM"], - "module": "commonjs", + "module": "Node16", "outDir": "./dist", "rootDir": "./src", "skipLibCheck": false, diff --git a/yarn.lock b/yarn.lock index 6c24afb..ff8bdd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1289,6 +1289,13 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1301,6 +1308,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1320,6 +1332,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -1504,6 +1521,20 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== +boxen@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" + integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== + dependencies: + ansi-align "^3.0.1" + camelcase "^7.0.1" + chalk "^5.2.0" + cli-boxes "^3.0.0" + string-width "^5.1.2" + type-fest "^2.13.0" + widest-line "^4.0.1" + wrap-ansi "^8.1.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1584,6 +1615,11 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelcase@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" + integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== + caniuse-lite@^1.0.30001449: version "1.0.30001458" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz#871e35866b4654a7d25eccca86864f411825540c" @@ -1606,6 +1642,11 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.2.0, chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -1653,6 +1694,11 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1942,6 +1988,11 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -1962,6 +2013,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -4476,6 +4532,15 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" @@ -4524,6 +4589,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" @@ -4698,6 +4770,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^2.13.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -4896,6 +4973,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +widest-line@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" + integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== + dependencies: + string-width "^5.0.1" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -4910,6 +4994,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"