Skip to content

Commit 5448e31

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Move AssetRegistry implementation into main package
Summary: **Problem** The separate `react-native/assets-registry` package isn’t working for the ecosystem. `registry.js` holds asset state in a module-scoped variable, which makes the package a stateful singleton: exactly one instance must exist per JS runtime, or registration and lookup diverge. We provide no guarantee that this singleton requirement holds: - The install layout — how the package manager dedupes packages in `node_modules` — decides how many copies exist, and `react-native`'s exact-version pin means third-party ranges never dedupe against it. Effects: - **Consumers silently break**: `expo-asset` and `expo-image` can land on a second copy: assets register in one, resolve as `undefined` from the other. Expo neutralizes this with a shim in `expo/cli` that redirects every registry import to a single virtual module — bare React Native + Metro has no such protection. - **This blocks 1.0**: The ecosystem can't move from exact-version lockstep to semver ranges until stateful packages like the asset registry are safe to duplicate. Today, relaxing the pin would turn a latent footgun into a common one. **To solve this**, move towards (but not quite yet) deleting `react-native/assets-registry`, in favour of a replacement `AssetRegistry` API offered directly by `react-native`. **This diff** - Adds new `AssetRegistry` API, along with the `PackagerAsset` and `AssetDestPathResolver` root type exports in `react-native`. - Updates `react-native/assets-registry` to source from this relocated implementation — fixing the duplicate install layout bug (where apps/frameworks enforce a single copy of `react-native`). **Impact** - **✅ Fixed**: Imports from either `react-native` or `react-native/assets-registry` in RN 0.87+ will be durable to duplicate package installs — Expo can remove their virtual module shim. Changelog: - [General][Fixed] - **assets-registry**: `react-native/assets-registry` now shares state across duplicate installs, sourcing from a relocated implementation in the `react-native` package - [General][Added] - Add `AssetRegistry` API (replaces `react-native/assets-registry/registry`) Differential Revision: D108750302
1 parent 4f142ee commit 5448e31

12 files changed

Lines changed: 117 additions & 62 deletions

File tree

packages/assets-registry/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Most apps never import this directly — assets are handled through `<Image>`.
1010

1111
### `@react-native/assets-registry/registry`
1212

13+
> [!Note]
14+
> Aliases to [`AssetRegistry`](https://reactnative.dev/docs/assetregistry) (since 0.87). Prefer importing directly from the `'react-native'` package in libraries.
15+
1316
| Export | Signature | Notes |
1417
|---|---|---|
1518
| `registerAsset` | `(asset: PackagerAsset) => number` | Stores the asset; returns a numeric ID |

packages/assets-registry/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,8 @@
2727
"!**/__fixtures__/**",
2828
"!**/__mocks__/**",
2929
"!**/__tests__/**"
30-
]
30+
],
31+
"peerDependencies": {
32+
"react-native": "*"
33+
}
3134
}

packages/assets-registry/src/registry.js

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,19 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @flow strict
7+
* @flow strict-local
88
* @format
99
*/
1010

1111
'use strict';
1212

13-
/*::
14-
export type AssetDestPathResolver = 'android' | 'generic';
13+
import {AssetRegistry} from 'react-native';
1514

16-
export type PackagerAsset = {
17-
readonly __packager_asset: boolean,
18-
readonly fileSystemLocation: string,
19-
readonly httpServerLocation: string,
20-
readonly width: ?number,
21-
readonly height: ?number,
22-
readonly scales: Array<number>,
23-
readonly hash: string,
24-
readonly name: string,
25-
readonly type: string,
26-
readonly resolver?: AssetDestPathResolver,
27-
...
28-
};
15+
/*::
16+
export type {AssetDestPathResolver, PackagerAsset} from 'react-native';
2917
*/
3018

