mirror of
https://github.com/react/react-native.git
synced 2026-09-29 16:58:04 +08:00
Create @react-native/asset-utils package (#57368)
Summary: Pull Request resolved: https://github.com/react/react-native/pull/57368 **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) Reviewed By: robhogan Differential Revision: D110045272 fbshipit-source-id: 781e75c58ae9ce7f0fa860e66beca3b9990d26ea
This commit is contained in:
committed by
meta-codesync[bot]
parent
a2cd37f828
commit
41d52189e2
@@ -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 |
|
||||
@@ -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
@@ -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>;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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 * from './AndroidPathUtils';
|
||||
@@ -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');
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
+3
-1
@@ -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', () => {
|
||||
|
||||
@@ -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<number>,
|
||||
): ImageSet {
|
||||
const fileName = assetPathUtils.getResourceIdentifier(asset);
|
||||
const fileName = getAndroidResourceIdentifier(asset);
|
||||
return {
|
||||
basePath: path.join(catalogDir, `${fileName}.imageset`),
|
||||
files: scales.map((scale, idx) => {
|
||||
|
||||
@@ -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<string> = new Set<string>([
|
||||
'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,
|
||||
};
|
||||
@@ -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 = `<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="${assetsList.join(',')}" />\n`;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {PackagerAsset} from './assetPathUtils';
|
||||
import type {PackagerAsset} from '@react-native/asset-utils';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ const {pickScale} = require('./AssetUtils');
|
||||
const {
|
||||
getAndroidResourceFolderName,
|
||||
getAndroidResourceIdentifier,
|
||||
getBasePath,
|
||||
} = require('@react-native/assets-registry/path-support');
|
||||
} = require('@react-native/asset-utils');
|
||||
const invariant = require('invariant');
|
||||
|
||||
/**
|
||||
@@ -66,6 +65,11 @@ function getAssetPathInDrawableFolder(asset: PackagerAsset): string {
|
||||
return drawableFolder + '/' + fileName + '.' + asset.type;
|
||||
}
|
||||
|
||||
function getBasePath(asset: PackagerAsset): string {
|
||||
const basePath = asset.httpServerLocation;
|
||||
return basePath.startsWith('/') ? basePath.slice(1) : basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the asset can be loaded over the network.
|
||||
*
|
||||
|
||||
@@ -26,6 +26,7 @@ const config = {
|
||||
// Make Metro able to resolve required packages that might be imported from /packages/react-native
|
||||
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'),
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
||||
@@ -24,6 +24,7 @@ const config = {
|
||||
// Make Metro able to resolve required external dependencies
|
||||
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'),
|
||||
|
||||
@@ -29,6 +29,7 @@ const config = {
|
||||
// Make Metro able to resolve required external dependencies
|
||||
watchFolders: [
|
||||
repositoryPath('node_modules'),
|
||||
repositoryPath('packages/asset-utils'),
|
||||
repositoryPath('packages/assets-registry'),
|
||||
repositoryPath('packages/normalize-color'),
|
||||
repositoryPath('packages/polyfills'),
|
||||
|
||||
Reference in New Issue
Block a user