mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
Move AssetRegistry implementation into main package, expose as public API (#57369)
Summary: Pull Request resolved: https://github.com/react/react-native/pull/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 https://github.com/react/react-native/pull/57233 (D108750302) and https://github.com/react/react-native/pull/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). Reviewed By: robhogan Differential Revision: D109019622 fbshipit-source-id: 75599a94a9aba084a1266f2128c448379d1596cd
This commit is contained in:
committed by
meta-codesync[bot]
parent
41d52189e2
commit
6cfde8f296
@@ -50,7 +50,6 @@ module.exports = {
|
||||
files: [
|
||||
'./packages/react-native/Libraries/**/*.{js,flow}',
|
||||
'./packages/react-native/src/**/*.{js,flow}',
|
||||
'./packages/assets-registry/registry.js',
|
||||
],
|
||||
parser: 'hermes-eslint',
|
||||
rules: {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# @react-native/assets-registry
|
||||
|
||||
[![npm]](https://www.npmjs.com/package/@react-native/assets-registry) [![npm downloads]](https://www.npmjs.com/package/@react-native/assets-registry)
|
||||
|
||||
[npm]: https://img.shields.io/npm/v/@react-native/assets-registry.svg?color=blue
|
||||
[npm downloads]: https://img.shields.io/npm/dm/@react-native/assets-registry.svg
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -13,6 +10,9 @@ Most apps never import this directly — assets are handled through `<Image>`.
|
||||
|
||||
### `@react-native/assets-registry/registry`
|
||||
|
||||
> [!Note]
|
||||
> Aliases to [`AssetRegistry`](https://reactnative.dev/docs/assetregistry) (since 0.87). Prefer importing directly from the `'react-native'` package in libraries.
|
||||
|
||||
| Export | Signature | Notes |
|
||||
|---|---|---|
|
||||
| `registerAsset` | `(asset: PackagerAsset) => number` | Stores the asset; returns a numeric ID |
|
||||
|
||||
@@ -26,5 +26,8 @@
|
||||
"!**/__fixtures__/**",
|
||||
"!**/__mocks__/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,41 +4,20 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*::
|
||||
export type AssetDestPathResolver = 'android' | 'generic';
|
||||
import {AssetRegistry} from 'react-native';
|
||||
|
||||
export type PackagerAsset = {
|
||||
readonly __packager_asset: boolean,
|
||||
readonly fileSystemLocation: string,
|
||||
readonly httpServerLocation: string,
|
||||
readonly width: ?number,
|
||||
readonly height: ?number,
|
||||
readonly scales: Array<number>,
|
||||
readonly hash: string,
|
||||
readonly name: string,
|
||||
readonly type: string,
|
||||
readonly resolver?: AssetDestPathResolver,
|
||||
...
|
||||
};
|
||||
/*::
|
||||
export type {AssetDestPathResolver, PackagerAsset} from 'react-native';
|
||||
*/
|
||||
|
||||
const assets /*: Array<PackagerAsset> */ = [];
|
||||
|
||||
function registerAsset(asset /*: PackagerAsset */) /*: number */ {
|
||||
// `push` returns new array length, so the first asset will
|
||||
// get id 1 (not 0) to make the value truthy
|
||||
return assets.push(asset);
|
||||
}
|
||||
|
||||
function getAssetByID(assetId /*: number */) /*: PackagerAsset */ {
|
||||
return assets[assetId - 1];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @react-native/monorepo/no-commonjs-exports
|
||||
module.exports = {registerAsset, getAssetByID};
|
||||
module.exports = {
|
||||
registerAsset: AssetRegistry.registerAsset,
|
||||
getAssetByID: AssetRegistry.getAssetByID,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ module.exports = {
|
||||
if (
|
||||
!isDeepReactNativeImport(node.source) ||
|
||||
isInitializeCoreImport(node.source) ||
|
||||
isSecondaryEntryPoint(node.source) ||
|
||||
isFbInternalImport(node.source)
|
||||
) {
|
||||
return;
|
||||
@@ -88,6 +89,7 @@ module.exports = {
|
||||
if (
|
||||
!isDeepRequire(node) ||
|
||||
isInitializeCoreImport(node.arguments[0]) ||
|
||||
isSecondaryEntryPoint(node.arguments[0]) ||
|
||||
isFbInternalImport(node.arguments[0])
|
||||
) {
|
||||
return;
|
||||
@@ -173,6 +175,14 @@ module.exports = {
|
||||
return source.value === 'react-native/Libraries/Core/InitializeCore';
|
||||
}
|
||||
|
||||
function isSecondaryEntryPoint(source) {
|
||||
if (source.type !== 'Literal' || typeof source.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return source.value === 'react-native/asset-registry';
|
||||
}
|
||||
|
||||
function isFbInternalImport(source) {
|
||||
if (source.type !== 'Literal' || typeof source.value !== 'string') {
|
||||
return false;
|
||||
|
||||
@@ -84,7 +84,7 @@ export function getDefaultConfig(projectRoot: string): ConfigT {
|
||||
},
|
||||
transformer: {
|
||||
allowOptionalDependencies: true,
|
||||
assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
|
||||
assetRegistryPath: 'react-native/asset-registry',
|
||||
asyncRequireModulePath: require.resolve(
|
||||
'metro-runtime/src/modules/asyncRequire',
|
||||
),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
* @format
|
||||
*/
|
||||
|
||||
export {
|
||||
registerAsset,
|
||||
getAssetByID,
|
||||
} from '@react-native/assets-registry/registry';
|
||||
+12
-25
@@ -10,31 +10,10 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
export type ResolvedAssetSource = {
|
||||
readonly __packager_asset: boolean,
|
||||
readonly width: ?number,
|
||||
readonly height: ?number,
|
||||
readonly uri: string,
|
||||
readonly scale: number,
|
||||
};
|
||||
|
||||
// From @react-native/assets-registry
|
||||
type AssetDestPathResolver = 'android' | 'generic';
|
||||
|
||||
// From @react-native/assets-registry
|
||||
type PackagerAsset = Readonly<{
|
||||
__packager_asset: boolean,
|
||||
fileSystemLocation: string,
|
||||
httpServerLocation: string,
|
||||
width: ?number,
|
||||
height: ?number,
|
||||
scales: Array<number>,
|
||||
hash: string,
|
||||
name: string,
|
||||
type: string,
|
||||
resolver?: AssetDestPathResolver,
|
||||
...
|
||||
}>;
|
||||
import type {
|
||||
AssetDestPathResolver,
|
||||
PackagerAsset,
|
||||
} from '../../src/private/assets/AssetRegistry';
|
||||
|
||||
const PixelRatio = require('../Utilities/PixelRatio').default;
|
||||
const Platform = require('../Utilities/Platform').default;
|
||||
@@ -45,6 +24,14 @@ const {
|
||||
} = require('@react-native/asset-utils');
|
||||
const invariant = require('invariant');
|
||||
|
||||
export type ResolvedAssetSource = {
|
||||
readonly __packager_asset: boolean,
|
||||
readonly width: ?number,
|
||||
readonly height: ?number,
|
||||
readonly uri: string,
|
||||
readonly scale: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// This is a stub for flow to make it understand require('./icon.png')
|
||||
// See metro/src/Bundler/index.js
|
||||
|
||||
const AssetRegistry = require('@react-native/assets-registry/registry');
|
||||
const {AssetRegistry} = require('../../src/private/assets/AssetRegistry');
|
||||
|
||||
const RelativeImageStub = AssetRegistry.registerAsset({
|
||||
__packager_asset: true,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {PackagerAsset} from '../../../../assets-registry/registry';
|
||||
import type {PackagerAsset} from '../../../src/private/assets/AssetRegistry';
|
||||
import type {ResolvedAssetSource} from '../AssetSourceResolver';
|
||||
|
||||
describe('resolveAssetSource', () => {
|
||||
@@ -20,7 +20,8 @@ describe('resolveAssetSource', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
AssetRegistry = require('@react-native/assets-registry/registry');
|
||||
AssetRegistry =
|
||||
require('../../../src/private/assets/AssetRegistry').AssetRegistry;
|
||||
resolveAssetSource = require('../resolveAssetSource').default;
|
||||
NativeSourceCode =
|
||||
require('../../NativeModules/specs/NativeSourceCode').default;
|
||||
|
||||
@@ -16,10 +16,10 @@ import type {ImageSource} from './ImageSource';
|
||||
|
||||
import SourceCode from '../NativeModules/specs/NativeSourceCode';
|
||||
|
||||
const {AssetRegistry} = require('../../src/private/assets/AssetRegistry');
|
||||
const AssetSourceResolver: AssetSourceResolverT =
|
||||
require('./AssetSourceResolver').default;
|
||||
const {pickScale} = require('./AssetUtils');
|
||||
const AssetRegistry = require('@react-native/assets-registry/registry');
|
||||
|
||||
type CustomSourceTransformer = (
|
||||
resolver: AssetSourceResolver,
|
||||
|
||||
+21
-1
@@ -4,7 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @generated SignedSource<<82e308c1a4f2f713285c7586ff3eb618>>
|
||||
* @generated SignedSource<<637f1d0b012ebca68c9e020a52eb2803>>
|
||||
*
|
||||
* This file was generated by scripts/js-api/build-types/index.js.
|
||||
*/
|
||||
@@ -151,6 +151,10 @@ declare const AnimatedScrollView_default: AnimatedComponentType<
|
||||
>
|
||||
declare const AppState: typeof AppState_default
|
||||
declare const AppState_default: AppStateImpl
|
||||
declare const AssetRegistry: {
|
||||
getAssetByID(assetId: number): PackagerAsset
|
||||
registerAsset(asset: PackagerAsset): number
|
||||
}
|
||||
declare const attachNativeEvent: typeof $$AnimatedImplementation.attachNativeEvent
|
||||
declare const BackHandler: typeof BackHandler_default
|
||||
declare const BackHandler_default: TBackHandler
|
||||
@@ -1645,6 +1649,8 @@ declare interface ArrayLike_2<T> extends Iterable<T> {
|
||||
[indexer: number]: T
|
||||
readonly length: number
|
||||
}
|
||||
declare type AssetDestPathResolver = "android" | "generic"
|
||||
declare type AssetRegistry = typeof AssetRegistry
|
||||
declare type attachNativeEvent = typeof attachNativeEvent
|
||||
declare function attachNativeEventImpl(
|
||||
viewRef: any,
|
||||
@@ -3485,6 +3491,17 @@ declare type OptionalVirtualizedSectionListProps<
|
||||
declare type OrientationChangeEvent = {
|
||||
readonly orientation: "landscape" | "portrait"
|
||||
}
|
||||
declare type PackagerAsset = {
|
||||
readonly fileSystemLocation: string
|
||||
readonly hash: string
|
||||
readonly height: number | undefined
|
||||
readonly httpServerLocation: string
|
||||
readonly name: string
|
||||
readonly resolver?: AssetDestPathResolver
|
||||
readonly scales: Array<number>
|
||||
readonly type: string
|
||||
readonly width: number | undefined
|
||||
}
|
||||
declare type PanResponder = typeof PanResponder
|
||||
declare type PanResponderCallbacks = {
|
||||
readonly onMoveShouldSetPanResponder?: ActiveCallback
|
||||
@@ -5911,6 +5928,8 @@ export {
|
||||
AppStateEvent, // 80f034c3
|
||||
AppStateStatus, // 447e5ef2
|
||||
Appearance, // 83e9641a
|
||||
AssetDestPathResolver, // 59047424
|
||||
AssetRegistry, // 6070bb45
|
||||
AutoCapitalize, // c0e857a0
|
||||
BackHandler, // f139fc69
|
||||
BackPressEventName, // 4620fb76
|
||||
@@ -6044,6 +6063,7 @@ export {
|
||||
NativeUIEvent, // 44ac26ac
|
||||
Networking, // bbc5be42
|
||||
OpaqueColorValue, // 25f3fa5b
|
||||
PackagerAsset, // d1c88cf4
|
||||
PanResponder, // f8f71cac
|
||||
PanResponderCallbacks, // 6d63e7be
|
||||
PanResponderGestureState, // 54baf558
|
||||
|
||||
Vendored
+3
@@ -233,6 +233,9 @@ module.exports = {
|
||||
get AppState() {
|
||||
return require('./Libraries/AppState/AppState').default;
|
||||
},
|
||||
get AssetRegistry() {
|
||||
return require('./src/private/assets/AssetRegistry').AssetRegistry;
|
||||
},
|
||||
get BackHandler() {
|
||||
return require('./Libraries/Utilities/BackHandler').default;
|
||||
},
|
||||
|
||||
@@ -279,6 +279,12 @@ export type {
|
||||
} from './Libraries/AppState/AppState';
|
||||
export {default as AppState} from './Libraries/AppState/AppState';
|
||||
|
||||
export {AssetRegistry} from './src/private/assets/AssetRegistry';
|
||||
export type {
|
||||
AssetDestPathResolver,
|
||||
PackagerAsset,
|
||||
} from './src/private/assets/AssetRegistry';
|
||||
|
||||
export type {BackPressEventName} from './Libraries/Utilities/BackHandler';
|
||||
export {default as BackHandler} from './Libraries/Utilities/BackHandler';
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ const config = {
|
||||
watchFolders: [
|
||||
path.resolve(__dirname, '../../node_modules'),
|
||||
path.resolve(__dirname, '../asset-utils'),
|
||||
path.resolve(__dirname, '../assets-registry'),
|
||||
path.resolve(__dirname, '../normalize-color'),
|
||||
path.resolve(__dirname, '../polyfills'),
|
||||
path.resolve(__dirname, '../virtualized-lists'),
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
"types": null,
|
||||
"default": "./src/*.js"
|
||||
},
|
||||
"./asset-registry": {
|
||||
"types": null,
|
||||
"default": "./src/asset-registry.js"
|
||||
},
|
||||
"./jest-preset": "./jest-preset.js",
|
||||
"./rn-get-polyfills": "./rn-get-polyfills.js",
|
||||
"./src/fb_internal/*": "./src/fb_internal/*",
|
||||
@@ -146,7 +150,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native/asset-utils": "0.87.0-main",
|
||||
"@react-native/assets-registry": "0.87.0-main",
|
||||
"@react-native/codegen": "0.87.0-main",
|
||||
"@react-native/community-cli-plugin": "0.87.0-main",
|
||||
"@react-native/gradle-plugin": "0.87.0-main",
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Secondary react-native/asset-registry entry point.
|
||||
//
|
||||
// This is an untyped secondary entry point intended to be referenced from
|
||||
// Metro's `transformer.assetRegistryPath` config option. This entry point may
|
||||
// also be preferred in server-side code.
|
||||
//
|
||||
// Apps/libraries should use `import {AssetRegistry} from 'react-native'`.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const {AssetRegistry} = require('./private/assets/AssetRegistry');
|
||||
|
||||
/* eslint-disable @react-native/monorepo/no-commonjs-exports */
|
||||
module.exports = {
|
||||
registerAsset: AssetRegistry.registerAsset,
|
||||
getAssetByID: AssetRegistry.getAssetByID,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
* @format
|
||||
*/
|
||||
|
||||
export type AssetDestPathResolver = 'android' | 'generic';
|
||||
|
||||
export type PackagerAsset = Readonly<{
|
||||
__packager_asset: boolean,
|
||||
fileSystemLocation: string,
|
||||
httpServerLocation: string,
|
||||
width: ?number,
|
||||
height: ?number,
|
||||
scales: Array<number>,
|
||||
hash: string,
|
||||
name: string,
|
||||
type: string,
|
||||
resolver?: AssetDestPathResolver,
|
||||
...
|
||||
}>;
|
||||
|
||||
const assets: Array<PackagerAsset> = [];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Most apps do not use this directly — assets are handled through `<Image>`.
|
||||
*/
|
||||
export const AssetRegistry = {
|
||||
/**
|
||||
* Register an asset. Returns the asset ID.
|
||||
*/
|
||||
registerAsset(asset: PackagerAsset): number {
|
||||
// `push` returns the new length, so the first asset gets id 1 (not 0),
|
||||
// keeping ids truthy.
|
||||
return assets.push(asset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a registered asset by ID.
|
||||
*/
|
||||
getAssetByID(assetId: number): PackagerAsset {
|
||||
return assets[assetId - 1];
|
||||
},
|
||||
};
|
||||
@@ -25,7 +25,6 @@ const config = {
|
||||
watchFolders: [
|
||||
path.resolve(__dirname, '../../node_modules'),
|
||||
path.resolve(__dirname, '../asset-utils'),
|
||||
path.resolve(__dirname, '../assets-registry'),
|
||||
path.resolve(__dirname, '../community-cli-plugin'),
|
||||
path.resolve(__dirname, '../dev-middleware'),
|
||||
path.resolve(__dirname, '../new-app-screen'),
|
||||
|
||||
@@ -30,7 +30,6 @@ const config = {
|
||||
watchFolders: [
|
||||
repositoryPath('node_modules'),
|
||||
repositoryPath('packages/asset-utils'),
|
||||
repositoryPath('packages/assets-registry'),
|
||||
repositoryPath('packages/normalize-color'),
|
||||
repositoryPath('packages/polyfills'),
|
||||
repositoryPath('packages/react-native'),
|
||||
|
||||
@@ -155,7 +155,11 @@ async function updatePackageJson(
|
||||
}
|
||||
|
||||
for (const dependency in newPackageVersions) {
|
||||
if (dependency in deps) {
|
||||
if (
|
||||
dependency in deps &&
|
||||
// Preserve wildcard specifiers
|
||||
deps[dependency] !== '*'
|
||||
) {
|
||||
deps[dependency] = newPackageVersions[dependency];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user