31-
const assets /*: Array<PackagerAsset> */ = [];
32-
33-
function registerAsset(asset /*: PackagerAsset */) /*: number */ {
34-
// `push` returns new array length, so the first asset will
35-
// get id 1 (not 0) to make the value truthy
36-
return assets.push(asset);
37-
}
38-
39-
function getAssetByID(assetId /*: number */) /*: PackagerAsset */ {
40-
return assets[assetId - 1];
41-
}
42-
4319
module.exports = {
44-
registerAsset,
45-
getAssetByID,
20+
registerAsset: AssetRegistry.registerAsset,
21+
getAssetByID: AssetRegistry.getAssetByID,
4622
};

packages/react-native/Libraries/Image/AssetSourceResolver.js

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,10 @@
1010

1111
'use strict';
1212

13-
export type ResolvedAssetSource = {
14-
readonly __packager_asset: boolean,
15-
readonly width: ?number,
16-
readonly height: ?number,
17-
readonly uri: string,
18-
readonly scale: number,
19-
};
20-
21-
// From @react-native/assets-registry
22-
type AssetDestPathResolver = 'android' | 'generic';
23-
24-
// From @react-native/assets-registry
25-
type PackagerAsset = Readonly<{
26-
__packager_asset: boolean,
27-
fileSystemLocation: string,
28-
httpServerLocation: string,
29-
width: ?number,
30-
height: ?number,
31-
scales: Array<number>,
32-
hash: string,
33-
name: string,
34-
type: string,
35-
resolver?: AssetDestPathResolver,
36-
...
37-
}>;
13+
import type {
14+
AssetDestPathResolver,
15+
PackagerAsset,
16+
} from '../../src/private/assets/AssetRegistry';
3817

3918
const PixelRatio = require('../Utilities/PixelRatio').default;
4019
const Platform = require('../Utilities/Platform').default;
@@ -46,6 +25,14 @@ const {
4625
} = require('@react-native/assets-registry/path-support');
4726
const invariant = require('invariant');
4827

28+
export type ResolvedAssetSource = {
29+
readonly __packager_asset: boolean,
30+
readonly width: ?number,
31+
readonly height: ?number,
32+
readonly uri: string,
33+
readonly scale: number,
34+
};
35+
4936
/**
5037
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
5138
*/

packages/react-native/Libraries/Image/RelativeImageStub.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// This is a stub for flow to make it understand require('./icon.png')
1414
// See metro/src/Bundler/index.js
1515

16-
const AssetRegistry = require('@react-native/assets-registry/registry');
16+
const {AssetRegistry} = require('../../src/private/assets/AssetRegistry');
1717

