diff --git a/packages/asset-utils/README.md b/packages/asset-utils/README.md new file mode 100644 index 000000000000..ebcd7e938e64 --- /dev/null +++ b/packages/asset-utils/README.md @@ -0,0 +1,23 @@ +# @react-native/asset-utils + +[![npm]](https://www.npmjs.com/package/@react-native/asset-utils) [![npm downloads]](https://www.npmjs.com/package/@react-native/asset-utils) + +[npm]: https://img.shields.io/npm/v/@react-native/asset-utils.svg?color=blue +[npm downloads]: https://img.shields.io/npm/dm/@react-native/asset-utils.svg + +Android resource-path helpers used when copying React Native assets into `drawable-*` / `raw` folders. Consumed by bundling and build tooling; most apps never import this directly. + +## API + +```js +import { + getAndroidResourceFolderName, + getAndroidResourceIdentifier, +} from '@react-native/asset-utils'; +``` + +| Export | Signature | Notes | +|---|---|---| +| `getAndroidResourceFolderName` | `(asset: PackagerAsset, scale: number) => string` | e.g. `drawable-xhdpi`; non-drawable types resolve to `raw` | +| `getAndroidResourceIdentifier` | `(asset: PackagerAsset) => string` | Sanitised resource name | +| `drawableFileTypes` | `Set` | Asset types that map to a `drawable-*` folder | diff --git a/packages/asset-utils/package.json b/packages/asset-utils/package.json new file mode 100644 index 000000000000..bec6ea4d6931 --- /dev/null +++ b/packages/asset-utils/package.json @@ -0,0 +1,31 @@ +{ + "name": "@react-native/asset-utils", + "version": "0.87.0-main", + "description": "Asset path utilities for React Native.", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/react/react-native.git", + "directory": "packages/asset-utils" + }, + "homepage": "https://github.com/react/react-native/tree/HEAD/packages/asset-utils#readme", + "keywords": [ + "react-native" + ], + "bugs": "https://github.com/react/react-native/issues", + "engines": { + "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" + }, + "exports": { + ".": "./src/index.js", + "./package.json": "./package.json" + }, + "files": [ + "src", + "README.md", + "!**/__docs__/**", + "!**/__fixtures__/**", + "!**/__mocks__/**", + "!**/__tests__/**" + ] +} diff --git a/packages/asset-utils/src/AndroidPathUtils.d.ts b/packages/asset-utils/src/AndroidPathUtils.d.ts new file mode 100644 index 000000000000..854eecc3338f --- /dev/null +++ b/packages/asset-utils/src/AndroidPathUtils.d.ts @@ -0,0 +1,23 @@ +/** + * 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. + * + * @format + */ + +export type PackagerAsset = Readonly<{ + httpServerLocation: string; + name: string; + type: string; +}>; + +export function getAndroidResourceFolderName( + asset: PackagerAsset, + scale: number, +): string; + +export function getAndroidResourceIdentifier(asset: PackagerAsset): string; + +export const drawableFileTypes: Set; diff --git a/packages/asset-utils/src/AndroidPathUtils.js b/packages/asset-utils/src/AndroidPathUtils.js new file mode 100644 index 000000000000..3b76e82aab02 --- /dev/null +++ b/packages/asset-utils/src/AndroidPathUtils.js @@ -0,0 +1,93 @@ +/** + * 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 + */ + +'use strict'; + +/*:: +// Conforms to the `PackagerAsset` type from `react-native`. +export type PackagerAsset = Readonly<{ + httpServerLocation: string, + name: string, + type: string, + ... +}>; +*/ + +const androidScaleSuffix /*: {[string]: string} */ = { + '0.75': 'ldpi', + '1': 'mdpi', + '1.5': 'hdpi', + '2': 'xhdpi', + '3': 'xxhdpi', + '4': 'xxxhdpi', +}; + +const ANDROID_BASE_DENSITY = 160; + +// FIXME: Using number to represent discrete scale numbers is fragile in +// essence because of floating point number imprecision. +function getAndroidAssetSuffix(scale /*: number */) /*: string */ { + if (scale.toString() in androidScaleSuffix) { + return androidScaleSuffix[scale.toString()]; + } + + // NOTE: Android Gradle Plugin does not fully support the nnndpi format. + // See https://issuetracker.google.com/issues/72884435 + if (Number.isFinite(scale) && scale > 0) { + return Math.round(scale * ANDROID_BASE_DENSITY) + 'dpi'; + } + + throw new Error('no such scale ' + scale.toString()); +} + +// See https://developer.android.com/guide/topics/resources/drawable-resource.html +const drawableFileTypes /*: Set */ = new Set([ + 'gif', + 'heic', + 'heif', + 'jpeg', + 'jpg', + 'ktx', + 'png', + 'webp', + 'xml', +]); + +function getAndroidResourceFolderName( + asset /*: PackagerAsset */, + scale /*: number */, +) /*: string */ { + if (!drawableFileTypes.has(asset.type)) { + return 'raw'; + } + + return 'drawable-' + getAndroidAssetSuffix(scale); +} + +function getAndroidResourceIdentifier( + asset /*: PackagerAsset */, +) /*: string */ { + return (getBasePath(asset) + '/' + asset.name) + .toLowerCase() + .replace(/\//g, '_') // Encode folder structure in file name + .replace(/([^a-z0-9_])/g, '') // Remove illegal chars + .replace(/^(?:assets|assetsunstable_path)_/, ''); // Remove "assets_" or "assetsunstable_path_" prefix +} + +function getBasePath(asset /*: PackagerAsset */) /*: string */ { + const basePath = asset.httpServerLocation; + return basePath.startsWith('/') ? basePath.slice(1) : basePath; +} + +module.exports = { + drawableFileTypes, + getAndroidResourceFolderName, + getAndroidResourceIdentifier, +}; diff --git a/packages/asset-utils/src/__tests__/AndroidPathUtils-test.js b/packages/asset-utils/src/__tests__/AndroidPathUtils-test.js new file mode 100644 index 000000000000..2496c1d9ee01 --- /dev/null +++ b/packages/asset-utils/src/__tests__/AndroidPathUtils-test.js @@ -0,0 +1,72 @@ +/** + * 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 + */ + +import {getAndroidResourceFolderName} from '../AndroidPathUtils'; + +const DRAWABLE_ASSET = { + httpServerLocation: '/assets/', + name: 'foo', + type: 'png', +}; + +const NON_DRAWABLE_ASSET = { + httpServerLocation: '/assets/', + name: 'foo', + type: 'txt', +}; + +describe('getAndroidResourceFolderName', () => { + test('supports the six primary density buckets', () => { + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 0.75)).toBe( + 'drawable-ldpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1)).toBe( + 'drawable-mdpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.5)).toBe( + 'drawable-hdpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 2)).toBe( + 'drawable-xhdpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 3)).toBe( + 'drawable-xxhdpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 4)).toBe( + 'drawable-xxxhdpi', + ); + }); + + test('supports nonstandard densities', () => { + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.25)).toBe( + 'drawable-200dpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.66)).toBe( + 'drawable-266dpi', + ); + expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.33)).toBe( + 'drawable-213dpi', + ); // ~tvdpi + }); + + test('throws if the density cannot be processed', () => { + expect(() => getAndroidResourceFolderName(DRAWABLE_ASSET, -1)).toThrow(); + expect(() => getAndroidResourceFolderName(DRAWABLE_ASSET, 0)).toThrow(); + expect(() => + getAndroidResourceFolderName(DRAWABLE_ASSET, Infinity), + ).toThrow(); + }); + + test('returns "raw" for non-drawables', () => { + expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 0.75)).toBe('raw'); + expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 1)).toBe('raw'); + expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 1.25)).toBe('raw'); + }); +}); diff --git a/packages/react-native/Libraries/Image/AssetRegistry.js b/packages/asset-utils/src/index.d.ts similarity index 65% rename from packages/react-native/Libraries/Image/AssetRegistry.js rename to packages/asset-utils/src/index.d.ts index 3e1785346ff3..c67af666a8a3 100644 --- a/packages/react-native/Libraries/Image/AssetRegistry.js +++ b/packages/asset-utils/src/index.d.ts @@ -4,11 +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 * @format */ -export { - registerAsset, - getAssetByID, -} from '@react-native/assets-registry/registry'; +export * from './AndroidPathUtils'; diff --git a/packages/asset-utils/src/index.js b/packages/asset-utils/src/index.js new file mode 100644 index 000000000000..4fdff4702d04 --- /dev/null +++ b/packages/asset-utils/src/index.js @@ -0,0 +1,17 @@ +/** + * 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 + */ + +'use strict'; + +/*:: +export type {PackagerAsset} from './AndroidPathUtils'; +*/ + +module.exports = require('./AndroidPathUtils'); diff --git a/packages/assets-registry/README.md b/packages/assets-registry/README.md index 791fdc592aa9..1dd81110155e 100644 --- a/packages/assets-registry/README.md +++ b/packages/assets-registry/README.md @@ -1,9 +1,9 @@ # @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 package](https://img.shields.io/npm/v/@react-native/assets-registry?color=brightgreen&label=npm%20package) -[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 +> [!Warning] +> **This package is deprecated (since 0.87)** and will be removed in a future release. Use [`AssetRegistry`](https://reactnative.dev/docs/assetregistry) from `react-native` (or the `react-native/asset-registry` build entry point) instead of `@react-native/assets-registry/registry`, and [`@react-native/asset-utils`](https://www.npmjs.com/package/@react-native/asset-utils) instead of `@react-native/assets-registry/path-support`. Runtime registry that maps asset IDs generated in a Metro bundle to asset metadata. It backs ``, `Image.resolveAssetSource()`, and any code that resolves `require('./img.png')` on native. @@ -11,14 +11,24 @@ Most apps never import this directly — assets are handled through ``. ## API -### `@react-native/assets-registry/registry` +### `@react-native/assets-registry/registry` (DEPRECATED) + +> [!Warning] +> **Deprecated**: Aliases to [`AssetRegistry`](https://reactnative.dev/docs/assetregistry) (since 0.87). +> +> Please use: +> - `import { AssetRegistry } from 'react-native';` (apps/library code) +> - `'react-native/asset-registry'` (entrypoint for Metro/build configs) | Export | Signature | Notes | |---|---|---| | `registerAsset` | `(asset: PackagerAsset) => number` | Stores the asset; returns a numeric ID | | `getAssetByID` | `(assetId: number) => PackagerAsset` | Looks an asset back up by ID | -### `@react-native/assets-registry/path-support` +### `@react-native/assets-registry/path-support` (DEPRECATED) + +> [!Warning] +> **Deprecated**: Use [`@react-native/asset-utils`](https://www.npmjs.com/package/@react-native/asset-utils) (since 0.87). Android resource-path helpers, used when copying assets into `drawable-*` folders. diff --git a/packages/assets-registry/package.json b/packages/assets-registry/package.json index f3c3c9664f06..645604cc9846 100644 --- a/packages/assets-registry/package.json +++ b/packages/assets-registry/package.json @@ -26,5 +26,8 @@ "!**/__fixtures__/**", "!**/__mocks__/**", "!**/__tests__/**" - ] + ], + "peerDependencies": { + "react-native": "*" + } } diff --git a/packages/assets-registry/path-support.d.ts b/packages/assets-registry/path-support.d.ts index abdea750aacd..cddc55f69bac 100644 --- a/packages/assets-registry/path-support.d.ts +++ b/packages/assets-registry/path-support.d.ts @@ -9,11 +9,20 @@ import type {PackagerAsset} from './registry'; +/** + * @deprecated Use `getAndroidResourceFolderName` from `@react-native/asset-utils` instead. + */ export function getAndroidResourceFolderName( asset: PackagerAsset, scale: number, ): string; +/** + * @deprecated Use `getAndroidResourceIdentifier` from `@react-native/asset-utils` instead. + */ export function getAndroidResourceIdentifier(asset: PackagerAsset): string; +/** + * @deprecated Use `@react-native/asset-utils` instead. + */ export function getBasePath(asset: PackagerAsset): string; diff --git a/packages/assets-registry/path-support.js b/packages/assets-registry/path-support.js index c73a2e2bbf2e..f7e51a9e6ecd 100644 --- a/packages/assets-registry/path-support.js +++ b/packages/assets-registry/path-support.js @@ -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 */ diff --git a/packages/assets-registry/registry.d.ts b/packages/assets-registry/registry.d.ts index 7509ef9ba13e..fb8bfcc2a156 100644 --- a/packages/assets-registry/registry.d.ts +++ b/packages/assets-registry/registry.d.ts @@ -7,8 +7,14 @@ * @format */ +/** + * @deprecated Use `import type {AssetDestPathResolver} from 'react-native'` instead. + */ export type AssetDestPathResolver = 'android' | 'generic'; +/** + * @deprecated Use `import type {PackagerAsset} from 'react-native'` instead. + */ export type PackagerAsset = { readonly __packager_asset: boolean; readonly fileSystemLocation: string; @@ -22,6 +28,12 @@ export type PackagerAsset = { readonly resolver?: AssetDestPathResolver | undefined; }; +/** + * @deprecated Use `import {AssetRegistry} from 'react-native'` instead. + */ export function registerAsset(asset: PackagerAsset): number; +/** + * @deprecated Use `import {AssetRegistry} from 'react-native'` instead. + */ export function getAssetByID(assetId: number): PackagerAsset; diff --git a/packages/assets-registry/registry.js b/packages/assets-registry/registry.js index d193d3af1c87..08a0ef59fb35 100644 --- a/packages/assets-registry/registry.js +++ b/packages/assets-registry/registry.js @@ -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, - readonly hash: string, - readonly name: string, - readonly type: string, - readonly resolver?: AssetDestPathResolver, - ... -}; +/*:: +export type {AssetDestPathResolver, PackagerAsset} from 'react-native'; */ -const assets /*: Array */ = []; - -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, +}; diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 966a8e8385d7..1b5a3327ffaf 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -31,6 +31,7 @@ "prepack": "node ../../scripts/build/prepack.js" }, "dependencies": { + "@react-native/asset-utils": "0.87.0-main", "@react-native/dev-middleware": "0.87.0-main", "debug": "^4.4.0", "invariant": "^2.2.4", diff --git a/packages/community-cli-plugin/src/commands/bundle/__tests__/filterPlatformAssetScales-test.js b/packages/community-cli-plugin/src/commands/bundle/__tests__/filterPlatformAssetScales-test.js index 5973fdceb678..d5ff14434f13 100644 --- a/packages/community-cli-plugin/src/commands/bundle/__tests__/filterPlatformAssetScales-test.js +++ b/packages/community-cli-plugin/src/commands/bundle/__tests__/filterPlatformAssetScales-test.js @@ -10,7 +10,7 @@ import filterPlatformAssetScales from '../filterPlatformAssetScales'; -jest.dontMock('../filterPlatformAssetScales').dontMock('../assetPathUtils'); +jest.dontMock('../filterPlatformAssetScales'); describe('filterPlatformAssetScales', () => { test('removes everything but 2x and 3x for iOS', () => { diff --git a/packages/community-cli-plugin/src/commands/bundle/__tests__/getAssetDestPathAndroid-test.js b/packages/community-cli-plugin/src/commands/bundle/__tests__/getAssetDestPathAndroid-test.js index c2198025d691..55d0456bc803 100644 --- a/packages/community-cli-plugin/src/commands/bundle/__tests__/getAssetDestPathAndroid-test.js +++ b/packages/community-cli-plugin/src/commands/bundle/__tests__/getAssetDestPathAndroid-test.js @@ -12,7 +12,9 @@ import getAssetDestPathAndroid from '../getAssetDestPathAndroid'; const path = require('path'); -jest.dontMock('../getAssetDestPathAndroid').dontMock('../assetPathUtils'); +jest + .dontMock('../getAssetDestPathAndroid') + .dontMock('@react-native/asset-utils'); describe('getAssetDestPathAndroid', () => { test('should use the right destination folder', () => { diff --git a/packages/community-cli-plugin/src/commands/bundle/assetCatalogIOS.js b/packages/community-cli-plugin/src/commands/bundle/assetCatalogIOS.js index eefc3c6d5ad9..64577c397e0c 100644 --- a/packages/community-cli-plugin/src/commands/bundle/assetCatalogIOS.js +++ b/packages/community-cli-plugin/src/commands/bundle/assetCatalogIOS.js @@ -10,7 +10,7 @@ import type {AssetData} from 'metro'; -import assetPathUtils from './assetPathUtils'; +import {getAndroidResourceIdentifier} from '@react-native/asset-utils'; import fs from 'fs'; import path from 'path'; @@ -33,7 +33,7 @@ export function getImageSet( asset: AssetData, scales: ReadonlyArray, ): ImageSet { - const fileName = assetPathUtils.getResourceIdentifier(asset); + const fileName = getAndroidResourceIdentifier(asset); return { basePath: path.join(catalogDir, `${fileName}.imageset`), files: scales.map((scale, idx) => { diff --git a/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js b/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js deleted file mode 100644 index 1ac88c6c01ec..000000000000 --- a/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js +++ /dev/null @@ -1,95 +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-local - * @format - */ - -export type PackagerAsset = Readonly<{ - httpServerLocation: string, - name: string, - type: string, - ... -}>; - -/** - * FIXME: using number to represent discrete scale numbers is fragile in essence because of - * floating point numbers imprecision. - */ -function getAndroidAssetSuffix(scale: number): string { - switch (scale) { - case 0.75: - return 'ldpi'; - case 1: - return 'mdpi'; - case 1.5: - return 'hdpi'; - case 2: - return 'xhdpi'; - case 3: - return 'xxhdpi'; - case 4: - return 'xxxhdpi'; - default: - return ''; - } -} - -// See https://developer.android.com/guide/topics/resources/drawable-resource.html -const drawableFileTypes: Set = new Set([ - 'gif', - 'heic', - 'heif', - 'jpeg', - 'jpg', - 'png', - 'webp', - 'xml', -]); - -function getAndroidResourceFolderName( - asset: PackagerAsset, - scale: number, -): string { - if (!drawableFileTypes.has(asset.type)) { - return 'raw'; - } - const suffix = getAndroidAssetSuffix(scale); - if (!suffix) { - throw new Error( - `Don't know which android drawable suffix to use for asset: ${JSON.stringify( - asset, - )}`, - ); - } - const androidFolder = `drawable-${suffix}`; - return androidFolder; -} - -function getResourceIdentifier(asset: PackagerAsset): string { - const folderPath = getBasePath(asset); - return `${folderPath}/${asset.name}` - .toLowerCase() - .replace(/\//g, '_') // Encode folder structure in file name - .replace(/([^a-z0-9_])/g, '') // Remove illegal chars - .replace(/^(?:assets|assetsunstable_path)_/, ''); // Remove "assets_" or "assetsunstable_path_" prefix -} - -function getBasePath(asset: PackagerAsset): string { - let basePath = asset.httpServerLocation; - if (basePath[0] === '/') { - basePath = basePath.substr(1); - } - return basePath; -} - -export default { - drawableFileTypes, - getAndroidAssetSuffix, - getAndroidResourceFolderName, - getResourceIdentifier, - getBasePath, -}; diff --git a/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js b/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js index 479c1bdf0143..29050c74e79c 100644 --- a/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js +++ b/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js @@ -10,7 +10,10 @@ import type {AssetData} from 'metro'; -import assetPathUtils from './assetPathUtils'; +import { + drawableFileTypes, + getAndroidResourceIdentifier, +} from '@react-native/asset-utils'; import fs from 'fs'; import path from 'path'; @@ -23,12 +26,8 @@ async function createKeepFileAsync( } const assetsList = []; for (const asset of assets) { - const prefix = assetPathUtils.drawableFileTypes.has(asset.type) - ? 'drawable' - : 'raw'; - assetsList.push( - `@${prefix}/${assetPathUtils.getResourceIdentifier(asset)}`, - ); + const prefix = drawableFileTypes.has(asset.type) ? 'drawable' : 'raw'; + assetsList.push(`@${prefix}/${getAndroidResourceIdentifier(asset)}`); } const keepPath = path.join(outputDirectory, 'raw/keep.xml'); const content = `\n`; diff --git a/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathAndroid.js b/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathAndroid.js index 6a8c913ce844..d847c6e77087 100644 --- a/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathAndroid.js +++ b/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathAndroid.js @@ -8,17 +8,17 @@ * @format */ -import type {PackagerAsset} from './assetPathUtils'; +import type {PackagerAsset} from '@react-native/asset-utils'; -import assetPathUtils from './assetPathUtils'; +import { + getAndroidResourceFolderName, + getAndroidResourceIdentifier, +} from '@react-native/asset-utils'; import path from 'path'; function getAssetDestPathAndroid(asset: PackagerAsset, scale: number): string { - const androidFolder = assetPathUtils.getAndroidResourceFolderName( - asset, - scale, - ); - const fileName = assetPathUtils.getResourceIdentifier(asset); + const androidFolder = getAndroidResourceFolderName(asset, scale); + const fileName = getAndroidResourceIdentifier(asset); return path.join(androidFolder, `${fileName}.${asset.type}`); } diff --git a/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathIOS.js b/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathIOS.js index 8f4cb0926a28..5c3e37c1dff7 100644 --- a/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathIOS.js +++ b/packages/community-cli-plugin/src/commands/bundle/getAssetDestPathIOS.js @@ -8,7 +8,7 @@ * @format */ -import type {PackagerAsset} from './assetPathUtils'; +import type {PackagerAsset} from '@react-native/asset-utils'; import path from 'path'; diff --git a/packages/eslint-plugin-react-native/no-deep-imports.js b/packages/eslint-plugin-react-native/no-deep-imports.js index 697c89b9d616..6446c70ba6ac 100644 --- a/packages/eslint-plugin-react-native/no-deep-imports.js +++ b/packages/eslint-plugin-react-native/no-deep-imports.js @@ -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; diff --git a/packages/metro-config/src/index.flow.js b/packages/metro-config/src/index.flow.js index 6bba33bd975a..03a9aed5ace4 100644 --- a/packages/metro-config/src/index.flow.js +++ b/packages/metro-config/src/index.flow.js @@ -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', ), diff --git a/packages/react-native/Libraries/Image/AssetSourceResolver.js b/packages/react-native/Libraries/Image/AssetSourceResolver.js index 2b1e9647608a..a515cf096f60 100644 --- a/packages/react-native/Libraries/Image/AssetSourceResolver.js +++ b/packages/react-native/Libraries/Image/AssetSourceResolver.js @@ -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, - 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; @@ -46,6 +25,14 @@ const { } = require('@react-native/assets-registry/path-support'); 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' */ diff --git a/packages/react-native/Libraries/Image/RelativeImageStub.js b/packages/react-native/Libraries/Image/RelativeImageStub.js index 8472f13dc4a9..73d403a4ecbf 100644 --- a/packages/react-native/Libraries/Image/RelativeImageStub.js +++ b/packages/react-native/Libraries/Image/RelativeImageStub.js @@ -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, diff --git a/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js index dfa7d011fa14..ae68d860e004 100644 --- a/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -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; diff --git a/packages/react-native/Libraries/Image/resolveAssetSource.js b/packages/react-native/Libraries/Image/resolveAssetSource.js index e08a6c848868..9da1b5f45a16 100644 --- a/packages/react-native/Libraries/Image/resolveAssetSource.js +++ b/packages/react-native/Libraries/Image/resolveAssetSource.js @@ -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, diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 699c2fff455a..dce9ba845872 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -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<<42200de8ca10d30541e23b67547d9a13>> + * @generated SignedSource<<51a4ff32766e21cd2638ff58450f31f9>> * * 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 @@ -1638,6 +1642,8 @@ declare interface ArrayLike_2 extends Iterable { [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, @@ -3479,6 +3485,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 + readonly type: string + readonly width: number | undefined +} declare type PanResponder = typeof PanResponder declare type PanResponderCallbacks = { readonly onMoveShouldSetPanResponder?: ActiveCallback @@ -5927,6 +5944,8 @@ export { AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 Appearance, // 83e9641a + AssetDestPathResolver, // 59047424 + AssetRegistry, // 6070bb45 AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6060,6 +6079,7 @@ export { NativeUIEvent, // 44ac26ac Networking, // bbc5be42 OpaqueColorValue, // 25f3fa5b + PackagerAsset, // d1c88cf4 PanResponder, // f8f71cac PanResponderCallbacks, // 6d63e7be PanResponderGestureState, // 54baf558 diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 69c3f67ca8b1..3b3c566ccd8c 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -211,6 +211,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; }, diff --git a/packages/react-native/index.js.flow b/packages/react-native/index.js.flow index 53c4818af9ce..9869ed8c5907 100644 --- a/packages/react-native/index.js.flow +++ b/packages/react-native/index.js.flow @@ -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'; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 991e42abc39a..2b5ea7365ab4 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -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/*", diff --git a/packages/react-native/src/asset-registry.js b/packages/react-native/src/asset-registry.js new file mode 100644 index 000000000000..783c998dd5b2 --- /dev/null +++ b/packages/react-native/src/asset-registry.js @@ -0,0 +1,27 @@ +/** + * 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 + */ + +// ---------------------------------------------------------------------------- +// 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'`. +// ---------------------------------------------------------------------------- + +import {AssetRegistry} from './private/assets/AssetRegistry'; + +/* eslint-disable @react-native/monorepo/no-commonjs-exports */ +module.exports = { + registerAsset: AssetRegistry.registerAsset, + getAssetByID: AssetRegistry.getAssetByID, +}; diff --git a/packages/react-native/src/private/assets/AssetRegistry.js b/packages/react-native/src/private/assets/AssetRegistry.js new file mode 100644 index 000000000000..22b716bd37c7 --- /dev/null +++ b/packages/react-native/src/private/assets/AssetRegistry.js @@ -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, + hash: string, + name: string, + type: string, + resolver?: AssetDestPathResolver, + ... +}>; + +const assets: Array = []; + +/** + * Runtime registry that maps asset IDs generated in a Metro bundle to asset + * metadata. It backs ``, `Image.resolveAssetSource()`, and any code + * that resolves `require('./img.png')` on native. + * + * Most apps do not use this directly — assets are handled through ``. + */ +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]; + }, +}; diff --git a/scripts/shared/monorepoUtils.js b/scripts/shared/monorepoUtils.js index 6260b0a19f70..eecb81250a01 100644 --- a/scripts/shared/monorepoUtils.js +++ b/scripts/shared/monorepoUtils.js @@ -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]; } }