Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
23 changes: 23 additions & 0 deletions packages/asset-utils/README.md
Original file line number Diff line number Diff line change
@@ -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<string>` | Asset types that map to a `drawable-*` folder |
31 changes: 31 additions & 0 deletions packages/asset-utils/package.json
Original file line number Diff line number Diff line change
@@ -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__/**"
]
}
23 changes: 23 additions & 0 deletions packages/asset-utils/src/AndroidPathUtils.d.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
93 changes: 93 additions & 0 deletions packages/asset-utils/src/AndroidPathUtils.js
Original file line number Diff line number Diff line change
@@ -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<string> */ = 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,
};
72 changes: 72 additions & 0 deletions packages/asset-utils/src/__tests__/AndroidPathUtils-test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
17 changes: 17 additions & 0 deletions packages/asset-utils/src/index.js
Original file line number Diff line number Diff line change
@@ -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');
8 changes: 4 additions & 4 deletions packages/assets-registry/README.md
Original file line number Diff line number Diff line change
@@ -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
![npm package](https://img.shields.io/npm/v/@react-native/assets-registry?color=brightgreen&label=npm%20package)

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.

Expand All @@ -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 |
Expand Down
5 changes: 4 additions & 1 deletion packages/assets-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@
"!**/__fixtures__/**",
"!**/__mocks__/**",
"!**/__tests__/**"
]
],
"peerDependencies": {
"react-native": "*"
}
}
2 changes: 1 addition & 1 deletion packages/assets-registry/path-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand Down
37 changes: 8 additions & 29 deletions packages/assets-registry/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Key change 1: Singleton module is located in the typically deduplicated react-native package (both conventionally in the ecosystem and specifically requested in this case by Expo).


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

Check warning on line 19 in packages/assets-registry/registry.js

View workflow job for this annotation

GitHub Actions / test_js (22.13.0)

'@react-native/monorepo/no-commonjs-exports' rule is disabled but never reported

Check warning on line 19 in packages/assets-registry/registry.js

View workflow job for this annotation

GitHub Actions / test_js (24)

'@react-native/monorepo/no-commonjs-exports' rule is disabled but never reported
module.exports = {registerAsset, getAssetByID};
module.exports = {
registerAsset: AssetRegistry.registerAsset,
getAssetByID: AssetRegistry.getAssetByID,
};
1 change: 1 addition & 0 deletions packages/community-cli-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading
Loading