diff --git a/example/package.json b/example/package.json index 491473a..08142cb 100644 --- a/example/package.json +++ b/example/package.json @@ -2,10 +2,12 @@ "name": "example", "version": "1.0.0", "dependencies": { + "@shopify/react-native-skia": "^0.1.214", "expo": "~49.0.15", "expo-status-bar": "~1.6.0", "react": "18.2.0", - "react-native": "0.72.6" + "react-native": "0.72.6", + "react-native-reanimated": "^3.5.4" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/package.json b/package.json index 8ee2f08..f692b3d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "author": "Mateusz Łopaciński ", "devDependencies": { + "@shopify/react-native-skia": "^0.1.214", "eslint": "^8.52.0", "eslint-config-react-native-matipl01": "^1.0.2", "husky": "^8.0.0", @@ -11,6 +12,7 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-native": "^0.72.5", + "react-native-reanimated": "^3.5.4", "syncpack": "^11.2.1", "typescript": "^5.2.2" }, @@ -26,6 +28,12 @@ ] }, "main": "index.js", + "peerDependencies": { + "@shopify/react-native-skia": ">=0.1.75", + "react": "*", + "react-native": "*", + "react-native-reanimated": ">=2.0.0" + }, "private": true, "publishConfig": { "access": "public" diff --git a/src/components/ResponsiveText.tsx b/src/components/ResponsiveText.tsx new file mode 100644 index 0000000..327d187 --- /dev/null +++ b/src/components/ResponsiveText.tsx @@ -0,0 +1,136 @@ +import { Group, Rect, SkFont, TextProps } from '@shopify/react-native-skia'; +import { memo, useMemo } from 'react'; +import { runOnJS, SharedValue, useDerivedValue } from 'react-native-reanimated'; + +import { DEFAULT_FONT } from '@/constants'; +import { useAnimatableValue } from '@/hooks'; +import { + AnimatableProps, + AnimationSettings, + EllipsizeMode, + HorizontalAlignment, + PartialBy, + TextLineData, + VerticalAlignment +} from '@/types'; +import { + getTextLinesAlignment, + getVerticalAlignmentOffset, + wrapText +} from '@/utils'; + +import TextLine from './TextLine'; + +type ResponsiveTextProps = PartialBy & { + ellipsizeMode?: EllipsizeMode; + height?: number; + numberOfLines?: number; + onMeasure?: (width: number, height: number) => void; + width?: number; +} & AnimatableProps<{ + backgroundColor?: string; + font: SkFont; + horizontalAlignment?: HorizontalAlignment; + lineHeight?: number; + verticalAlignment?: VerticalAlignment; + }> & + ( + | { animationProgress?: SharedValue } + | { animationSettings?: AnimationSettings } + ); + +function ResponsiveText({ + backgroundColor: backgroundColorProp = 'transparent', + children, + ellipsizeMode, + font = DEFAULT_FONT, + height = 0, + horizontalAlignment: horizontalAlignmentProp = 'left', + lineHeight: lineHeightProp, + numberOfLines, + onMeasure, + text = '', + verticalAlignment: verticalAlignmentProp = 'top', + width = 0, + x = 0, + y = 0, + ...rest +}: ResponsiveTextProps) { + const fontSize = font.getSize(); + // Create shared values from animatable props + const backgroundColor = useAnimatableValue(backgroundColorProp); + + const lineHeight = useAnimatableValue(lineHeightProp ?? fontSize); + const horizontalAlignment = useAnimatableValue(horizontalAlignmentProp); + const verticalAlignment = useAnimatableValue(verticalAlignmentProp); + + // Divide text into lines + const textLines = useMemo>( + () => wrapText(text, font, width, numberOfLines, ellipsizeMode), + [text, font, width, numberOfLines, ellipsizeMode] + ); + + // Calculate remaining values + const horizontalAlignmentOffsets = useDerivedValue(() => { + const alignments = getTextLinesAlignment( + textLines, + width, + horizontalAlignment.value + ); + + const textWidth = textLines.reduce( + (acc, { width: w }) => Math.max(acc, w), + 0 + ); + const textHeight = textLines.length * lineHeight.value; + + if (onMeasure) { + runOnJS(onMeasure)(textWidth, textHeight); + } + + return alignments; + }, [textLines]); + const textHeight = useDerivedValue( + () => + textLines.length * lineHeight.value - (lineHeight.value - 1.5 * fontSize) + ); + const backgroundHeight = useDerivedValue(() => + Math.max(textHeight.value, height) + ); + const verticalAlignmentOffset = useDerivedValue(() => + getVerticalAlignmentOffset( + textHeight.value, + height, + verticalAlignment.value + ) + ); + + return ( + + {backgroundColor && ( + + )} + {textLines.map((line, i) => ( + + {children} + + ))} + + ); +} + +export default memo(ResponsiveText); diff --git a/src/components/TextLine.tsx b/src/components/TextLine.tsx new file mode 100644 index 0000000..a5cab42 --- /dev/null +++ b/src/components/TextLine.tsx @@ -0,0 +1,86 @@ +import { Text, TextProps } from '@shopify/react-native-skia'; +import { + interpolate, + SharedValue, + useAnimatedReaction, + useSharedValue, + withTiming +} from 'react-native-reanimated'; + +import { AnimationSettings } from '@/types'; + +type TextLineProps = Omit & { + animationProgress?: SharedValue; + animationSettings?: AnimationSettings; + fontSize: number; + horizontalAlignmentOffsets: SharedValue>; + index: number; + lineHeight: SharedValue; + verticalAlignmentOffset: SharedValue; +}; + +export default function TextLine({ + animationProgress, + animationSettings, + fontSize, + horizontalAlignmentOffsets, + index, + lineHeight, + verticalAlignmentOffset, + ...rest +}: TextLineProps) { + const getCurrentX = () => { + 'worklet'; + return horizontalAlignmentOffsets.value[index] ?? 0; + }; + const getCurrentY = () => { + 'worklet'; + return verticalAlignmentOffset.value + index * lineHeight.value + fontSize; + }; + + const startX = useSharedValue(getCurrentX()); + const startY = useSharedValue(getCurrentY()); + const prevTargetX = useSharedValue(getCurrentX()); + const prevTargetY = useSharedValue(getCurrentY()); + const x = useSharedValue(getCurrentX()); + const y = useSharedValue(getCurrentY()); + + useAnimatedReaction( + () => ({ + x: horizontalAlignmentOffsets.value[index] ?? 0, + y: verticalAlignmentOffset.value + index * lineHeight.value + fontSize + }), + target => { + if (animationSettings) { + const { onComplete, ...animation } = animationSettings; + x.value = withTiming(target.x, animation, onComplete); + y.value = withTiming(target.y, animation, onComplete); + } else if (!animationProgress) { + x.value = target.x; + y.value = target.y; + } + } + ); + + useAnimatedReaction( + () => animationProgress?.value ?? null, + progress => { + if (progress === null) return; + + const targetX = getCurrentX(); + const targetY = getCurrentY(); + + if (targetX !== prevTargetX.value || targetY !== prevTargetY.value) { + startX.value = x.value; + startY.value = y.value; + prevTargetX.value = targetX; + prevTargetY.value = targetY; + } + + x.value = interpolate(progress, [0, 1], [startX.value, targetX]); + y.value = interpolate(progress, [0, 1], [startY.value, targetY]); + } + ); + + return ; +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..8ccadc5 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { default as ResponsiveText } from './ResponsiveText'; diff --git a/src/constants/font.ts b/src/constants/font.ts new file mode 100644 index 0000000..09471ad --- /dev/null +++ b/src/constants/font.ts @@ -0,0 +1,12 @@ +/* eslint-disable new-cap */ +import { FontStyle, Skia } from '@shopify/react-native-skia'; +import { Platform } from 'react-native'; + +const familyName = Platform.select({ default: 'serif', ios: 'Helvetica' }); +const fontSize = 32; + +const fontMgr = Skia.FontMgr.System(); + +const typeface = fontMgr.matchFamilyStyle(familyName, FontStyle.Bold); + +export const DEFAULT_FONT = Skia.Font(typeface, fontSize); diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..f36d3d5 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export * from './font'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..c5803a2 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as useAnimatableValue } from './useAnimatableValue'; diff --git a/src/hooks/useAnimatableValue.tsx b/src/hooks/useAnimatableValue.tsx new file mode 100644 index 0000000..012d3e5 --- /dev/null +++ b/src/hooks/useAnimatableValue.tsx @@ -0,0 +1,16 @@ +import { + isSharedValue, + SharedValue, + useDerivedValue +} from 'react-native-reanimated'; + +import { AnimatableValue } from '@/types'; + +export default function useAnimatableValue( + value: AnimatableValue +): SharedValue { + return useDerivedValue( + () => (isSharedValue(value) ? value.value : (value as T)), + [value] + ); +} diff --git a/src/index.ts b/src/index.ts index e69de29..d05535f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,9 @@ +export { ResponsiveText } from './components'; +export type { + AnimatableProps, + AnimatableValue, + AnimationSettings, + EllipsizeMode, + HorizontalAlignment, + VerticalAlignment +} from './types'; diff --git a/src/types/animations.ts b/src/types/animations.ts new file mode 100644 index 0000000..50ef3b9 --- /dev/null +++ b/src/types/animations.ts @@ -0,0 +1,20 @@ +import { EasingFunction } from 'react-native'; +import { EasingFunctionFactory, SharedValue } from 'react-native-reanimated'; + +type AnimationEasing = EasingFunction | EasingFunctionFactory; + +export type AnimationSettings = { + duration?: number; + easing?: AnimationEasing; + onComplete?: (finished?: boolean) => void; +}; + +export type AnimatableValue = T extends infer U | undefined + ? U extends undefined + ? never + : SharedValue | U + : SharedValue | T; + +export type AnimatableProps = { + [K in keyof T]: AnimatableValue; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..14079cc --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,4 @@ +export * from './animations'; +export * from './layout'; +export * from './text'; +export * from './utils'; diff --git a/src/types/layout.ts b/src/types/layout.ts new file mode 100644 index 0000000..5820a26 --- /dev/null +++ b/src/types/layout.ts @@ -0,0 +1,8 @@ +export type HorizontalAlignment = + | 'center' + | 'center-left' + | 'center-right' + | 'left' + | 'right'; + +export type VerticalAlignment = 'bottom' | 'center' | 'top'; diff --git a/src/types/text.ts b/src/types/text.ts new file mode 100644 index 0000000..d5c1428 --- /dev/null +++ b/src/types/text.ts @@ -0,0 +1,6 @@ +export type EllipsizeMode = 'clip' | 'head' | 'middle' | 'tail'; + +export type TextLineData = { + text: string; + width: number; +}; diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000..ce7e0b4 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1 @@ +export type PartialBy = Omit & Partial>; diff --git a/src/utils/alignment.ts b/src/utils/alignment.ts new file mode 100644 index 0000000..a937cc0 --- /dev/null +++ b/src/utils/alignment.ts @@ -0,0 +1,42 @@ +import { HorizontalAlignment, TextLineData, VerticalAlignment } from '@/types'; + +export const getTextLinesAlignment = ( + lines: Array, + width: number, + alignment: HorizontalAlignment = 'left' +): Array => { + 'worklet'; + switch (alignment) { + case 'right': + return lines.map(line => width - line.width); + case 'center': + return lines.map(line => (width - line.width) / 2); + case 'center-left': + case 'center-right': + const longestLineWidth = Math.max(...lines.map(line => line.width)); + return lines.map(line => + alignment === 'center-left' + ? (width - longestLineWidth) / 2 + : (width + longestLineWidth) / 2 - line.width + ); + case 'left': + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return lines.map(_ => 0); + } +}; + +export const getVerticalAlignmentOffset = ( + componentHeight: number, + parentHeight = 0, + verticalAlignment: VerticalAlignment = 'top' +): number => { + 'worklet'; + switch (verticalAlignment) { + case 'top': + return 0; + case 'bottom': + return parentHeight - componentHeight; + case 'center': + return (parentHeight - componentHeight) / 2; + } +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..425f9ca --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './alignment'; +export * from './text'; diff --git a/src/utils/text.ts b/src/utils/text.ts new file mode 100644 index 0000000..a1ea6df --- /dev/null +++ b/src/utils/text.ts @@ -0,0 +1,230 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { SkFont } from '@shopify/react-native-skia'; + +import { EllipsizeMode, TextLineData } from '@/types'; + +const ELLIPSIS = '...'; + +const getTextChunks = (text: string): Array => + text.split(/(\s+(?=\S))/g).filter(word => word.length > 0); + +const isSpace = (text: string): boolean => text.trim().length === 0; + +const wrapWithoutTrimming = ( + chunks: Array, + font: SkFont, + width: number +): Array => { + const result: Array = []; + + let currentLine: { chunks: Array; width: number } = { + chunks: [], + width: 0 + }; + + let nextChunkWidth = font.getTextWidth(chunks[0]!); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]!; + const chunkWidth = nextChunkWidth; + const nextChunk = chunks[i + 1]; + nextChunkWidth = nextChunk ? font.getTextWidth(nextChunk) : Infinity; + + if (currentLine.chunks.length || !isSpace(chunk)) { + currentLine.chunks.push(chunk); + currentLine.width += chunkWidth; + } + + if (currentLine.width + nextChunkWidth >= width) { + result.push({ + text: currentLine.chunks.join(''), + width: currentLine.width + }); + currentLine = { chunks: [], width: 0 }; + } + } + + return result; +}; + +const trimLineEnd = ( + line: TextLineData, + chunks: Array, + chunkIdx: number, + font: SkFont, + width: number, + mode: 'clip' | 'tail' +): TextLineData => { + const renderEllipsis = mode !== 'clip'; + const additionalWidth = renderEllipsis ? font.getTextWidth(ELLIPSIS) : 0; + + let lastLineText = line.text; + if (++chunkIdx < chunks.length) { + lastLineText += chunks[chunkIdx]!; + } + + // Go from the last char and find how many chars must be sliced to + // display ellipsis at the end or clip the end of the line + let lastIndex = lastLineText.length - 1; + let lineWidth = font.getTextWidth(lastLineText) + additionalWidth; + + while (lineWidth > width && lastIndex >= 0) { + lineWidth -= font.getTextWidth(lastLineText[lastIndex]!); + lastIndex--; + } + + return { + text: `${lastLineText.slice(0, lastIndex)}${ + renderEllipsis ? ELLIPSIS : '' + }`, + width: lineWidth + }; +}; + +const trimLineStart = ( + line: TextLineData, + chunks: Array, + chunkIdx: number, + font: SkFont, + width: number +): TextLineData => { + let lastLineText = line.text; + if (++chunkIdx < chunks.length) { + lastLineText += chunks[chunkIdx]!; + } + + // Go from the first char and find how many chars must be sliced to + // display ellipsis at the beginning + let firstIndex = 0; + let lineWidth = font.getTextWidth(lastLineText) + font.getTextWidth(ELLIPSIS); + + while (lineWidth > width && firstIndex < line.text.length) { + lineWidth -= font.getTextWidth(lastLineText[firstIndex]!); + firstIndex++; + } + + return { + text: `${ELLIPSIS}${lastLineText.slice(firstIndex)}`, + width: lineWidth + }; +}; + +const trimLineCenter = ( + line: TextLineData, + chunks: Array, + chunkIdx: number, + font: SkFont, + width: number +): TextLineData => { + let lastLineText = line.text; + if (++chunkIdx < chunks.length) { + lastLineText += chunks[chunkIdx]!; + } + + const middleIdx = Math.ceil(lastLineText.length / 2); + let leftIdx = middleIdx; + let rightIdx = middleIdx; + let lineWidth = font.getTextWidth(lastLineText) + font.getTextWidth(ELLIPSIS); + + let selectedIdx = -1; // -1 - left, 1 - right + while (lineWidth > width && leftIdx >= 0 && rightIdx < lastLineText.length) { + if (selectedIdx === -1) { + lineWidth -= font.getTextWidth(lastLineText[leftIdx]!); + leftIdx--; + } else { + lineWidth -= font.getTextWidth(lastLineText[rightIdx]!); + rightIdx++; + } + selectedIdx *= -1; + } + + return { + text: `${lastLineText.slice(0, leftIdx)}${ELLIPSIS}${lastLineText.slice( + rightIdx + )}`, + width: lineWidth + }; +}; + +const wrapWithTrimming = ( + chunks: Array, + font: SkFont, + width: number, + numberOfLines: number, + mode: EllipsizeMode +): Array => { + const result: Array = []; + + let currentLine: { chunks: Array; width: number } = { + chunks: [], + width: 0 + }; + + let i = 0; + let shouldTrim = false; + let nextChunkWidth = font.getTextWidth(chunks[0]!); + + for (; i < chunks.length; i++) { + const chunk = chunks[i]!; + const chunkWidth = nextChunkWidth; + const nextChunk = chunks[i + 1]; + nextChunkWidth = nextChunk ? font.getTextWidth(nextChunk) : Infinity; + + if (currentLine.chunks.length || !isSpace(chunk)) { + currentLine.chunks.push(chunk); + currentLine.width += chunkWidth; + } + + if (currentLine.width + nextChunkWidth >= width) { + result.push({ + text: currentLine.chunks.join(''), + width: currentLine.width + }); + // Break if the last displayed line is reached + if (result.length === numberOfLines && nextChunkWidth < Infinity) { + shouldTrim = true; + break; + } + currentLine = { chunks: [], width: 0 }; + } + } + + if (shouldTrim) { + let lastLine = result[result.length - 1]!; + switch (mode) { + case 'clip': + case 'tail': + lastLine = trimLineEnd(lastLine, chunks, i, font, width, mode); + break; + case 'head': + lastLine = trimLineStart(lastLine, chunks, i, font, width); + break; + case 'middle': + lastLine = trimLineCenter(lastLine, chunks, i, font, width); + } + + result[result.length - 1] = lastLine; + } + + return result; +}; + +export const wrapText = ( + text: string, + font: SkFont, + width: number, + numberOfLines = Infinity, + ellipsizeMode: EllipsizeMode = 'tail' +): Array => { + const chunks = getTextChunks(text); + + if (!chunks.length) { + return []; + } + + if (numberOfLines === Infinity) { + return wrapWithoutTrimming(chunks, font, width); + } + + return wrapWithTrimming(chunks, font, width, numberOfLines, ellipsizeMode); +}; diff --git a/yarn.lock b/yarn.lock index e48d60a..9ad329c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -794,6 +794,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-transform-object-assign@^7.16.7": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.22.5.tgz#290c1b9555dcea48bb2c29ad94237777600d04f9" + integrity sha512-iDhx9ARkXq4vhZ2CYOSnQXkmxkDgosLi3J8Z17mKz7LyzthtkdVchLD7WZ3aXeCuvJDOW3+1I5TpJmwIbF9MKQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-object-rest-spread@^7.22.15": version "7.22.15" resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz" @@ -1113,6 +1120,17 @@ "@babel/plugin-transform-modules-commonjs" "^7.23.0" "@babel/plugin-transform-typescript" "^7.22.15" +"@babel/preset-typescript@^7.16.7": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.2.tgz#c8de488130b7081f7e1482936ad3de5b018beef4" + integrity sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-typescript" "^7.22.15" + "@babel/register@^7.13.16": version "7.22.15" resolved "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz" @@ -2135,6 +2153,14 @@ component-type "^1.2.1" join-component "^1.1.0" +"@shopify/react-native-skia@^0.1.214": + version "0.1.214" + resolved "https://registry.yarnpkg.com/@shopify/react-native-skia/-/react-native-skia-0.1.214.tgz#55111074919b1e4746e89f6868dad3dff6eda4a9" + integrity sha512-QUt9JRzjQIyZUJxRwjhCQtin6qusZkLUfUNAcEQNfGOQlK5JWXrxYsi4BlMQJSiYDLGZmWZhRuPmmQ3ZpNP9Jw== + dependencies: + canvaskit-wasm "0.38.2" + react-reconciler "^0.27.0" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz" @@ -3134,6 +3160,11 @@ caniuse-lite@^1.0.30001538: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz" integrity sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA== +canvaskit-wasm@0.38.2: + version "0.38.2" + resolved "https://registry.yarnpkg.com/canvaskit-wasm/-/canvaskit-wasm-0.38.2.tgz#b6c2be236670fd0f18977b9026652b2c0e201fee" + integrity sha512-ieRb6DO4yL91qUfyRgmyhp2Hi1KmQ9lIMfKacxHVlfp/CpKCkzgAxRGUbCsJFzwLKjs9fufGrIyvnzEYRwm1XQ== + chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -7524,6 +7555,16 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-native-reanimated@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz#a6c2b0c43b6dad246f5d276213974afedb8e3fc7" + integrity sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg== + dependencies: + "@babel/plugin-transform-object-assign" "^7.16.7" + "@babel/preset-typescript" "^7.16.7" + convert-source-map "^2.0.0" + invariant "^2.2.4" + react-native@0.72.6, react-native@^0.72.5: version "0.72.6" resolved "https://registry.npmjs.org/react-native/-/react-native-0.72.6.tgz" @@ -7566,6 +7607,14 @@ react-native@0.72.6, react-native@^0.72.5: ws "^6.2.2" yargs "^17.6.2" +react-reconciler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.27.0.tgz#360124fdf2d76447c7491ee5f0e04503ed9acf5b" + integrity sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.21.0" + react-refresh@^0.4.0: version "0.4.3" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz" @@ -7932,6 +7981,13 @@ scheduler@0.24.0-canary-efb381bbf-20230505: dependencies: loose-envify "^1.1.0" +scheduler@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" + integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== + dependencies: + loose-envify "^1.1.0" + semver@7.3.2: version "7.3.2" resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz"