Skip to content

Commit 4bbcf2f

Browse files
huntiefacebook-github-bot
authored andcommitted
Create @react-native/asset-utils package (#57368)
Summary: **Context** This stack intends to relocate the `AssetRegistry` API from `react-native/assets-registry` into `react-native` — addressing runtime safety and public deep imports issues (1.0 and JS Stable API blockers). **This diff** Create a new `react-native/asset-utils` package, intended for internal/framework use (read: **will not be an end user concern**). This re-houses `react-native/assets-registry/path-support.js`. Stacked with the next two diffs, this contributes to the 0.87 objective to delete `react-native/assets-registry` towards install-layout-safe behaviour and JS deep import removal. - (Temporarily, this leaves two copies of `path-support.js` until I enact the package removal down the stack.) This change also enables us to de-duplicate `assetPathUtils.js` inside `community-cli-plugin` and use one source of truth. - Additionally, we have fbsource consumers of this util that cannot have a circular Buck dep on `react-native`, so the main package was not a suitable relocation point (secondarily to not adding an awkward new public API here). **Changes** - Add `react-native/asset-utils` (`packages/asset-utils/`): `src/AssetPathUtils.js` containing Android path helpers and tests. - De-duplicate usages in `community-cli-plugin`: delete `src/commands/bundle/assetPathUtils.js`. Changelog: [General][Added] - Introduce `react-native/asset-utils` package (relocates Android path utils for libraries/frameworks) Differential Revision: D110045272
1 parent b364490 commit 4bbcf2f

19 files changed

Lines changed: 294 additions & 115 deletions

packages/asset-utils/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# @react-native/asset-utils
2+
3+
[![npm]](https://www.npmjs.com/package/@react-native/asset-utils) [![npm downloads]](https://www.npmjs.com/package/@react-native/asset-utils)
4+
5+
[npm]: https://img.shields.io/npm/v/@react-native/asset-utils.svg?color=blue
6+
[npm downloads]: https://img.shields.io/npm/dm/@react-native/asset-utils.svg
7+
8+
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.
9+
10+
## API
11+
12+
```js
13+
import {
14+
getAndroidResourceFolderName,
15+
getAndroidResourceIdentifier,
16+
} from '@react-native/asset-utils';
17+
```
18+
19+
| Export | Signature | Notes |
20+
|---|---|---|
21+
| `getAndroidResourceFolderName` | `(asset: PackagerAsset, scale: number) => string` | e.g. `drawable-xhdpi`; non-drawable types resolve to `raw` |
22+
| `getAndroidResourceIdentifier` | `(asset: PackagerAsset) => string` | Sanitised resource name |
23+
| `drawableFileTypes` | `Set<string>` | Asset types that map to a `drawable-*` folder |

packages/asset-utils/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@react-native/asset-utils",
3+
"version": "0.87.0-main",
4+
"description": "Asset path utilities for React Native.",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/react/react-native.git",
9+
"directory": "packages/asset-utils"
10+
},
11+
"homepage": "https://github.com/react/react-native/tree/HEAD/packages/asset-utils#readme",
12+
"keywords": [
13+
"react-native"
14+
],
15+
"bugs": "https://github.com/react/react-native/issues",
16+
"engines": {
17+
"node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
18+
},
19+
"exports": {
20+
".": "./src/index.js",
21+
"./package.json": "./package.json"
22+
},
23+
"files": [
24+
"src",
25+
"README.md",
26+
"!**/__docs__/**",
27+
"!**/__fixtures__/**",
28+
"!**/__mocks__/**",
29+
"!**/__tests__/**"
30+
]
31+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
export type PackagerAsset = Readonly<{
11+
httpServerLocation: string;
12+
name: string;
13+
type: string;
14+
}>;
15+
16+
export function getAndroidResourceFolderName(
17+
asset: PackagerAsset,
18+
scale: number,
19+
): string;
20+
21+
export function getAndroidResourceIdentifier(asset: PackagerAsset): string;
22+
23+
export const drawableFileTypes: Set<string>;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
/*::
14+
// Conforms to the `PackagerAsset` type from `react-native`.
15+
export type PackagerAsset = Readonly<{
16+
httpServerLocation: string,
17+
name: string,
18+
type: string,
19+
...
20+
}>;
21+
*/
22+
23+
const androidScaleSuffix /*: {[string]: string} */ = {
24+
'0.75': 'ldpi',
25+
'1': 'mdpi',
26+
'1.5': 'hdpi',
27+
'2': 'xhdpi',
28+
'3': 'xxhdpi',
29+
'4': 'xxxhdpi',
30+
};
31+
32+
const ANDROID_BASE_DENSITY = 160;
33+
34+
// FIXME: Using number to represent discrete scale numbers is fragile in
35+
// essence because of floating point number imprecision.
36+
function getAndroidAssetSuffix(scale /*: number */) /*: string */ {
37+
if (scale.toString() in androidScaleSuffix) {
38+
return androidScaleSuffix[scale.toString()];
39+
}
40+
41+
// NOTE: Android Gradle Plugin does not fully support the nnndpi format.
42+
// See https://issuetracker.google.com/issues/72884435
43+
if (Number.isFinite(scale) && scale > 0) {
44+
return Math.round(scale * ANDROID_BASE_DENSITY) + 'dpi';
45+
}
46+
47+
throw new Error('no such scale ' + scale.toString());
48+
}
49+
50+
// See https://developer.android.com/guide/topics/resources/drawable-resource.html
51+
const drawableFileTypes /*: Set<string> */ = new Set([
52+
'gif',
53+
'heic',
54+
'heif',
55+
'jpeg',
56+
'jpg',
57+
'ktx',
58+
'png',
59+
'webp',
60+
'xml',
61+
]);
62+
63+
function getAndroidResourceFolderName(
64+
asset /*: PackagerAsset */,
65+
scale /*: number */,
66+
) /*: string */ {
67+
if (!drawableFileTypes.has(asset.type)) {
68+
return 'raw';
69+
}
70+
71+
return 'drawable-' + getAndroidAssetSuffix(scale);
72+
}
73+
74+
function getAndroidResourceIdentifier(
75+
asset /*: PackagerAsset */,
76+
) /*: string */ {
77+
return (getBasePath(asset) + '/' + asset.name)
78+
.toLowerCase()
79+
.replace(/\//g, '_') // Encode folder structure in file name
80+
.replace(/([^a-z0-9_])/g, '') // Remove illegal chars
81+
.replace(/^(?:assets|assetsunstable_path)_/, ''); // Remove "assets_" or "assetsunstable_path_" prefix
82+
}
83+
84+
function getBasePath(asset /*: PackagerAsset */) /*: string */ {
85+
const basePath = asset.httpServerLocation;
86+
return basePath.startsWith('/') ? basePath.slice(1) : basePath;
87+
}
88+
89+
module.exports = {
90+
drawableFileTypes,
91+
getAndroidResourceFolderName,
92+
getAndroidResourceIdentifier,
93+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import {getAndroidResourceFolderName} from '../AndroidPathUtils';
12+
13+
const DRAWABLE_ASSET = {
14+
httpServerLocation: '/assets/',
15+
name: 'foo',
16+
type: 'png',
17+
};
18+
19+
const NON_DRAWABLE_ASSET = {
20+
httpServerLocation: '/assets/',
21+
name: 'foo',
22+
type: 'txt',
23+
};
24+
25+
describe('getAndroidResourceFolderName', () => {
26+
test('supports the six primary density buckets', () => {
27+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 0.75)).toBe(
28+
'drawable-ldpi',
29+
);
30+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1)).toBe(
31+
'drawable-mdpi',
32+
);
33+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.5)).toBe(
34+
'drawable-hdpi',
35+
);
36+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 2)).toBe(
37+
'drawable-xhdpi',
38+
);
39+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 3)).toBe(
40+
'drawable-xxhdpi',
41+
);
42+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 4)).toBe(
43+
'drawable-xxxhdpi',
44+
);
45+
});
46+
47+
test('supports nonstandard densities', () => {
48+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.25)).toBe(
49+
'drawable-200dpi',
50+
);
51+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.66)).toBe(
52+
'drawable-266dpi',
53+
);
54+
expect(getAndroidResourceFolderName(DRAWABLE_ASSET, 1.33)).toBe(
55+
'drawable-213dpi',
56+
); // ~tvdpi
57+
});
58+
59+
test('throws if the density cannot be processed', () => {
60+
expect(() => getAndroidResourceFolderName(DRAWABLE_ASSET, -1)).toThrow();
61+
expect(() => getAndroidResourceFolderName(DRAWABLE_ASSET, 0)).toThrow();
62+
expect(() =>
63+
getAndroidResourceFolderName(DRAWABLE_ASSET, Infinity),
64+
).toThrow();
65+
});
66+
67+
test('returns "raw" for non-drawables', () => {
68+
expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 0.75)).toBe('raw');
69+
expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 1)).toBe('raw');
70+
expect(getAndroidResourceFolderName(NON_DRAWABLE_ASSET, 1.25)).toBe('raw');
71+
});
72+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
export * from './AndroidPathUtils';

