Skip to content

Commit 534bd79

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Move AssetRegistry implementation into main package, expose as public API (#57369)
Summary: Pull Request resolved: #57369 **Problem** The separate `react-native/assets-registry` package includes a longstanding ecosystem footgun. `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`. **Key changes** NOTE: **Reviewer note**: Browsing file changes on GitHub may be more focused — https://github.com/react/react-native/pull/57369/changes NOTE: Squash of #57233 (D108750302) and #57232 (D108750303) `'react-native'`: - Add new `AssetRegistry` API, along with the `PackagerAsset` and `AssetDestPathResolver` root type exports in `react-native`. - Add a new `'react-native/asset-registry'` secondary entry point — intended for Metro's `transformer.assetRegistryPath` config contract. `react-native/assets-registry`: - Update 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. - **✅ Fixed**: Deep import `'react-native/Libraries/Image/AssetRegistry'` dependency removed (migrated in `react-native/metro-config`). 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`) - [General][Breaking] - `react-native/Libraries/Image/AssetRegistry` is removed. Please use the `AssetRegistry` API (apps/library code) and/or the `react-native/asset-registry` entrypoint (Metro/build configs). Differential Revision: D109019622
1 parent 722e89d commit 534bd79

22 files changed

Lines changed: 163 additions & 86 deletions

File tree

.eslintrc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ module.exports = {
5050
files: [
5151
'./packages/react-native/Libraries/**/*.{js,flow}',
5252
'./packages/react-native/src/**/*.{js,flow}',
53-
'./packages/assets-registry/registry.js',
5453
],
5554
parser: 'hermes-eslint',
5655
rules: {

packages/assets-registry/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# @react-native/assets-registry
22

3-
[![npm]](https://www.npmjs.com/package/@react-native/assets-registry) [![npm downloads]](https://www.npmjs.com/package/@react-native/assets-registry)
4-
5-
[npm]: https://img.shields.io/npm/v/@react-native/assets-registry.svg?color=blue
6-
[npm downloads]: https://img.shields.io/npm/dm/@react-native/assets-registry.svg
3+
![npm package](https://img.shields.io/npm/v/@react-native/assets-registry?color=brightgreen&label=npm%20package)
74

85
Runtime registry that maps asset IDs generated in a Metro bundle to asset metadata. It backs `<Image>`, `Image.resolveAssetSource()`, and any code that resolves `require('./img.png')` on native.
96

@@ -13,6 +10,9 @@ Most apps never import this directly — assets are handled through `<Image>`.
1310

1411
### `@react-native/assets-registry/registry`
1512

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+
1616
| Export | Signature | Notes |
1717
|---|---|---|
1818
| `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
@@ -26,5 +26,8 @@
2626
"!**/__fixtures__/**",
2727
"!**/__mocks__/**",
2828
"!**/__tests__/**"
29-
]
29+
],
30+
"peerDependencies": {
31+
"react-native": "*"
32+
}
3033
}

packages/assets-registry/path-support.js

Lines changed: 1 addition & 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-
* @flow strict
7+
* @flow strict-local
88
* @format
99
*/
1010

packages/assets-registry/registry.js

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,20 @@
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
// eslint-disable-next-line @react-native/monorepo/no-commonjs-exports
44-
module.exports = {registerAsset, getAssetByID};
20+
module.exports = {
21+
registerAsset: AssetRegistry.registerAsset,
22+
getAssetByID: AssetRegistry.getAssetByID,
23+
};

packages/eslint-plugin-react-native/no-deep-imports.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
if (
3333
!isDeepReactNativeImport(node.source) ||
3434
isInitializeCoreImport(node.source) ||
35+
isSecondaryEntryPoint(node.source) ||
3536
isFbInternalImport(node.source)
3637
) {
3738
return;
@@ -88,6 +89,7 @@ module.exports = {
8889
if (
8990
!isDeepRequire(node) ||
9091
isInitializeCoreImport(node.arguments[0]) ||
92+
isSecondaryEntryPoint(node.arguments[0]) ||
9193
isFbInternalImport(node.arguments[0])
9294
) {
9395
return;
@@ -173,6 +175,14 @@ module.exports = {
173175
return source.value === 'react-native/Libraries/Core/InitializeCore';
174176
}
175177

178+
function isSecondaryEntryPoint(source) {
179+
if (source.type !== 'Literal' || typeof source.value !== 'string') {
180+
return false;
181+
}
182+
183+
return source.value === 'react-native/asset-registry';
184+
}
185+
176186
function isFbInternalImport(source) {
177187
if (source.type !== 'Literal' || typeof source.value !== 'string') {
178188
return false;

packages/metro-config/src/index.flow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function getDefaultConfig(projectRoot: string): ConfigT {
8484
},
8585
transformer: {
8686
allowOptionalDependencies: true,
87-
assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
87+
assetRegistryPath: 'react-native/asset-registry',
8888
asyncRequireModulePath: require.resolve(
8989
'metro-runtime/src/modules/asyncRequire',
9090
),

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

Lines changed: 0 additions & 14 deletions
This file was deleted.

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;
@@ -45,6 +24,14 @@ const {
4524
} = require('@react-native/asset-utils');
4625
const invariant = require('invariant');
4726

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

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,

0 commit comments

Comments
 (0)