From 3004c642dab239b4e060f7da5c3ac12d2dc9691d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 4 May 2026 11:37:17 +0200 Subject: [PATCH] fix: Fractional index validation (#11258) - Vendored fractional-indexing and converted to TypeScript - Stricter index format validation in fractional-indexing - Added format validation to fractional index validation --- Signed-off-by: Mark Tolmacs Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- excalidraw-app/vite.config.mts | 7 + package.json | 3 +- packages/element/package.json | 3 +- packages/element/src/fractionalIndex.ts | 14 +- .../element/tests/fractionalIndex.test.ts | 66 +++- packages/excalidraw/package.json | 1 - packages/fractional-indexing/global.d.ts | 0 packages/fractional-indexing/package.json | 45 +++ packages/fractional-indexing/src/index.ts | 322 ++++++++++++++++++ packages/fractional-indexing/tsconfig.json | 8 + packages/tsconfig.base.json | 4 +- scripts/buildBase.js | 7 +- scripts/buildPackage.js | 7 +- scripts/buildUtils.js | 4 + scripts/release.js | 8 +- tsconfig.json | 2 + vitest.config.mts | 14 + yarn.lock | 10 +- 18 files changed, 508 insertions(+), 17 deletions(-) create mode 100644 packages/fractional-indexing/global.d.ts create mode 100644 packages/fractional-indexing/package.json create mode 100644 packages/fractional-indexing/src/index.ts create mode 100644 packages/fractional-indexing/tsconfig.json diff --git a/excalidraw-app/vite.config.mts b/excalidraw-app/vite.config.mts index fa8d63d956..dfb213ef3a 100644 --- a/excalidraw-app/vite.config.mts +++ b/excalidraw-app/vite.config.mts @@ -75,6 +75,13 @@ export default defineConfig(({ mode }) => { find: /^@excalidraw\/utils\/(.*?)/, replacement: path.resolve(__dirname, "../packages/utils/src/$1"), }, + { + find: /^@excalidraw\/fractional-indexing$/, + replacement: path.resolve( + __dirname, + "../packages/fractional-indexing/src/index.ts", + ), + }, ], }, build: { diff --git a/package.json b/package.json index 65ff221a49..df5c4e8a79 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "build:element": "yarn --cwd ./packages/element build:esm", "build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm", "build:math": "yarn --cwd ./packages/math build:esm", - "build:packages": "yarn build:common && yarn build:math && yarn build:element && yarn build:excalidraw", + "build:fractional-indexing": "yarn --cwd ./packages/fractional-indexing build:esm", + "build:packages": "yarn build:common && yarn build:fractional-indexing && yarn build:math && yarn build:element && yarn build:excalidraw", "build:version": "yarn --cwd ./excalidraw-app build:version", "build": "yarn --cwd ./excalidraw-app build", "build:preview": "yarn --cwd ./excalidraw-app build:preview", diff --git a/packages/element/package.json b/packages/element/package.json index 7dff00a750..408f2c9d10 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -64,6 +64,7 @@ }, "dependencies": { "@excalidraw/common": "0.18.0", - "@excalidraw/math": "0.18.0" + "@excalidraw/math": "0.18.0", + "@excalidraw/fractional-indexing": "3.3.0" } } diff --git a/packages/element/src/fractionalIndex.ts b/packages/element/src/fractionalIndex.ts index 44ca523c80..90a2e7c217 100644 --- a/packages/element/src/fractionalIndex.ts +++ b/packages/element/src/fractionalIndex.ts @@ -1,7 +1,10 @@ -import { generateNKeysBetween } from "fractional-indexing"; - import { arrayToMap } from "@excalidraw/common"; +import { + validateOrderKey, + generateNKeysBetween, +} from "@excalidraw/fractional-indexing"; + import { mutateElement, newElementWith } from "./mutateElement"; import { getBoundTextElement } from "./textElement"; import { hasBoundTextElement } from "./typeChecks"; @@ -382,6 +385,13 @@ const isValidFractionalIndex = ( return false; } + try { + // Format validation + validateOrderKey(index); + } catch { + return false; + } + if (predecessor && successor) { return predecessor < index && index < successor; } diff --git a/packages/element/tests/fractionalIndex.test.ts b/packages/element/tests/fractionalIndex.test.ts index 1cc3ca5af3..2834a831e1 100644 --- a/packages/element/tests/fractionalIndex.test.ts +++ b/packages/element/tests/fractionalIndex.test.ts @@ -1,9 +1,8 @@ /* eslint-disable no-lone-blocks */ -import { generateKeyBetween } from "fractional-indexing"; - import { arrayToMap } from "@excalidraw/common"; import { + InvalidFractionalIndexError, syncInvalidIndices, syncMovedIndices, validateFractionalIndices, @@ -13,13 +12,34 @@ import { deepCopyElement } from "@excalidraw/element"; import { API } from "@excalidraw/excalidraw/tests/helpers/api"; +import { + generateKeyBetween, + validateOrderKey, +} from "@excalidraw/fractional-indexing"; + import type { ElementsMap, ExcalidrawElement, FractionalIndex, } from "@excalidraw/element/types"; -import { InvalidFractionalIndexError } from "../src/fractionalIndex"; +describe("fractional index format validation", () => { + it("should reject malformed base62 order keys", () => { + expect(() => validateOrderKey("a!")).toThrow(); + expect(() => validateOrderKey("a_")).toThrow(); + expect(() => validateOrderKey("a1!")).toThrow(); + expect(() => validateOrderKey("a1_")).toThrow(); + expect(() => validateOrderKey("zd0032")).toThrow(); + }); + + it("should accept valid base62 order keys", () => { + expect(() => validateOrderKey("Zz")).not.toThrow(); + expect(() => validateOrderKey("a0")).not.toThrow(); + expect(() => validateOrderKey("a1")).not.toThrow(); + expect(() => validateOrderKey("a1V")).not.toThrow(); + expect(() => validateOrderKey("z".padEnd(28, "z"))).not.toThrow(); + }); +}); describe("sync invalid indices with array order", () => { describe("should NOT sync empty array", () => { @@ -104,6 +124,46 @@ describe("sync invalid indices with array order", () => { }); }); + describe("should sync when fractional index is malformed", () => { + // "zd0032" has head "z" which requires length 28 per getIntegerLength, + // but the string is far too short, so validateOrderKey throws for it + testInvalidIndicesSync({ + elements: [{ id: "A", index: "zd0032" }], + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "zd0032" }, + { id: "C", index: "a3" }, + ], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testInvalidIndicesSync({ + elements: [{ id: "A", index: "a!" }], + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a!" }, + { id: "C", index: "a2" }, + ], + expect: { + unchangedElements: ["A", "C"], + }, + }); + }); + describe("should sync when fractional indices are duplicated", () => { testInvalidIndicesSync({ elements: [ diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index 1d01b82bd8..d2f3d9f0b2 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -95,7 +95,6 @@ "clsx": "1.1.1", "cross-env": "7.0.3", "es6-promise-pool": "2.5.0", - "fractional-indexing": "3.2.0", "fuzzy": "0.1.3", "image-blob-reduce": "3.0.1", "jotai": "2.11.0", diff --git a/packages/fractional-indexing/global.d.ts b/packages/fractional-indexing/global.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/fractional-indexing/package.json b/packages/fractional-indexing/package.json new file mode 100644 index 0000000000..36082678e3 --- /dev/null +++ b/packages/fractional-indexing/package.json @@ -0,0 +1,45 @@ +{ + "name": "@excalidraw/fractional-indexing", + "version": "3.3.0", + "description": "Provides functions for generating ordering strings", + "type": "module", + "types": "./dist/types/fractional-indexing/src/index.d.ts", + "main": "./dist/prod/index.js", + "module": "./dist/prod/index.js", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "scripts": { + "gen:types": "rimraf types && tsc", + "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types" + }, + "keywords": [ + "fractional", + "indexing", + "ordering", + "order" + ], + "homepage": "https://github.com/rocicorp/fractional-indexing#readme", + "bugs": "https://github.com/excalidraw/excalidraw/issues", + "repository": "https://github.com/excalidraw/excalidraw", + "author": "arv@rocicorp.dev", + "license": "CC0-1.0", + "devDependencies": { + "prettier": "^2.6.0", + "typescript": "5.9.3" + }, + "exports": { + ".": { + "types": "./dist/types/fractional-indexing/src/index.d.ts", + "development": "./dist/dev/index.js", + "production": "./dist/prod/index.js", + "default": "./dist/prod/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/*" + ] +} diff --git a/packages/fractional-indexing/src/index.ts b/packages/fractional-indexing/src/index.ts new file mode 100644 index 0000000000..9f3bf31a6a --- /dev/null +++ b/packages/fractional-indexing/src/index.ts @@ -0,0 +1,322 @@ +// Vendored from https://www.npmjs.com/package/fractional-indexing +// License: CC0 (no rights reserved). +// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing + +export const BASE_62_DIGITS = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +// `a` may be empty string, `b` is null or non-empty string. +// `a < b` lexicographically if `b` is non-null. +// no trailing zeros allowed. +// digits is a string such as '0123456789' for base 10. Digits must be in +// ascending character code order! +/** + * @param {string} a + * @param {string | null | undefined} b + * @param {string} digits + * @returns {string} + */ +function midpoint( + a: string, + b: string | null | undefined, + digits: string, +): string { + const zero = digits[0]; + if (b != null && a >= b) { + throw new Error(`${a} >= ${b}`); + } + if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) { + throw new Error("trailing zero"); + } + if (b) { + // remove longest common prefix. pad `a` with 0s as we + // go. note that we don't need to pad `b`, because it can't + // end before `a` while traversing the common prefix. + let n = 0; + while ((a[n] || zero) === b[n]) { + n++; + } + if (n > 0) { + return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits); + } + } + // first digits (or lack of digit) are different + const digitA = a ? digits.indexOf(a[0]) : 0; + const digitB = b != null ? digits.indexOf(b[0]) : digits.length; + if (digitB - digitA > 1) { + const midDigit = Math.round(0.5 * (digitA + digitB)); + return digits[midDigit]; + } + // first digits are consecutive + if (b && b.length > 1) { + return b.slice(0, 1); + } + + // `b` is null or has length 1 (a single digit). + // the first digit of `a` is the previous digit to `b`, + // or 9 if `b` is null. + // given, for example, midpoint('49', '5'), return + // '4' + midpoint('9', null), which will become + // '4' + '9' + midpoint('', null), which is '495' + return digits[digitA] + midpoint(a.slice(1), null, digits); +} + +/** + * @param {string} int + * @return {void} + */ + +function validateInteger(int: string): void { + if (int.length !== getIntegerLength(int[0])) { + throw new Error(`invalid integer part of order key: ${int}`); + } +} + +/** + * @param {string} head + * @return {number} + */ + +function getIntegerLength(head: string): number { + if (head >= "a" && head <= "z") { + return head.charCodeAt(0) - "a".charCodeAt(0) + 2; + } else if (head >= "A" && head <= "Z") { + return "Z".charCodeAt(0) - head.charCodeAt(0) + 2; + } + + throw new Error(`invalid order key head: ${head}`); +} + +/** + * @param {string} key + * @return {string} + */ + +function getIntegerPart(key: string): string { + const integerPartLength = getIntegerLength(key[0]); + + if (integerPartLength > key.length) { + throw new Error(`invalid order key: ${key}`); + } + return key.slice(0, integerPartLength); +} + +/** + * @param {string} key + * @param {string} digits + * @return {void} + */ +export function validateOrderKey( + key: string, + digits: string = BASE_62_DIGITS, +): void { + const validChars = key.split("").every((char) => digits.includes(char)); + if (key === `A${digits[0].repeat(26)}` || !validChars) { + throw new Error(`invalid order key: ${key}`); + } + // getIntegerPart will throw if the first character is bad, + // or the key is too short. we'd call it to check these things + // even if we didn't need the result + const i = getIntegerPart(key); + const f = key.slice(i.length); + if (f.slice(-1) === digits[0]) { + throw new Error(`invalid order key: ${key}`); + } +} + +// note that this may return null, as there is a largest integer +/** + * @param {string} x + * @param {string} digits + * @return {string | null} + */ +function incrementInteger(x: string, digits: string): string | null { + validateInteger(x); + const [head, ...digs] = x.split(""); + let carry = true; + for (let i = digs.length - 1; carry && i >= 0; i--) { + const d = digits.indexOf(digs[i]) + 1; + if (d === digits.length) { + digs[i] = digits[0]; + } else { + digs[i] = digits[d]; + carry = false; + } + } + if (carry) { + if (head === "Z") { + return `a${digits[0]}`; + } + if (head === "z") { + return null; + } + const h = String.fromCharCode(head.charCodeAt(0) + 1); + if (h > "a") { + digs.push(digits[0]); + } else { + digs.pop(); + } + return h + digs.join(""); + } + return head + digs.join(""); +} + +// note that this may return null, as there is a smallest integer +/** + * @param {string} x + * @param {string} digits + * @return {string | null} + */ +function decrementInteger(x: string, digits: string): string | null { + validateInteger(x); + const [head, ...digs] = x.split(""); + let borrow = true; + for (let i = digs.length - 1; borrow && i >= 0; i--) { + const d = digits.indexOf(digs[i]) - 1; + if (d === -1) { + digs[i] = digits.slice(-1); + } else { + digs[i] = digits[d]; + borrow = false; + } + } + if (borrow) { + if (head === "a") { + return `Z${digits.slice(-1)}`; + } + if (head === "A") { + return null; + } + const h = String.fromCharCode(head.charCodeAt(0) - 1); + if (h < "Z") { + digs.push(digits.slice(-1)); + } else { + digs.pop(); + } + return h + digs.join(""); + } + return head + digs.join(""); +} + +// `a` is an order key or null (START). +// `b` is an order key or null (END). +// `a < b` lexicographically if both are non-null. +// digits is a string such as '0123456789' for base 10. Digits must be in +// ascending character code order! +/** + * @param {string | null | undefined} a + * @param {string | null | undefined} b + * @param {string=} digits + * @return {string} + */ +export function generateKeyBetween( + a: string | null | undefined, + b: string | null | undefined, + digits = BASE_62_DIGITS, +): string { + if (a != null) { + validateOrderKey(a, digits); + } + if (b != null) { + validateOrderKey(b, digits); + } + if (a != null && b != null && a >= b) { + throw new Error(`${a} >= ${b}`); + } + if (a == null) { + if (b == null) { + return `a${digits[0]}`; + } + + const ib = getIntegerPart(b); + const fb = b.slice(ib.length); + if (ib === `A${digits[0].repeat(26)}`) { + return ib + midpoint("", fb, digits); + } + if (ib < b) { + return ib; + } + const res = decrementInteger(ib, digits); + if (res == null) { + throw new Error("cannot decrement any more"); + } + return res; + } + + if (b == null) { + const ia = getIntegerPart(a); + const fa = a.slice(ia.length); + const i = incrementInteger(ia, digits); + return i == null ? ia + midpoint(fa, null, digits) : i; + } + + const ia = getIntegerPart(a); + const fa = a.slice(ia.length); + const ib = getIntegerPart(b); + const fb = b.slice(ib.length); + if (ia === ib) { + return ia + midpoint(fa, fb, digits); + } + const i = incrementInteger(ia, digits); + if (i == null) { + throw new Error("cannot increment any more"); + } + if (i < b) { + return i; + } + return ia + midpoint(fa, null, digits); +} + +/** + * same preconditions as generateKeysBetween. + * n >= 0. + * Returns an array of n distinct keys in sorted order. + * If a and b are both null, returns [a0, a1, ...] + * If one or the other is null, returns consecutive "integer" + * keys. Otherwise, returns relatively short keys between + * a and b. + * @param {string | null | undefined} a + * @param {string | null | undefined} b + * @param {number} n + * @param {string} digits + * @return {string[]} + */ +export function generateNKeysBetween( + a: string | null | undefined, + b: string | null | undefined, + n: number, + digits = BASE_62_DIGITS, +): string[] { + if (n === 0) { + return []; + } + if (n === 1) { + return [generateKeyBetween(a, b, digits)]; + } + if (b == null) { + let c = generateKeyBetween(a, b, digits); + const result = [c]; + for (let i = 0; i < n - 1; i++) { + c = generateKeyBetween(c, b, digits); + result.push(c); + } + return result; + } + if (a == null) { + let c = generateKeyBetween(a, b, digits); + const result = [c]; + for (let i = 0; i < n - 1; i++) { + c = generateKeyBetween(a, c, digits); + result.push(c); + } + result.reverse(); + return result; + } + const mid = Math.floor(n / 2); + const c = generateKeyBetween(a, b, digits); + return [ + ...generateNKeysBetween(a, c, mid, digits), + c, + ...generateNKeysBetween(c, b, n - mid - 1, digits), + ]; +} diff --git a/packages/fractional-indexing/tsconfig.json b/packages/fractional-indexing/tsconfig.json new file mode 100644 index 0000000000..6450145b1c --- /dev/null +++ b/packages/fractional-indexing/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/types" + }, + "include": ["src/**/*", "global.d.ts"], + "exclude": ["**/*.test.*", "tests", "types", "examples", "dist"] +} diff --git a/packages/tsconfig.base.json b/packages/tsconfig.base.json index b1d145ad87..1bd75695b5 100644 --- a/packages/tsconfig.base.json +++ b/packages/tsconfig.base.json @@ -20,7 +20,9 @@ "@excalidraw/math": ["./math/src/index.ts"], "@excalidraw/math/*": ["./math/src/*"], "@excalidraw/utils": ["./utils/src/index.ts"], - "@excalidraw/utils/*": ["./utils/src/*"] + "@excalidraw/utils/*": ["./utils/src/*"], + "@excalidraw/fractional-indexing": ["./fractional-indexing/src/index.ts"], + "@excalidraw/fractional-indexing/*": ["./fractional-indexing/src/*"] } } } diff --git a/scripts/buildBase.js b/scripts/buildBase.js index 25c0874f28..0b82b0a488 100644 --- a/scripts/buildBase.js +++ b/scripts/buildBase.js @@ -13,7 +13,12 @@ const getConfig = (outdir) => ({ alias: { "@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"), }, - external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"], + external: [ + "@excalidraw/common", + "@excalidraw/element", + "@excalidraw/math", + "@excalidraw/fractional-indexing", + ], }); function buildDev(config) { diff --git a/scripts/buildPackage.js b/scripts/buildPackage.js index 96c97cbbd5..839e135855 100644 --- a/scripts/buildPackage.js +++ b/scripts/buildPackage.js @@ -74,7 +74,12 @@ const getConfig = (outdir) => ({ alias: { "@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"), }, - external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"], + external: [ + "@excalidraw/common", + "@excalidraw/element", + "@excalidraw/math", + "@excalidraw/fractional-indexing", + ], loader: { ".woff2": "file", }, diff --git a/scripts/buildUtils.js b/scripts/buildUtils.js index 1cf3ffbaaf..24d9e0a7c0 100644 --- a/scripts/buildUtils.js +++ b/scripts/buildUtils.js @@ -18,6 +18,10 @@ const getConfig = (outdir) => ({ "@excalidraw/element": path.resolve(__dirname, "../packages/element/src"), "@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"), "@excalidraw/math": path.resolve(__dirname, "../packages/math/src"), + "@excalidraw/fractional-indexing": path.resolve( + __dirname, + "../packages/fractional-indexing/src", + ), "@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"), }, }); diff --git a/scripts/release.js b/scripts/release.js index f45afc66d2..b6f3ca5c87 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -6,7 +6,13 @@ const { execSync } = require("child_process"); const updateChangelog = require("./updateChangelog"); // skipping utils for now, as it has independent release process -const PACKAGES = ["common", "math", "element", "excalidraw"]; +const PACKAGES = [ + "common", + "fractional-indexing", + "math", + "element", + "excalidraw", +]; const PACKAGES_DIR = path.resolve(__dirname, "../packages"); /** diff --git a/tsconfig.json b/tsconfig.json index 45a29dd618..5948a866aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,8 @@ "@excalidraw/excalidraw/*": ["./packages/excalidraw/*"], "@excalidraw/element": ["./packages/element/src/index.ts"], "@excalidraw/element/*": ["./packages/element/src/*"], + "@excalidraw/fractional-indexing": ["./packages/fractional-indexing/src/index.ts"], + "@excalidraw/fractional-indexing/*": ["./packages/fractional-indexing/src/*"], "@excalidraw/math": ["./packages/math/src/index.ts"], "@excalidraw/math/*": ["./packages/math/src/*"], "@excalidraw/utils": ["./packages/utils/src/index.ts"], diff --git a/vitest.config.mts b/vitest.config.mts index 353f84ccfc..7c45c43f25 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -45,6 +45,20 @@ export default defineConfig({ find: /^@excalidraw\/utils\/(.*?)/, replacement: path.resolve(__dirname, "./packages/utils/src/$1"), }, + { + find: /^@excalidraw\/fractional-indexing$/, + replacement: path.resolve( + __dirname, + "./packages/fractional-indexing/src/index.ts", + ), + }, + { + find: /^@excalidraw\/fractional-indexing\/(.*?)/, + replacement: path.resolve( + __dirname, + "./packages/fractional-indexing/src/$1", + ), + }, ], }, //@ts-ignore diff --git a/yarn.lock b/yarn.lock index dd87b52426..09381a0264 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6572,11 +6572,6 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== -fractional-indexing@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fractional-indexing/-/fractional-indexing-3.2.0.tgz#1193e63d54ff4e0cbe0c79a9ed6cfbab25d91628" - integrity sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ== - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -8525,6 +8520,11 @@ prettier@2.6.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== +prettier@^2.6.0: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"