From ae74c7c2ccd88b5d3b1e5d700f2001ec23931177 Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Mon, 6 Dec 2021 18:49:25 -0500 Subject: [PATCH] feat: add Flag and Feature components (#9) BREAKING CHANGE: rename `useFlagQuery` to `useFlagQueryFn` --- README.md | 81 +++++++++++++++++++++--- package.json | 5 +- src/index.tsx | 151 +++++++++++++++++++++++++++++--------------- test/index.spec.tsx | 137 +++++++++++++++++++++++++++------------- yarn.lock | 113 +++++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index a877cdd..4f12c24 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,14 @@ It could be the feature slug or an flag queries array or more powerful, an objec type FlagQuery = | string | FlagQuery[] + | { + $or: FlagQuery[] + } + | { + $and: FlagQuery[] + } | { [slug: string]: boolean - [operator: symbol]: FlagQuery[] } ``` @@ -202,14 +207,14 @@ function App() { } ``` -### `useFlagQuery` +### `useFlagQueryFn` Hook that is used to get the magic function that can process a _flag query_. #### Specification ```ts -interface UseFlagQuery { +interface UseFlagQueryFn { (): (query: FlagQuery) => boolean } ``` @@ -217,14 +222,14 @@ interface UseFlagQuery { #### Example ```tsx -import { useFlagQuery } from 'toggled' +import { useFlagQueryFn } from 'toggled' export default function App() { - const flagQuery = useFlagQuery() + const flagQueryFn = useFlagQueryFn() return ( - - {flagQuery('chat') && } + + {flagQueryFn('chat') && } ) } @@ -236,7 +241,7 @@ export default function App() { Hook that is used to get a binary output based on the existence of a feature in the context. So, if the feature is in the context then the flag will be `true`, otherwise `false`. -> The `useFlagQuery` hook is used internally. +> The `useFlagQueryFn` hook is used internally. #### Specification @@ -264,6 +269,66 @@ export default function App() { } ``` +### `` + +Component to apply conditional rendering using a `flagQuery` + +#### Specification + +```ts +interface FlagProps { + flagQuery: FlagQuery + children: React.ReactNode +} +``` + +#### Example + +```tsx +import { Flag } from 'toggled' + +export default function App() { + return ( + + + + + + + + ) +} +``` + +### `` + +Component to consume a feature object declaratively instead of `useFeature` + +#### Specification + +```ts +export interface FeatureProps { + slug: string + children(feature: DefaultFeature): React.ReactElement +} +``` + +#### Example + +```tsx +import { Feature } from 'toggled' + +export default function App() { + return ( + + {feature => { + return + }} + + ) +} +``` + ## License MIT © [Ricardo Q. Bazan](https://rcrd.space) diff --git a/package.json b/package.json index 5cbee2c..0b4431b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "devDependencies": { "@size-limit/preset-small-lib": "^4.11.0", + "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^7.0.2", "@types/react": "^17.0.34", "@types/react-dom": "^17.0.11", @@ -81,11 +82,11 @@ "size-limit": [ { "path": "dist/toggled.cjs.production.min.js", - "limit": "500 B" + "limit": "600 B" }, { "path": "dist/toggled.esm.js", - "limit": "550 B" + "limit": "650 B" } ], "resolutions": { diff --git a/src/index.tsx b/src/index.tsx index b9dec5a..14db10e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,46 +4,64 @@ export interface DefaultFeature { slug: string } -export interface FeatureProviderProps { - features: F[] - children: React.ReactNode -} +export type TCache = Record + +export type FeatureProviderProps = + | { + cache: TCache + children: React.ReactNode + } + | { + features: DefaultFeature[] + children: React.ReactNode + } -export interface FeatureContextValue { - cache: Map +export interface FeatureContextValue { + cache: TCache } export const Op = { - OR: Symbol('$or'), - AND: Symbol('$and'), + OR: '$or' as const, + AND: '$and' as const, } export type FlagQuery = | string | FlagQuery[] + | { + [Op.OR]: FlagQuery[] + } + | { + [Op.AND]: FlagQuery[] + } | { [slug: string]: boolean - [operator: symbol]: FlagQuery[] } -const NO_PROVIDER = Symbol('No provider') +const NO_PROVIDER = '__toggled_no_provder_id__' // @ts-ignore export const FeatureContext = React.createContext(NO_PROVIDER) -export function FeatureProvider(props: FeatureProviderProps) { - const { features, children } = props +export function createCache(features: F[]) { + return features.reduce((cache, current) => { + cache[current.slug] = current + return cache + }, {} as Record) +} - const contextValue = React.useMemo(() => { - const cache = new Map(features.map(feature => [feature.slug, feature])) +export function FeatureProvider(props: FeatureProviderProps) { + // @ts-expect-error + const { features, cache, children } = props - return { cache } - }, [features]) + const contextValue = React.useMemo(() => { + return { cache: cache ?? createCache(features) } + }, [cache, features]) return {children} } -function useFFContext() { +function useToggledContext() { const contextValue = React.useContext(FeatureContext) // @ts-ignore @@ -55,22 +73,20 @@ function useFFContext() { } export function useFeature(slug: string) { - const { cache } = useFFContext() + const { cache } = useToggledContext() - return cache.get(slug) + return cache[slug] } -export function useFlagQuery() { - const { cache } = useFFContext() - - return function fn(flagQuery: FlagQuery) { +export function createFlagQueryFn(cache: Record) { + return function flagQueryFn(flagQuery: FlagQuery) { if (typeof flagQuery === 'string') { - return cache.has(flagQuery) + return Boolean(cache[flagQuery]) } if (Array.isArray(flagQuery)) { for (const query of flagQuery) { - if (!fn(query)) { + if (!flagQueryFn(query)) { return false } } @@ -78,33 +94,32 @@ export function useFlagQuery() { return true } - for (let key of Reflect.ownKeys(flagQuery)) { + for (let key in flagQuery) { let phase: boolean - if (typeof key === 'string') { - phase = cache.has(key) === flagQuery[key] - } else { - if (key === Op.OR) { - phase = false - - for (const innerQuery of flagQuery[key]) { - if (fn(innerQuery)) { - phase = true - break - } + if (key === Op.OR) { + phase = false + + // @ts-expect-error + for (const innerQuery of flagQuery[key]) { + if (flagQueryFn(innerQuery)) { + phase = true + break } - } else if (key === Op.AND) { - phase = true - - for (const innerQuery of flagQuery[key]) { - if (!fn(innerQuery)) { - phase = false - break - } + } + } else if (key === Op.AND) { + phase = true + + // @ts-expect-error + for (const innerQuery of flagQuery[key]) { + if (!flagQueryFn(innerQuery)) { + phase = false + break } - } else { - throw Error('Invalid Operator') } + } else { + // @ts-expect-error + phase = Boolean(cache[key]) === flagQuery[key] } if (!phase) { @@ -116,8 +131,44 @@ export function useFlagQuery() { } } -export function useFlag(query: FlagQuery) { - const flagQuery = useFlagQuery() +export function useFlagQueryFn() { + const { cache } = useToggledContext() + + return React.useMemo(() => createFlagQueryFn(cache), [cache]) +} + +export function useFlag(flagQuery: FlagQuery) { + const flagQueryFn = useFlagQueryFn() + + return flagQueryFn(flagQuery) +} + +export interface FlagProps { + flagQuery: FlagQuery + children: React.ReactNode +} + +export function Flag({ flagQuery, children }: FlagProps) { + const enabled = useFlag(flagQuery) + + if (!enabled) { + return null + } + + return children as React.ReactElement +} + +export interface FeatureProps { + slug: string + children(feature: DefaultFeature): React.ReactElement +} + +export function Feature({ slug, children }: FeatureProps) { + const feature = useFeature(slug) + + if (!feature) { + return null + } - return flagQuery(query) + return children(feature) } diff --git a/test/index.spec.tsx b/test/index.spec.tsx index e0cfb7e..fc83650 100644 --- a/test/index.spec.tsx +++ b/test/index.spec.tsx @@ -1,32 +1,37 @@ import * as React from 'react' -import { renderHook, RenderHookOptions } from '@testing-library/react-hooks' -import { FeatureProvider, useFeature, useFlag, useFlagQuery, FeatureProviderProps, Op } from '../src' +import { renderHook as _renderHook, RenderHookOptions } from '@testing-library/react-hooks' +import { render as _render, RenderOptions } from '@testing-library/react' +import { FeatureProvider, useFeature, useFlag, useFlagQueryFn, FeatureProviderProps, Op, Flag, Feature } from '../src' import features from './fixtures/features.json' const messages = { NO_PROVIDER: 'Component must be wrapped with FeatureProvider.', } -function render( +const wrapper = ({ children }: Partial) => ( + {children} +) + +function renderHook( callback: (props: FeatureProviderProps) => TResult, options?: RenderHookOptions, ) { - const wrapper = ({ children }: FeatureProviderProps) => ( - {children} - ) + return _renderHook(callback, { wrapper, ...options }) +} - return renderHook(callback, { wrapper, ...options }) +function render(ui: React.ReactElement, options?: RenderOptions) { + return _render(ui, { wrapper, ...options }) } -function renderFlagQuery() { - const original = render(() => useFlagQuery()) +function renderFlagQueryFn() { + const original = renderHook(() => useFlagQueryFn()) - return { flagQuery: original.result.current, original } + return { flagQueryFn: original.result.current, original } } describe('useFeature', () => { it('gets the feature when it is in the context', () => { - const { result } = render(() => useFeature('example-1')) + const { result } = renderHook(() => useFeature('example-1')) expect(result.current).toMatchObject({ slug: 'example-1', @@ -35,13 +40,13 @@ describe('useFeature', () => { }) it('gets `undefined` when it is not in the context', () => { - const { result } = render(() => useFeature('xyz')) + const { result } = renderHook(() => useFeature('xyz')) expect(result.current).toBeUndefined() }) - it('crashs when the provider is not wrapper', () => { - const { result } = renderHook(() => useFeature('example-1')) + it('crashes when the provider is not wrapper', () => { + const { result } = _renderHook(() => useFeature('example-1')) expect(result.error?.message).toEqual(messages.NO_PROVIDER) }) @@ -49,94 +54,94 @@ describe('useFeature', () => { describe('useFlag', () => { it('gets `true` when a feature is in the context', () => { - const { result } = render(() => useFlag('example-1')) + const { result } = renderHook(() => useFlag('example-1')) expect(result.current).toBe(true) }) it('gets `false` when a feature is in the context', () => { - const { result } = render(() => useFlag('xyz')) + const { result } = renderHook(() => useFlag('xyz')) expect(result.current).toBe(false) }) - it('crashs when the provider is not wrapper', () => { - const { result } = renderHook(() => useFlag('example-1')) + it('crashes when the provider is not wrapper', () => { + const { result } = _renderHook(() => useFlag('example-1')) expect(result.error?.message).toEqual(messages.NO_PROVIDER) }) }) -describe('useFlagQuery', () => { +describe('useFlagQueryFn', () => { it('gets `true` when a feature is in the context', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() - expect(flagQuery('example-1')).toBe(true) + expect(flagQueryFn('example-1')).toBe(true) }) it('gets `true` when the flag query is truthy', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() - expect(flagQuery({ 'example-1': true, xyz: false })).toBe(true) + expect(flagQueryFn({ 'example-1': true, xyz: false })).toBe(true) }) it('gets `false` when the flag query is falsy', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() - expect(flagQuery({ 'example-1': false, xyz: true })).toBe(false) + expect(flagQueryFn({ 'example-1': false, xyz: true })).toBe(false) }) it('crashes when the provider is not wrapper', () => { - const { result } = renderHook(() => useFlagQuery()) + const { result } = _renderHook(() => useFlagQueryFn()) expect(result.error?.message).toEqual(messages.NO_PROVIDER) }) it('gets `true` when the `$and` operation succeed', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ [Op.AND]: ['example-1', 'example-2', 'example-3'], }), ).toBe(true) }) it('gets `false` when the `$and` operation fails', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ [Op.AND]: ['foo', 'bar', 'example-1'], }), ).toBe(false) }) it('gets `true` when the `$or` operation succeed', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ [Op.OR]: ['example-1', 'xyz'], }), ).toBe(true) }) it('gets `false` when the `$or` operation fails', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ [Op.OR]: ['foo', 'xyz'], }), ).toBe(false) }) it('gets `true` when a complex query is truthy', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ 'example-1': true, 'example-3': true, abc: false, @@ -162,10 +167,10 @@ describe('useFlagQuery', () => { }) it('gets `false` when a complex query is falsy', () => { - const { flagQuery } = renderFlagQuery() + const { flagQueryFn } = renderFlagQueryFn() expect( - flagQuery({ + flagQueryFn({ 'example-1': false, 'example-3': false, abc: true, @@ -189,14 +194,56 @@ describe('useFlagQuery', () => { }), ).toBe(false) }) +}) + +describe('Flag', () => { + it('does not render if the flag query is false', () => { + const { getByTestId, container } = render( + + Sample Text + , + ) + + expect(container.children.length).toBe(0) + expect(() => getByTestId(/sample/i)).toThrowError() + }) - it('crashes when the operator is unknown', () => { - const { flagQuery } = renderFlagQuery() + it('renders if the flag query is true', () => { + const { getByTestId, container } = render( + + Sample Text + , + ) + + expect(container.children.length).toBe(1) + expect(getByTestId(/sample/i)).not.toBeUndefined() + }) +}) - expect(() => { - flagQuery({ - [Symbol('$xor')]: ['foo', 'xyz'], - }) - }).toThrow('Invalid Operator') +describe('Feature', () => { + it('does not render if the feature does not exist', () => { + const { getByTestId, container } = render( + + {feature => { + return Sample Text + }} + , + ) + + expect(container.children.length).toBe(0) + expect(() => getByTestId(/sample/i)).toThrowError() + }) + + it('renders if the feature exists', () => { + const { getByTestId, container } = render( + + {feature => { + return Sample Text + }} + , + ) + + expect(container.children.length).toBe(1) + expect(getByTestId(/example-1/i)).not.toBeUndefined() }) }) diff --git a/yarn.lock b/yarn.lock index 7cd9786..f05d26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,13 @@ dependencies: "@babel/highlight" "^7.14.5" +"@babel/code-frame@^7.10.4": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" + integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== + dependencies: + "@babel/highlight" "^7.16.0" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.5.tgz#8ef4c18e58e801c5c95d3c1c0f2874a2680fadea" @@ -232,6 +239,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== +"@babel/helper-validator-identifier@^7.15.7": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" @@ -265,6 +277,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" + integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== + dependencies: + "@babel/helper-validator-identifier" "^7.15.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.11.5", "@babel/parser@^7.14.5", "@babel/parser@^7.7.0": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.5.tgz#4cd2f346261061b2518873ffecdf1612cb032829" @@ -1101,6 +1122,17 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@jest/types@^27.4.2": + version "27.4.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" + integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1532,6 +1564,20 @@ webpack "^4.44.1" webpack-bundle-analyzer "^4.4.2" +"@testing-library/dom@^8.0.0": + version "8.11.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.1.tgz#03fa2684aa09ade589b460db46b4c7be9fc69753" + integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + "@testing-library/react-hooks@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0" @@ -1543,6 +1589,14 @@ "@types/react-test-renderer" ">=16.9.0" react-error-boundary "^3.1.0" +"@testing-library/react@^12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76" + integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -1553,6 +1607,11 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -1628,6 +1687,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest@^25.2.1": version "25.2.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf" @@ -1749,6 +1815,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^2.12.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" @@ -2089,6 +2162,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2103,6 +2181,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansicolors@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" @@ -2172,6 +2255,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -3786,6 +3874,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.10" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c" + integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== + dom-serializer@^1.0.1: version "1.3.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" @@ -6611,6 +6704,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + magic-string@^0.25.2, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -8185,6 +8283,16 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^27.0.2: + version "27.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.2.tgz#e4ce92ad66c3888423d332b40477c87d1dac1fb8" + integrity sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw== + dependencies: + "@jest/types" "^27.4.2" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + proc-log@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77" @@ -8405,6 +8513,11 @@ react-is@^16.12.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"