Files
react-native/packages/react-native/Libraries/Image/AssetSourceResolver.js
T
Peter Abbondanzo 2e80f5acf1 Load XML files on disk by default (#46371)
Summary:
X-link: https://github.com/facebook/metro/pull/1348

Pull Request resolved: https://github.com/facebook/react-native/pull/46371

## Internal

Vector drawable image support was added in D59530172 but importing vector drawable asset types was not supported out of the box. It required custom source transformers like the one added in D60021474. This is because Android cannot load vector drawable XML over the network. Vector drawables are compiled by AAPT as part of the build process. Even though Metro can serve XML, it would never load.

## Summary

This adds some minor checks in the `AssetSourceResolver` to only attempt loading XML asset types from disk on the Android platform. XML assets like vector drawables are precompiled and cannot be served over the network by Metro.

## Changelog

[Android] [Added] - Adds support for importing XML assets as images

Reviewed By: javache

Differential Revision: D62302929

fbshipit-source-id: 01e49ac5b0429d291318984128dfca2dc058149d
2024-09-09 10:18:10 -07:00

203 lines
5.8 KiB
JavaScript

/**
* 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';
export type ResolvedAssetSource = {|
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
|};
import type {
AssetDestPathResolver,
PackagerAsset,
} from '@react-native/assets-registry/registry';
const PixelRatio = require('../Utilities/PixelRatio').default;
const Platform = require('../Utilities/Platform');
const {pickScale} = require('./AssetUtils');
const {
getAndroidResourceFolderName,
getAndroidResourceIdentifier,
getBasePath,
} = require('@react-native/assets-registry/path-support');
const invariant = require('invariant');
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset: PackagerAsset): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const assetDir = getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset: PackagerAsset): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const drawableFolder = getAndroidResourceFolderName(asset, scale);
const fileName = getAndroidResourceIdentifier(asset);
return drawableFolder + '/' + fileName + '.' + asset.type;
}
/**
* Returns true if the asset can be loaded over the network.
*/
function assetSupportsNetworkLoads(asset: PackagerAsset): boolean {
return !(asset.type === 'xml' && Platform.OS === 'android');
}
class AssetSourceResolver {
serverUrl: ?string;
// where the jsbundle is being run from
jsbundleUrl: ?string;
// the asset to resolve
asset: PackagerAsset;
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.jsbundleUrl = jsbundleUrl;
this.asset = asset;
}
isLoadedFromServer(): boolean {
return (
this.serverUrl != null &&
this.serverUrl !== '' &&
assetSupportsNetworkLoads(this.asset)
);
}
isLoadedFromFileSystem(): boolean {
return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://');
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (this.asset.resolver != null) {
return this.getAssetUsingResolver(this.asset.resolver);
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}
getAssetUsingResolver(resolver: AssetDestPathResolver): ResolvedAssetSource {
switch (resolver) {
case 'android':
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
case 'generic':
return this.scaledAssetURLNearBundle();
default:
throw new Error(
"Don't know how to get asset via provided resolver: " +
resolver +
'\nAsset: ' +
JSON.stringify(this.asset, null, '\t') +
'\nPossible resolvers are:' +
JSON.stringify(['android', 'generic'], null, '\t'),
);
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(this.serverUrl != null, 'need server to load from');
return this.fromSource(
this.serverUrl +
getScaledAssetPath(this.asset) +
'?platform=' +
Platform.OS +
'&hash=' +
this.asset.hash,
);
}
/**
* Resolves to just the scaled asset filename
* E.g. 'assets/AwesomeModule/icon@2x.png'
*/
scaledAssetPath(): ResolvedAssetSource {
return this.fromSource(getScaledAssetPath(this.asset));
}
/**
* Resolves to where the bundle is running from, with a scaled asset filename
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/icon@2x.png'
*/
scaledAssetURLNearBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl ?? 'file://';
return this.fromSource(
// Assets can have relative paths outside of the project root.
// When bundling them we replace `../` with `_` to make sure they
// don't end up outside of the expected assets directory.
path + getScaledAssetPath(this.asset).replace(/\.\.\//g, '_'),
);
}
/**
* The default location of assets bundled with the app, located by
* resource identifier
* The Android resource system picks the correct scale.
* E.g. 'assets_awesomemodule_icon'
*/
resourceIdentifierWithoutScale(): ResolvedAssetSource {
invariant(
Platform.OS === 'android',
'resource identifiers work on Android',
);
return this.fromSource(getAndroidResourceIdentifier(this.asset));
}
/**
* If the jsbundle is running from a sideload location, this resolves assets
* relative to its location
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
*/
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl ?? 'file://';
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
}
fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width,
height: this.asset.height,
uri: source,
scale: pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale: (scales: Array<number>, deviceScale?: number) => number =
pickScale;
}
module.exports = AssetSourceResolver;