1818
const RelativeImageStub = AssetRegistry.registerAsset({
1919
__packager_asset: true,

packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
import type {PackagerAsset} from '../../../../assets-registry/registry';
11+
import type {PackagerAsset} from '../../../src/private/assets/AssetRegistry';
1212
import type {ResolvedAssetSource} from '../AssetSourceResolver';
1313

1414
describe('resolveAssetSource', () => {
@@ -20,7 +20,8 @@ describe('resolveAssetSource', () => {
2020
beforeEach(() => {
2121
jest.resetModules();
2222

23-
AssetRegistry = require('@react-native/assets-registry/registry');
23+
AssetRegistry =
24+
require('../../../src/private/assets/AssetRegistry').AssetRegistry;
2425
resolveAssetSource = require('../resolveAssetSource').default;
2526
NativeSourceCode =
2627
require('../../NativeModules/specs/NativeSourceCode').default;

packages/react-native/Libraries/Image/resolveAssetSource.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import type {ImageSource} from './ImageSource';
1616

1717
import SourceCode from '../NativeModules/specs/NativeSourceCode';
1818

19+
const {AssetRegistry} = require('../../src/private/assets/AssetRegistry');
1920
const AssetSourceResolver: AssetSourceResolverT =
2021
require('./AssetSourceResolver').default;
2122
const {pickScale} = require('./AssetUtils');
22-
const AssetRegistry = require('@react-native/assets-registry/registry');
2323

2424
type CustomSourceTransformer = (
2525
resolver: AssetSourceResolver,

packages/react-native/ReactNativeApi.d.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<e62248f4d4932263a592c35ebf383e1a>>
7+
* @generated SignedSource<<a2099475ab89899f0854f5f13f5d1af7>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -151,6 +151,10 @@ declare const AnimatedScrollView_default: AnimatedComponentType<
151151
>
152152
declare const AppState: typeof AppState_default
153153
declare const AppState_default: AppStateImpl
154+
declare const AssetRegistry: {
155+
getAssetByID(assetId: number): PackagerAsset
156+
registerAsset(asset: PackagerAsset): number
157+
}
154158
declare const attachNativeEvent: typeof $$AnimatedImplementation.attachNativeEvent
155159
declare const BackHandler: typeof BackHandler_default
156160
declare const BackHandler_default: TBackHandler
@@ -1628,6 +1632,8 @@ declare interface ArrayLike_2<T> extends Iterable<T> {
16281632
[indexer: number]: T
16291633
readonly length: number
16301634
}
1635+
declare type AssetDestPathResolver = "android" | "generic"
1636+
declare type AssetRegistry = typeof AssetRegistry
16311637
declare type attachNativeEvent = typeof attachNativeEvent
16321638
declare function attachNativeEventImpl(
16331639
viewRef: any,
@@ -3469,6 +3475,17 @@ declare type OptionalVirtualizedSectionListProps<
34693475
declare type OrientationChangeEvent = {
34703476
readonly orientation: "landscape" | "portrait"
34713477
}
3478+
declare type PackagerAsset = {
3479+
readonly fileSystemLocation: string
3480+
readonly hash: string
3481+
readonly height: number | undefined
3482+
readonly httpServerLocation: string
3483+
readonly name: string
3484+
readonly resolver?: AssetDestPathResolver
3485+
readonly scales: Array<number>
3486+
readonly type: string
3487+
readonly width: number | undefined
3488+
}
34723489
declare type PanResponder = typeof PanResponder
34733490
declare type PanResponderCallbacks = {
34743491
readonly onMoveShouldSetPanResponder?: ActiveCallback
@@ -5915,6 +5932,8 @@ export {
59155932
AppStateEvent, // 80f034c3
59165933
AppStateStatus, // 447e5ef2
59175934
Appearance, // 83e9641a
5935+
AssetDestPathResolver, // 59047424
5936+
AssetRegistry, // 6070bb45
59185937
AutoCapitalize, // c0e857a0
59195938
BackHandler, // f139fc69
59205939
BackPressEventName, // 4620fb76
@@ -6046,6 +6065,7 @@ export {
60466065
NativeUIEvent, // 44ac26ac
60476066
Networking, // bbc5be42
60486067
OpaqueColorValue, // 25f3fa5b
6068+
PackagerAsset, // d1c88cf4
60496069
PanResponder, // f8f71cac
60506070
PanResponderCallbacks, // 6d63e7be
60516071
PanResponderGestureState, // 54baf558

packages/react-native/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ module.exports = {
211211
get AppState() {
212212
return require('./Libraries/AppState/AppState').default;
213213
},
214+
get AssetRegistry() {
215+
return require('./src/private/assets/AssetRegistry').AssetRegistry;
216+
},
214217
get BackHandler() {
215218
return require('./Libraries/Utilities/BackHandler').default;
216219
},

packages/react-native/index.js.flow

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ export type {
276276
} from './Libraries/AppState/AppState';
277277
export {default as AppState} from './Libraries/AppState/AppState';
278278

279+
export {AssetRegistry} from './src/private/assets/AssetRegistry';
280+
export type {
281+
AssetDestPathResolver,
282+
PackagerAsset,
283+
} from './src/private/assets/AssetRegistry';
284+
279285
export type {BackPressEventName} from './Libraries/Utilities/BackHandler';
280286
export {default as BackHandler} from './Libraries/Utilities/BackHandler';
281287

0 commit comments

Comments
 (0)