From 36f46b3510a64848140214a3189181bf03dd850a Mon Sep 17 00:00:00 2001 From: Peter Velkov Date: Fri, 6 Jan 2023 22:51:39 +0200 Subject: [PATCH 1/4] [add] Image source headers handling Extend ImageLoader functionality to be able to work with image sources containing headers We preserve the existing strategy that works with image.src for cases where source is just an uri with no headers When sources contain headers we make a fetch request and then render a local url for the downloaded blob (URL.createObjectURL) Fix #1019 Fix #2268 Close #2442 --- .../pages/image/index.js | 23 ++++ .../__snapshots__/index-test.js.snap | 8 +- .../src/exports/Image/__tests__/index-test.js | 103 ++++++++++++++--- .../src/exports/Image/index.js | 104 ++++++++++++++---- .../src/modules/ImageLoader/index.js | 57 +++++++++- 5 files changed, 253 insertions(+), 42 deletions(-) diff --git a/packages/react-native-web-examples/pages/image/index.js b/packages/react-native-web-examples/pages/image/index.js index 086a21a674..623f46c77a 100644 --- a/packages/react-native-web-examples/pages/image/index.js +++ b/packages/react-native-web-examples/pages/image/index.js @@ -15,6 +15,18 @@ const dataBase64Svg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg=='; const dataSvg = 'data:image/svg+xml;utf8,'; +const sourceWithHeaders = { + uri: placeholder, + headers: { + 'x-token': '0012345' + } +}; +const sourceWithHeadersAndRedirect = { + uri: source, + headers: { + 'x-token': '0012345' + } +}; function Divider() { return ; @@ -118,6 +130,17 @@ export default function ImagePage() { /> + + + + With Headers + + + + Headers & Redirect + + + ); } diff --git a/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap index 55e2d30ac5..b7314426d8 100644 --- a/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap +++ b/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap @@ -329,14 +329,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`] >
{ beforeEach(() => { ImageUriCache._entries = {}; window.Image = jest.fn(() => ({})); + ImageLoader.load = jest + .fn() + .mockImplementation((source, onLoad, onError) => { + act(() => onLoad({ source })); + }); + ImageLoader.loadWithHeaders = jest.fn().mockImplementation((source) => ({ + source, + promise: Promise.resolve(`blob:${Math.random()}`), + cancel: jest.fn() + })); }); afterEach(() => { @@ -102,10 +112,6 @@ describe('components/Image', () => { describe('prop "onLoad"', () => { test('is called after image is loaded from network', () => { - jest.useFakeTimers(); - ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => { - onLoad(); - }); const onLoadStartStub = jest.fn(); const onLoadStub = jest.fn(); const onLoadEndStub = jest.fn(); @@ -117,15 +123,10 @@ describe('components/Image', () => { source="https://test.com/img.jpg" /> ); - jest.runOnlyPendingTimers(); expect(onLoadStub).toBeCalled(); }); test('is called after image is loaded from cache', () => { - jest.useFakeTimers(); - ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => { - onLoad(); - }); const onLoadStartStub = jest.fn(); const onLoadStub = jest.fn(); const onLoadEndStub = jest.fn(); @@ -139,7 +140,6 @@ describe('components/Image', () => { source={uri} /> ); - jest.runOnlyPendingTimers(); expect(onLoadStub).toBeCalled(); ImageUriCache.remove(uri); }); @@ -223,6 +223,34 @@ describe('components/Image', () => { }); }); + describe('prop "onLoadStart"', () => { + test('is called on update if "headers" are modified', () => { + const onLoadStartStub = jest.fn(); + const { rerender } = render( + + ); + act(() => { + rerender( + + ); + }); + + expect(onLoadStartStub.mock.calls.length).toBe(2); + }); + }); + describe('prop "resizeMode"', () => { ['contain', 'cover', 'none', 'repeat', 'stretch', undefined].forEach( (resizeMode) => { @@ -241,7 +269,8 @@ describe('components/Image', () => { '', {}, { uri: '' }, - { uri: 'https://google.com' } + { uri: 'https://google.com' }, + { uri: 'https://google.com', headers: { 'x-custom-header': 'abc123' } } ]; sources.forEach((source) => { expect(() => render()).not.toThrow(); @@ -257,11 +286,6 @@ describe('components/Image', () => { test('is set immediately if the image was preloaded', () => { const uri = 'https://yahoo.com/favicon.ico'; - ImageLoader.load = jest - .fn() - .mockImplementationOnce((_, onLoad, onError) => { - onLoad(); - }); return Image.prefetch(uri).then(() => { const source = { uri }; const { container } = render(, { @@ -342,6 +366,51 @@ describe('components/Image', () => { 'http://localhost/static/img@2x.png' ); }); + + test('it works with headers in 2 stages', async () => { + const uri = 'https://google.com/favicon.ico'; + const headers = { 'x-custom-header': 'abc123' }; + const source = { uri, headers }; + + // Stage 1 + const loadRequest = { + promise: Promise.resolve('blob:123'), + cancel: jest.fn(), + source + }; + + ImageLoader.loadWithHeaders.mockReturnValue(loadRequest); + + render(); + + expect(ImageLoader.loadWithHeaders).toHaveBeenCalledWith( + expect.objectContaining(source) + ); + + // Stage 2 + return waitFor(() => { + expect(ImageLoader.load).toHaveBeenCalledWith( + 'blob:123', + expect.any(Function), + expect.any(Function) + ); + }); + }); + + // A common case is `source` declared as an inline object, which cause is to be a + // new object (with the same content) each time parent component renders + test('it still loads the image if source object is changed', () => { + const uri = 'https://google.com/favicon.ico'; + const headers = { 'x-custom-header': 'abc123' }; + const { rerender } = render(); + rerender(); + + // when the underlying source didn't change we don't expect more than 1 load calls + return waitFor(() => { + expect(ImageLoader.loadWithHeaders).toHaveBeenCalledTimes(1); + expect(ImageLoader.load).toHaveBeenCalledTimes(1); + }); + }); }); describe('prop "style"', () => { diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js index bd69e5e844..1862ad6e64 100644 --- a/packages/react-native-web/src/exports/Image/index.js +++ b/packages/react-native-web/src/exports/Image/index.js @@ -8,6 +8,7 @@ * @flow */ +import type { ImageSource, LoadRequest } from '../../modules/ImageLoader'; import type { ImageProps } from './types'; import * as React from 'react'; @@ -165,6 +166,23 @@ function resolveAssetUri(source): ?string { return uri; } +function raiseOnErrorEvent(uri, { onError, onLoadEnd }) { + if (onError) { + onError({ + nativeEvent: { + error: `Failed to load resource ${uri} (404)` + } + }); + } + if (onLoadEnd) onLoadEnd(); +} + +function hasSourceDiff(a: ImageSource, b: ImageSource) { + return ( + a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers) + ); +} + interface ImageStatics { getSize: ( uri: string, @@ -177,10 +195,12 @@ interface ImageStatics { ) => Promise<{| [uri: string]: 'disk/memory' |}>; } -const Image: React.AbstractComponent< +type ImageComponent = React.AbstractComponent< ImageProps, React.ElementRef -> = React.forwardRef((props, ref) => { +>; + +const BaseImage: ImageComponent = React.forwardRef((props, ref) => { const { 'aria-label': ariaLabel, blurRadius, @@ -300,16 +320,7 @@ const Image: React.AbstractComponent< }, function error() { updateState(ERRORED); - if (onError) { - onError({ - nativeEvent: { - error: `Failed to load resource ${uri} (404)` - } - }); - } - if (onLoadEnd) { - onLoadEnd(); - } + raiseOnErrorEvent(uri, { onError, onLoadEnd }); } ); } @@ -353,14 +364,69 @@ const Image: React.AbstractComponent< ); }); -Image.displayName = 'Image'; +BaseImage.displayName = 'Image'; -// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet -const ImageWithStatics = (Image: React.AbstractComponent< - ImageProps, - React.ElementRef -> & - ImageStatics); +/** + * This component handles specifically loading an image source with headers + * default source is never loaded using headers + */ +const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => { + // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource` + const nextSource: ImageSource = props.source; + const [blobUri, setBlobUri] = React.useState(''); + const request = React.useRef({ + cancel: () => {}, + source: { uri: '', headers: {} }, + promise: Promise.resolve('') + }); + + const { onLoadStart, ...forwardedProps } = props; + const { onError, onLoadEnd } = forwardedProps; + + React.useEffect(() => { + if (!hasSourceDiff(nextSource, request.current.source)) { + return; + } + + // When source changes we want to clean up any old/running requests + request.current.cancel(); + + if (onLoadStart) { + onLoadStart(); + } + + // Store a ref for the current load request so we know what's the last loaded source, + // and so we can cancel it if a different source is passed through props + request.current = ImageLoader.loadWithHeaders(nextSource); + + request.current.promise + .then((uri) => setBlobUri(uri)) + .catch(() => + raiseOnErrorEvent(request.current.source.uri, { onError, onLoadEnd }) + ); + }, [nextSource, onLoadStart, onError, onLoadEnd]); + + // Cancel any request on unmount + React.useEffect(() => request.current.cancel, []); + + // Until the current component resolves the request (using headers) + // we skip forwarding the source so the base component doesn't attempt + // to load the original source + const source = blobUri ? { ...nextSource, uri: blobUri } : undefined; + + return ; +}); + +// $FlowFixMe +const ImageWithStatics: ImageComponent & ImageStatics = React.forwardRef( + (props, ref) => { + if (props.source && props.source.headers) { + return ; + } + + return ; + } +); ImageWithStatics.getSize = function (uri, success, failure) { ImageLoader.getSize(uri, success, failure); diff --git a/packages/react-native-web/src/modules/ImageLoader/index.js b/packages/react-native-web/src/modules/ImageLoader/index.js index 892db99292..0d7ceda8ff 100644 --- a/packages/react-native-web/src/modules/ImageLoader/index.js +++ b/packages/react-native-web/src/modules/ImageLoader/index.js @@ -122,9 +122,18 @@ const ImageLoader = { id += 1; const image = new window.Image(); image.onerror = onError; - image.onload = (e) => { + image.onload = (nativeEvent) => { // avoid blocking the main thread - const onDecode = () => onLoad({ nativeEvent: e }); + const onDecode = () => { + // Append `source` to match RN's ImageLoadEvent interface + nativeEvent.source = { + uri: image.src, + width: image.naturalWidth, + height: image.naturalHeight + }; + + onLoad({ nativeEvent }); + }; if (typeof image.decode === 'function') { // Safari currently throws exceptions when decoding svgs. // We want to catch that error and allow the load handler @@ -136,8 +145,41 @@ const ImageLoader = { }; image.src = uri; requests[`${id}`] = image; + return id; }, + loadWithHeaders(source: ImageSource): LoadRequest { + let uri: string; + const abortController = new AbortController(); + const request = new Request(source.uri, { + headers: source.headers, + signal: abortController.signal + }); + request.headers.append('accept', 'image/*'); + + const promise = fetch(request) + .then((response) => response.blob()) + .then((blob) => { + uri = URL.createObjectURL(blob); + return uri; + }) + .catch((error) => { + if (error.name === 'AbortError') { + return ''; + } + + throw error; + }); + + return { + promise, + source, + cancel: () => { + abortController.abort(); + URL.revokeObjectURL(uri); + } + }; + }, prefetch(uri: string): Promise { return new Promise((resolve, reject) => { ImageLoader.load( @@ -164,4 +206,15 @@ const ImageLoader = { } }; +export type LoadRequest = {| + cancel: Function, + source: ImageSource, + promise: Promise +|}; + +export type ImageSource = { + uri: string, + headers: { [key: string]: string } +}; + export default ImageLoader; From e722282c09d12350ec3bbe1bfbab47ad5330fe56 Mon Sep 17 00:00:00 2001 From: Peter Velkov Date: Thu, 23 Feb 2023 13:56:58 +0200 Subject: [PATCH 2/4] [fix] ImageLoader: ImageUriCache is not kept up to date with loaded images Close #2493 --- .../react-native-web/src/exports/Image/index.js | 2 +- .../src/modules/ImageLoader/index.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js index 1862ad6e64..64b7a73276 100644 --- a/packages/react-native-web/src/exports/Image/index.js +++ b/packages/react-native-web/src/exports/Image/index.js @@ -327,7 +327,7 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => { function abortPendingRequest() { if (requestRef.current != null) { - ImageLoader.abort(requestRef.current); + ImageLoader.clear(requestRef.current); requestRef.current = null; } } diff --git a/packages/react-native-web/src/modules/ImageLoader/index.js b/packages/react-native-web/src/modules/ImageLoader/index.js index 0d7ceda8ff..db887df6d3 100644 --- a/packages/react-native-web/src/modules/ImageLoader/index.js +++ b/packages/react-native-web/src/modules/ImageLoader/index.js @@ -74,12 +74,13 @@ let id = 0; const requests = {}; const ImageLoader = { - abort(requestId: number) { - let image = requests[`${requestId}`]; + clear(requestId: number) { + const image = requests[`${requestId}`]; if (image) { image.onerror = null; image.onload = null; - image = null; + ImageUriCache.remove(image.src); + image.src = ''; delete requests[`${requestId}`]; } }, @@ -102,7 +103,7 @@ const ImageLoader = { } } if (complete) { - ImageLoader.abort(requestId); + ImageLoader.clear(requestId); clearInterval(interval); } } @@ -111,7 +112,7 @@ const ImageLoader = { if (typeof failure === 'function') { failure(); } - ImageLoader.abort(requestId); + ImageLoader.clear(requestId); clearInterval(interval); } }, @@ -123,6 +124,7 @@ const ImageLoader = { const image = new window.Image(); image.onerror = onError; image.onload = (nativeEvent) => { + ImageUriCache.add(uri); // avoid blocking the main thread const onDecode = () => { // Append `source` to match RN's ImageLoadEvent interface @@ -185,9 +187,8 @@ const ImageLoader = { ImageLoader.load( uri, () => { - // Add the uri to the cache so it can be immediately displayed when used - // but also immediately remove it to correctly reflect that it has no active references - ImageUriCache.add(uri); + // load() adds the uri to the cache so it can be immediately displayed when used, + // but we also immediately remove it to correctly reflect that it has no active references ImageUriCache.remove(uri); resolve(); }, From 7cb5800e2503f60e3c7251268c472415e2e8b130 Mon Sep 17 00:00:00 2001 From: Peter Velkov Date: Fri, 24 Feb 2023 20:15:10 +0200 Subject: [PATCH 3/4] [fix] Image: image LOADED state is only captured initially If the Image component is rendered with a `null` source, and consecutively updated with actual source url that was already loaded, it would fail to pick up the change - `state` would be `IDLE` for a brief moment and this would cause a small flicker when the image renders Let's always start from IDLE state, and update `shouldDisplaySource` condition to be based on `ImageLoader.has` cache or not Fix #2492 --- .../__snapshots__/index-test.js.snap | 20 +++++++++---------- .../src/exports/Image/__tests__/index-test.js | 4 ++-- .../src/exports/Image/index.js | 19 ++++++------------ 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap index b7314426d8..c87e95253f 100644 --- a/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap +++ b/packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap @@ -255,7 +255,7 @@ exports[`components/Image prop "source" is correctly updated when missing in ini
`; -exports[`components/Image prop "source" is not set immediately if the image has not already been loaded 1`] = ` +exports[`components/Image prop "source" is set immediately if the image has already been loaded 1`] = `
@@ -272,53 +272,53 @@ exports[`components/Image prop "source" is not set immediately if the image has
`; -exports[`components/Image prop "source" is set immediately if the image has already been loaded 1`] = ` +exports[`components/Image prop "source" is set immediately if the image has already been loaded 2`] = `
`; -exports[`components/Image prop "source" is set immediately if the image has already been loaded 2`] = ` +exports[`components/Image prop "source" is set immediately if the image was preloaded 1`] = `
`; -exports[`components/Image prop "source" is set immediately if the image was preloaded 1`] = ` +exports[`components/Image prop "source" is set immediately while image is loading and there is no default source 1`] = `
`; diff --git a/packages/react-native-web/src/exports/Image/__tests__/index-test.js b/packages/react-native-web/src/exports/Image/__tests__/index-test.js index 0e75e792e2..abfae47872 100644 --- a/packages/react-native-web/src/exports/Image/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/Image/__tests__/index-test.js @@ -277,8 +277,8 @@ describe('components/Image', () => { }); }); - test('is not set immediately if the image has not already been loaded', () => { - const uri = 'https://google.com/favicon.ico'; + test('is set immediately while image is loading and there is no default source', () => { + const uri = 'https://google.com/not-yet-loaded-image.ico'; const source = { uri }; const { container } = render(); expect(container.firstChild).toMatchSnapshot(); diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js index 64b7a73276..c89dd0ab3b 100644 --- a/packages/react-native-web/src/exports/Image/index.js +++ b/packages/react-native-web/src/exports/Image/index.js @@ -225,24 +225,18 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => { } } - const [state, updateState] = React.useState(() => { - const uri = resolveAssetUri(source); - if (uri != null) { - const isLoaded = ImageLoader.has(uri); - if (isLoaded) { - return LOADED; - } - } - return IDLE; - }); - + const [state, updateState] = React.useState(IDLE); const [layout, updateLayout] = React.useState({}); const hasTextAncestor = React.useContext(TextAncestorContext); const hiddenImageRef = React.useRef(null); const filterRef = React.useRef(_filterId++); const requestRef = React.useRef(null); + const uri = resolveAssetUri(source); + const isCached = uri != null && ImageLoader.has(uri); const shouldDisplaySource = - state === LOADED || (state === LOADING && defaultSource == null); + state === LOADED || + isCached || + (state === LOADING && defaultSource == null); const [flatStyle, _resizeMode, filter, _tintColor] = getFlatStyle( style, blurRadius, @@ -297,7 +291,6 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => { } // Image loading - const uri = resolveAssetUri(source); React.useEffect(() => { abortPendingRequest(); From cca0fb16451563871c631a03c299805393e3224a Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 11 Apr 2023 16:15:47 -0700 Subject: [PATCH 4/4] [change] babel-plugin option for style runtime --- .../__snapshots__/index-test.js.snap | 249 +++++++++++++++--- .../src/__tests__/index-test.js | 123 +++++---- .../src/index.js | 76 ++++-- .../src/exports/StyleSheet/runtime.js | 32 +++ .../Animated/nodes/AnimatedStyle.js | 15 +- 5 files changed, 376 insertions(+), 119 deletions(-) create mode 100644 packages/react-native-web/src/exports/StyleSheet/runtime.js diff --git a/packages/babel-plugin-react-native-web/src/__tests__/__snapshots__/index-test.js.snap b/packages/babel-plugin-react-native-web/src/__tests__/__snapshots__/index-test.js.snap index 62166c18f4..3a81278d70 100644 --- a/packages/babel-plugin-react-native-web/src/__tests__/__snapshots__/index-test.js.snap +++ b/packages/babel-plugin-react-native-web/src/__tests__/__snapshots__/index-test.js.snap @@ -1,6 +1,115 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Rewrite react-native to react-native-web export from "react-native": export from "react-native" 1`] = ` +exports[`[commonjs] Rewrite react-native to react-native-web export from "react-native": export from "react-native" 1`] = ` + +export { View } from 'react-native'; +export { StyleSheet, Text, unstable_createElement } from 'react-native'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +export { default as View } from 'react-native-web/dist/cjs/exports/View'; +export { default as StyleSheet } from 'react-native-web/dist/cjs/exports/StyleSheet'; +export { default as Text } from 'react-native-web/dist/cjs/exports/Text'; +export { default as unstable_createElement } from 'react-native-web/dist/cjs/exports/createElement'; + + +`; + +exports[`[commonjs] Rewrite react-native to react-native-web export from "react-native-web": export from "react-native-web" 1`] = ` + +export { View } from 'react-native-web'; +export { StyleSheet, Text, unstable_createElement } from 'react-native-web'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +export { default as View } from 'react-native-web/dist/cjs/exports/View'; +export { default as StyleSheet } from 'react-native-web/dist/cjs/exports/StyleSheet'; +export { default as Text } from 'react-native-web/dist/cjs/exports/Text'; +export { default as unstable_createElement } from 'react-native-web/dist/cjs/exports/createElement'; + + +`; + +exports[`[commonjs] Rewrite react-native to react-native-web import from "react-native": import from "react-native" 1`] = ` + +import ReactNative from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import { Invalid, View as MyView } from 'react-native'; +import { useLocaleContext } from 'react-native'; +import * as ReactNativeModules from 'react-native'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +import ReactNative from 'react-native-web/dist/cjs/index'; +import StyleSheet from 'react-native-web/dist/cjs/exports/StyleSheet'; +import View from 'react-native-web/dist/cjs/exports/View'; +import { Invalid } from 'react-native-web/dist/cjs/index'; +import MyView from 'react-native-web/dist/cjs/exports/View'; +import useLocaleContext from 'react-native-web/dist/cjs/exports/useLocaleContext'; +import * as ReactNativeModules from 'react-native-web/dist/cjs/index'; + + +`; + +exports[`[commonjs] Rewrite react-native to react-native-web import from "react-native-web": import from "react-native-web" 1`] = ` + +import { unstable_createElement } from 'react-native-web'; +import { StyleSheet, View, Pressable, processColor } from 'react-native-web'; +import * as ReactNativeModules from 'react-native-web'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +import unstable_createElement from 'react-native-web/dist/cjs/exports/createElement'; +import StyleSheet from 'react-native-web/dist/cjs/exports/StyleSheet'; +import View from 'react-native-web/dist/cjs/exports/View'; +import Pressable from 'react-native-web/dist/cjs/exports/Pressable'; +import processColor from 'react-native-web/dist/cjs/exports/processColor'; +import * as ReactNativeModules from 'react-native-web/dist/cjs/index'; + + +`; + +exports[`[commonjs] Rewrite react-native to react-native-web require "react-native": require "react-native" 1`] = ` + +const ReactNative = require('react-native'); +const { View } = require('react-native'); +const { StyleSheet, Pressable } = require('react-native'); + + ↓ ↓ ↓ ↓ ↓ ↓ + +const ReactNative = require('react-native-web/dist/cjs/index'); +const View = require('react-native-web/dist/cjs/exports/View').default; +const StyleSheet = + require('react-native-web/dist/cjs/exports/StyleSheet').default; +const Pressable = + require('react-native-web/dist/cjs/exports/Pressable').default; + + +`; + +exports[`[commonjs] Rewrite react-native to react-native-web require "react-native-web": require "react-native-web" 1`] = ` + +const ReactNative = require('react-native-web'); +const { unstable_createElement } = require('react-native-web'); +const { StyleSheet, View, Pressable, processColor } = require('react-native-web'); + + ↓ ↓ ↓ ↓ ↓ ↓ + +const ReactNative = require('react-native-web/dist/cjs/index'); +const unstable_createElement = + require('react-native-web/dist/cjs/exports/createElement').default; +const StyleSheet = + require('react-native-web/dist/cjs/exports/StyleSheet').default; +const View = require('react-native-web/dist/cjs/exports/View').default; +const Pressable = + require('react-native-web/dist/cjs/exports/Pressable').default; +const processColor = + require('react-native-web/dist/cjs/exports/processColor').default; + + +`; + +exports[`[legacy] Rewrite react-native to react-native-web export from "react-native": export from "react-native" 1`] = ` export { View } from 'react-native'; export { StyleSheet, Text, unstable_createElement } from 'react-native'; @@ -15,7 +124,7 @@ export { default as unstable_createElement } from 'react-native-web/dist/exports `; -exports[`Rewrite react-native to react-native-web export from "react-native-web": export from "react-native-web" 1`] = ` +exports[`[legacy] Rewrite react-native to react-native-web export from "react-native-web": export from "react-native-web" 1`] = ` export { View } from 'react-native-web'; export { StyleSheet, Text, unstable_createElement } from 'react-native-web'; @@ -30,10 +139,10 @@ export { default as unstable_createElement } from 'react-native-web/dist/exports `; -exports[`Rewrite react-native to react-native-web import from "react-native": import from "react-native" 1`] = ` +exports[`[legacy] Rewrite react-native to react-native-web import from "react-native": import from "react-native" 1`] = ` import ReactNative from 'react-native'; -import { View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Invalid, View as MyView } from 'react-native'; import { useLocaleContext } from 'react-native'; import * as ReactNativeModules from 'react-native'; @@ -41,6 +150,7 @@ import * as ReactNativeModules from 'react-native'; ↓ ↓ ↓ ↓ ↓ ↓ import ReactNative from 'react-native-web/dist/index'; +import StyleSheet from 'react-native-web/dist/exports/StyleSheet'; import View from 'react-native-web/dist/exports/View'; import { Invalid } from 'react-native-web/dist/index'; import MyView from 'react-native-web/dist/exports/View'; @@ -50,25 +160,7 @@ import * as ReactNativeModules from 'react-native-web/dist/index'; `; -exports[`Rewrite react-native to react-native-web import from "react-native": import from "react-native" 2`] = ` - -import ReactNative from 'react-native'; -import { View } from 'react-native'; -import { Invalid, View as MyView } from 'react-native'; -import * as ReactNativeModules from 'react-native'; - - ↓ ↓ ↓ ↓ ↓ ↓ - -import ReactNative from 'react-native-web/dist/cjs/index'; -import View from 'react-native-web/dist/cjs/exports/View'; -import { Invalid } from 'react-native-web/dist/cjs/index'; -import MyView from 'react-native-web/dist/cjs/exports/View'; -import * as ReactNativeModules from 'react-native-web/dist/cjs/index'; - - -`; - -exports[`Rewrite react-native to react-native-web import from "react-native-web": import from "react-native-web" 1`] = ` +exports[`[legacy] Rewrite react-native to react-native-web import from "react-native-web": import from "react-native-web" 1`] = ` import { unstable_createElement } from 'react-native-web'; import { StyleSheet, View, Pressable, processColor } from 'react-native-web'; @@ -86,7 +178,7 @@ import * as ReactNativeModules from 'react-native-web/dist/index'; `; -exports[`Rewrite react-native to react-native-web require "react-native": require "react-native" 1`] = ` +exports[`[legacy] Rewrite react-native to react-native-web require "react-native": require "react-native" 1`] = ` const ReactNative = require('react-native'); const { View } = require('react-native'); @@ -102,7 +194,89 @@ const Pressable = require('react-native-web/dist/exports/Pressable').default; `; -exports[`Rewrite react-native to react-native-web require "react-native": require "react-native" 2`] = ` +exports[`[legacy] Rewrite react-native to react-native-web require "react-native-web": require "react-native-web" 1`] = ` + +const ReactNative = require('react-native-web'); +const { unstable_createElement } = require('react-native-web'); +const { StyleSheet, View, Pressable, processColor } = require('react-native-web'); + + ↓ ↓ ↓ ↓ ↓ ↓ + +const ReactNative = require('react-native-web/dist/index'); +const unstable_createElement = + require('react-native-web/dist/exports/createElement').default; +const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default; +const View = require('react-native-web/dist/exports/View').default; +const Pressable = require('react-native-web/dist/exports/Pressable').default; +const processColor = + require('react-native-web/dist/exports/processColor').default; + + +`; + +exports[`Rewrite react-native to react-native-web export from "react-native": export from "react-native" 1`] = ` + +export { View } from 'react-native'; +export { StyleSheet, Text, unstable_createElement } from 'react-native'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +export { View } from 'react-native'; +export { StyleSheet, Text, unstable_createElement } from 'react-native'; + + +`; + +exports[`Rewrite react-native to react-native-web export from "react-native-web": export from "react-native-web" 1`] = ` + +export { View } from 'react-native-web'; +export { StyleSheet, Text, unstable_createElement } from 'react-native-web'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +export { View } from 'react-native-web'; +export { StyleSheet, Text, unstable_createElement } from 'react-native-web'; + + +`; + +exports[`Rewrite react-native to react-native-web import from "react-native": import from "react-native" 1`] = ` + +import ReactNative from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import { Invalid, View as MyView } from 'react-native'; +import { useLocaleContext } from 'react-native'; +import * as ReactNativeModules from 'react-native'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +import ReactNative from 'react-native'; +import StyleSheet from 'react-native-web/dist/exports/StyleSheet/runtime'; +import { View } from 'react-native'; +import { Invalid, View as MyView } from 'react-native'; +import { useLocaleContext } from 'react-native'; +import * as ReactNativeModules from 'react-native'; + + +`; + +exports[`Rewrite react-native to react-native-web import from "react-native-web": import from "react-native-web" 1`] = ` + +import { unstable_createElement } from 'react-native-web'; +import { StyleSheet, View, Pressable, processColor } from 'react-native-web'; +import * as ReactNativeModules from 'react-native-web'; + + ↓ ↓ ↓ ↓ ↓ ↓ + +import { unstable_createElement } from 'react-native-web'; +import StyleSheet from 'react-native-web/dist/exports/StyleSheet/runtime'; +import { View, Pressable, processColor } from 'react-native'; +import * as ReactNativeModules from 'react-native-web'; + + +`; + +exports[`Rewrite react-native to react-native-web require "react-native": require "react-native" 1`] = ` const ReactNative = require('react-native'); const { View } = require('react-native'); @@ -110,12 +284,9 @@ const { StyleSheet, Pressable } = require('react-native'); ↓ ↓ ↓ ↓ ↓ ↓ -const ReactNative = require('react-native-web/dist/cjs/index'); -const View = require('react-native-web/dist/cjs/exports/View').default; -const StyleSheet = - require('react-native-web/dist/cjs/exports/StyleSheet').default; -const Pressable = - require('react-native-web/dist/cjs/exports/Pressable').default; +const ReactNative = require('react-native'); +const { View } = require('react-native'); +const { StyleSheet, Pressable } = require('react-native'); `; @@ -128,14 +299,14 @@ const { StyleSheet, View, Pressable, processColor } = require('react-native-web' ↓ ↓ ↓ ↓ ↓ ↓ -const ReactNative = require('react-native-web/dist/index'); -const unstable_createElement = - require('react-native-web/dist/exports/createElement').default; -const StyleSheet = require('react-native-web/dist/exports/StyleSheet').default; -const View = require('react-native-web/dist/exports/View').default; -const Pressable = require('react-native-web/dist/exports/Pressable').default; -const processColor = - require('react-native-web/dist/exports/processColor').default; +const ReactNative = require('react-native-web'); +const { unstable_createElement } = require('react-native-web'); +const { + StyleSheet, + View, + Pressable, + processColor +} = require('react-native-web'); `; diff --git a/packages/babel-plugin-react-native-web/src/__tests__/index-test.js b/packages/babel-plugin-react-native-web/src/__tests__/index-test.js index 9ef0377913..0f0368c67f 100644 --- a/packages/babel-plugin-react-native-web/src/__tests__/index-test.js +++ b/packages/babel-plugin-react-native-web/src/__tests__/index-test.js @@ -1,69 +1,86 @@ const plugin = require('..'); const pluginTester = require('babel-plugin-tester').default; -const tests = [ - // import react-native - { - title: 'import from "react-native"', - code: `import ReactNative from 'react-native'; -import { View } from 'react-native'; +function createTests(pluginOptions) { + return [ + // import react-native + { + title: 'import from "react-native"', + code: `import ReactNative from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Invalid, View as MyView } from 'react-native'; import { useLocaleContext } from 'react-native'; import * as ReactNativeModules from 'react-native';`, - snapshot: true - }, - { - title: 'import from "react-native"', - code: `import ReactNative from 'react-native'; -import { View } from 'react-native'; -import { Invalid, View as MyView } from 'react-native'; -import * as ReactNativeModules from 'react-native';`, - snapshot: true, - pluginOptions: { commonjs: true } - }, - { - title: 'import from "react-native-web"', - code: `import { unstable_createElement } from 'react-native-web'; + snapshot: true, + pluginOptions + }, + { + title: 'import from "react-native-web"', + code: `import { unstable_createElement } from 'react-native-web'; import { StyleSheet, View, Pressable, processColor } from 'react-native-web'; import * as ReactNativeModules from 'react-native-web';`, - snapshot: true - }, - { - title: 'export from "react-native"', - code: `export { View } from 'react-native'; + snapshot: true, + pluginOptions + }, + { + title: 'export from "react-native"', + code: `export { View } from 'react-native'; export { StyleSheet, Text, unstable_createElement } from 'react-native';`, - snapshot: true - }, - { - title: 'export from "react-native-web"', - code: `export { View } from 'react-native-web'; + snapshot: true, + pluginOptions + }, + { + title: 'export from "react-native-web"', + code: `export { View } from 'react-native-web'; export { StyleSheet, Text, unstable_createElement } from 'react-native-web';`, - snapshot: true - }, - // require react-native - { - title: 'require "react-native"', - code: `const ReactNative = require('react-native'); + snapshot: true, + pluginOptions + }, + // require react-native + { + title: 'require "react-native"', + code: `const ReactNative = require('react-native'); const { View } = require('react-native'); const { StyleSheet, Pressable } = require('react-native');`, - snapshot: true - }, - { - title: 'require "react-native"', - code: `const ReactNative = require('react-native'); -const { View } = require('react-native'); -const { StyleSheet, Pressable } = require('react-native');`, - snapshot: true, - pluginOptions: { commonjs: true } - }, - { - title: 'require "react-native-web"', - code: `const ReactNative = require('react-native-web'); + snapshot: true, + pluginOptions + }, + { + title: 'require "react-native-web"', + code: `const ReactNative = require('react-native-web'); const { unstable_createElement } = require('react-native-web'); const { StyleSheet, View, Pressable, processColor } = require('react-native-web');`, - snapshot: true - } -]; + snapshot: true, + pluginOptions + } + ]; +} + +pluginTester({ + babelOptions: { + generatorOpts: { + jsescOption: { + quotes: 'single' + } + } + }, + plugin, + pluginName: '[legacy] Rewrite react-native to react-native-web', + tests: createTests({}) +}); + +pluginTester({ + babelOptions: { + generatorOpts: { + jsescOption: { + quotes: 'single' + } + } + }, + plugin, + pluginName: '[commonjs] Rewrite react-native to react-native-web', + tests: createTests({ commonjs: true }) +}); pluginTester({ babelOptions: { @@ -75,5 +92,5 @@ pluginTester({ }, plugin, pluginName: 'Rewrite react-native to react-native-web', - tests + tests: createTests({ legacy: false }) }); diff --git a/packages/babel-plugin-react-native-web/src/index.js b/packages/babel-plugin-react-native-web/src/index.js index 17230a85ab..dcb6533212 100644 --- a/packages/babel-plugin-react-native-web/src/index.js +++ b/packages/babel-plugin-react-native-web/src/index.js @@ -42,36 +42,64 @@ module.exports = function ({ types: t }) { ImportDeclaration(path, state) { const { specifiers } = path.node; if (isReactNativeModule(path.node)) { - const imports = specifiers - .map((specifier) => { - if (t.isImportSpecifier(specifier)) { - const importName = specifier.imported.name; - const distLocation = getDistLocation(importName, state.opts); + if (state.opts.legacy !== false) { + const imports = specifiers + .map((specifier) => { + if (t.isImportSpecifier(specifier)) { + const importName = specifier.imported.name; + const distLocation = getDistLocation(importName, state.opts); - if (distLocation) { - return t.importDeclaration( - [ - t.importDefaultSpecifier( - t.identifier(specifier.local.name) - ) - ], - t.stringLiteral(distLocation) - ); + if (distLocation) { + return t.importDeclaration( + [ + t.importDefaultSpecifier( + t.identifier(specifier.local.name) + ) + ], + t.stringLiteral(distLocation) + ); + } } - } - return t.importDeclaration( - [specifier], - t.stringLiteral(getDistLocation('index', state.opts)) - ); - }) - .filter(Boolean); + return t.importDeclaration( + [specifier], + t.stringLiteral(getDistLocation('index', state.opts)) + ); + }) + .filter(Boolean); - path.replaceWithMultiple(imports); + path.replaceWithMultiple(imports); + } else { + const styleSheetSpecifierIndex = specifiers.findIndex( + (specifier) => + specifier.imported && specifier.imported.name === 'StyleSheet' + ); + if (styleSheetSpecifierIndex !== -1) { + const otherSpecifiers = [ + ...specifiers.slice(0, styleSheetSpecifierIndex), + ...specifiers.slice(styleSheetSpecifierIndex + 1) + ]; + + const newImports = [ + t.importDeclaration( + [t.importDefaultSpecifier(t.identifier('StyleSheet'))], + t.stringLiteral( + 'react-native-web/dist/exports/StyleSheet/runtime' + ) + ), + t.importDeclaration( + otherSpecifiers, + t.stringLiteral('react-native') + ) + ]; + + path.replaceWithMultiple(newImports); + } + } } }, ExportNamedDeclaration(path, state) { const { specifiers } = path.node; - if (isReactNativeModule(path.node)) { + if (isReactNativeModule(path.node) && state.opts.legacy !== false) { const exports = specifiers .map((specifier) => { if (t.isExportSpecifier(specifier)) { @@ -104,7 +132,7 @@ module.exports = function ({ types: t }) { } }, VariableDeclaration(path, state) { - if (isReactNativeRequire(t, path.node)) { + if (isReactNativeRequire(t, path.node) && state.opts.legacy !== false) { const { id } = path.node.declarations[0]; if (t.isObjectPattern(id)) { const imports = id.properties diff --git a/packages/react-native-web/src/exports/StyleSheet/runtime.js b/packages/react-native-web/src/exports/StyleSheet/runtime.js new file mode 100644 index 0000000000..24514ed642 --- /dev/null +++ b/packages/react-native-web/src/exports/StyleSheet/runtime.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import { localizeStyle } from 'styleq/transform-localize-style'; +import { styleq } from 'styleq'; + +type StyleProps = [string, { [key: string]: mixed } | null]; +type Options = { writingDirection: 'ltr' | 'rtl' }; + +function customStyleq(styles, isRTL) { + return styleq.factory({ + transform(style) { + return localizeStyle(style, isRTL); + } + })(styles); +} + +export default function StyleSheet( + styles: $ReadOnlyArray, + options?: Options +): StyleProps { + const isRTL = options != null && options.writingDirection === 'rtl'; + const styleProps: StyleProps = customStyleq(styles, isRTL); + // inline styles are not processed in any way + return styleProps; +} diff --git a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedStyle.js b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedStyle.js index 1b654319a2..375f960cfc 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedStyle.js @@ -15,9 +15,18 @@ import AnimatedTransform from './AnimatedTransform'; import AnimatedWithChildren from './AnimatedWithChildren'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; -import StyleSheet from '../../../../exports/StyleSheet'; - -const flattenStyle = StyleSheet.flatten; +function flattenStyle(...styles: any): { [key: string]: any } { + const flatArray = styles.flat(Infinity); + const result = {}; + for (let i = 0; i < flatArray.length; i++) { + const style = flatArray[i]; + if (style != null && typeof style === 'object') { + // $FlowFixMe + Object.assign(result, style); + } + } + return result; +} function createAnimatedStyle(inputStyle: any): Object { const style = flattenStyle(inputStyle);