packages/asset-utils/src/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
/*::
14+
export type {PackagerAsset} from './AndroidPathUtils';
15+
*/
16+
17+
module.exports = require('./AndroidPathUtils');

packages/community-cli-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"prepack": "node ../../scripts/build/prepack.js"
3232
},
3333
"dependencies": {
34+
"@react-native/asset-utils": "0.87.0-main",
3435
"@react-native/dev-middleware": "0.87.0-main",
3536
"debug": "^4.4.0",
3637
"invariant": "^2.2.4",

packages/community-cli-plugin/src/commands/bundle/__tests__/filterPlatformAssetScales-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import filterPlatformAssetScales from '../filterPlatformAssetScales';
1212

13-
jest.dontMock('../filterPlatformAssetScales').dontMock('../assetPathUtils');
13+
jest.dontMock('../filterPlatformAssetScales');
1414

1515
describe('filterPlatformAssetScales', () => {
1616
test('removes everything but 2x and 3x for iOS', () => {

packages/community-cli-plugin/src/commands/bundle/__tests__/getAssetDestPathAndroid-test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import getAssetDestPathAndroid from '../getAssetDestPathAndroid';
1212

1313
const path = require('path');
1414

15-
jest.dontMock('../getAssetDestPathAndroid').dontMock('../assetPathUtils');
15+
jest
16+
.dontMock('../getAssetDestPathAndroid')
17+
.dontMock('@react-native/asset-utils');
1618

1719
describe('getAssetDestPathAndroid', () => {
1820
test('should use the right destination folder', () => {

0 commit comments

Comments
 (0)