|
| 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 | +'use strict'; |
| 12 | + |
| 13 | +export type ResolvedAssetSource = {| |
| 14 | + +__packager_asset: boolean, |
| 15 | + +width: ?number, |
| 16 | + +height: ?number, |
| 17 | + +uri: string, |
| 18 | + +scale: number, |
| 19 | +|}; |
| 20 | + |
| 21 | +import type {PackagerAsset} from '@react-native/assets-registry/registry'; |
| 22 | + |
| 23 | +const PixelRatio = require('../Utilities/PixelRatio').default; |
| 24 | +const Platform = require('../Utilities/Platform'); |
| 25 | +const {pickScale} = require('./AssetUtils'); |
| 26 | +const { |
| 27 | + getAndroidResourceFolderName, |
| 28 | + getAndroidResourceIdentifier, |
| 29 | +} = require('@react-native/assets-registry/path-support'); |
| 30 | +const invariant = require('invariant'); |
| 31 | +// $FlowFixMe[untyped-import] |
| 32 | +const ensureShortPath = require('./assetPaths.js'); // [Windows] |
| 33 | + |
| 34 | +// [Windows - instead of using basePath from @react-native/assets-registry/path-support] |
| 35 | +function getBasePath(asset: PackagerAsset, local: boolean) { |
| 36 | + let basePath = asset.httpServerLocation; |
| 37 | + if (basePath[0] === '/') { |
| 38 | + basePath = basePath.substr(1); |
| 39 | + } |
| 40 | + |
| 41 | + if (local) { |
| 42 | + const safePath = basePath.replace(/\.\.\//g, '_'); |
| 43 | + // If this asset was created with saveAssetPlugin, then we should shorten the path |
| 44 | + // This conditional allow compat of bundles which might have been created without the saveAssetPlugin |
| 45 | + // $FlowFixMe: __useShortPath not part of public type |
| 46 | + if (asset.__useShortPath) { |
| 47 | + return ensureShortPath(safePath); |
| 48 | + } |
| 49 | + return safePath; |
| 50 | + } |
| 51 | + |
| 52 | + return basePath; |
| 53 | +} |
| 54 | + |
| 55 | +/** |
| 56 | + * Returns a path like 'assets/AwesomeModule/[email protected]' |
| 57 | + */ |
| 58 | +function getScaledAssetPath(asset: PackagerAsset, local: boolean): string { |
| 59 | + const scale = pickScale(asset.scales, PixelRatio.get()); |
| 60 | + const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x'; |
| 61 | + const assetDir = getBasePath(asset, local); |
| 62 | + return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type; |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Returns a path like 'drawable-mdpi/icon.png' |
| 67 | + */ |
| 68 | +function getAssetPathInDrawableFolder(asset: PackagerAsset): string { |
| 69 | + const scale = pickScale(asset.scales, PixelRatio.get()); |
| 70 | + const drawableFolder = getAndroidResourceFolderName(asset, scale); |
| 71 | + const fileName = getAndroidResourceIdentifier(asset); |
| 72 | + return drawableFolder + '/' + fileName + '.' + asset.type; |
| 73 | +} |
| 74 | + |
| 75 | +class AssetSourceResolver { |
| 76 | + serverUrl: ?string; |
| 77 | + // where the jsbundle is being run from |
| 78 | + jsbundleUrl: ?string; |
| 79 | + // the asset to resolve |
| 80 | + asset: PackagerAsset; |
| 81 | + |
| 82 | + constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) { |
| 83 | + this.serverUrl = serverUrl; |
| 84 | + this.jsbundleUrl = jsbundleUrl; |
| 85 | + this.asset = asset; |
| 86 | + } |
| 87 | + |
| 88 | + isLoadedFromServer(): boolean { |
| 89 | + return !!this.serverUrl; |
| 90 | + } |
| 91 | + |
| 92 | + isLoadedFromFileSystem(): boolean { |
| 93 | + return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://'); |
| 94 | + } |
| 95 | + |
| 96 | + defaultAsset(): ResolvedAssetSource { |
| 97 | + if (this.isLoadedFromServer()) { |
| 98 | + return this.assetServerURL(); |
| 99 | + } |
| 100 | + |
| 101 | + if (Platform.OS === 'android') { |
| 102 | + return this.isLoadedFromFileSystem() |
| 103 | + ? this.drawableFolderInBundle() |
| 104 | + : this.resourceIdentifierWithoutScale(); |
| 105 | + } else { |
| 106 | + return this.scaledAssetURLNearBundle(); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Returns an absolute URL which can be used to fetch the asset |
| 112 | + * from the devserver |
| 113 | + */ |
| 114 | + assetServerURL(): ResolvedAssetSource { |
| 115 | + invariant(this.serverUrl != null, 'need server to load from'); |
| 116 | + return this.fromSource( |
| 117 | + this.serverUrl + |
| 118 | + getScaledAssetPath(this.asset, false) + |
| 119 | + '?platform=' + |
| 120 | + Platform.OS + |
| 121 | + '&hash=' + |
| 122 | + this.asset.hash, |
| 123 | + ); |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Resolves to just the scaled asset filename |
| 128 | + * E.g. 'assets/AwesomeModule/[email protected]' |
| 129 | + */ |
| 130 | + scaledAssetPath(local: boolean): ResolvedAssetSource { |
| 131 | + return this.fromSource(getScaledAssetPath(this.asset, local)); |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Resolves to where the bundle is running from, with a scaled asset filename |
| 136 | + * E.g. 'file:///sdcard/bundle/assets/AwesomeModule/[email protected]' |
| 137 | + */ |
| 138 | + scaledAssetURLNearBundle(): ResolvedAssetSource { |
| 139 | + const path = this.jsbundleUrl ?? 'file://'; |
| 140 | + return this.fromSource( |
| 141 | + // Assets can have relative paths outside of the project root. |
| 142 | + // When bundling them we replace `../` with `_` to make sure they |
| 143 | + // don't end up outside of the expected assets directory. |
| 144 | + path + getScaledAssetPath(this.asset, true).replace(/\.\.\//g, '_'), |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * The default location of assets bundled with the app, located by |
| 150 | + * resource identifier |
| 151 | + * The Android resource system picks the correct scale. |
| 152 | + * E.g. 'assets_awesomemodule_icon' |
| 153 | + */ |
| 154 | + resourceIdentifierWithoutScale(): ResolvedAssetSource { |
| 155 | + invariant( |
| 156 | + Platform.OS === 'android', |
| 157 | + 'resource identifiers work on Android', |
| 158 | + ); |
| 159 | + return this.fromSource(getAndroidResourceIdentifier(this.asset)); |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * If the jsbundle is running from a sideload location, this resolves assets |
| 164 | + * relative to its location |
| 165 | + * E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png' |
| 166 | + */ |
| 167 | + drawableFolderInBundle(): ResolvedAssetSource { |
| 168 | + const path = this.jsbundleUrl ?? 'file://'; |
| 169 | + return this.fromSource(path + getAssetPathInDrawableFolder(this.asset)); |
| 170 | + } |
| 171 | + |
| 172 | + fromSource(source: string): ResolvedAssetSource { |
| 173 | + return { |
| 174 | + __packager_asset: true, |
| 175 | + width: this.asset.width, |
| 176 | + height: this.asset.height, |
| 177 | + uri: source, |
| 178 | + scale: pickScale(this.asset.scales, PixelRatio.get()), |
| 179 | + }; |
| 180 | + } |
| 181 | + |
| 182 | + static pickScale: (scales: Array<number>, deviceScale?: number) => number = |
| 183 | + pickScale; |
| 184 | +} |
| 185 | + |
| 186 | +module.exports = AssetSourceResolver; |
0 commit comments