AssetSourceResolver.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /**
  2. * Copyright (c) Facebook, Inc. and its 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
  8. * @format
  9. */
  10. 'use strict';
  11. export type ResolvedAssetSource = {|
  12. +__packager_asset: boolean,
  13. +width: ?number,
  14. +height: ?number,
  15. +uri: string,
  16. +scale: number,
  17. |};
  18. import type {PackagerAsset} from './AssetRegistry';
  19. const PixelRatio = require('../Utilities/PixelRatio');
  20. const Platform = require('../Utilities/Platform');
  21. const assetPathUtils = require('./assetPathUtils');
  22. const invariant = require('invariant');
  23. /**
  24. * Returns a path like 'assets/AwesomeModule/icon@2x.png'
  25. */
  26. function getScaledAssetPath(asset): string {
  27. const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
  28. const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
  29. const assetDir = assetPathUtils.getBasePath(asset);
  30. return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
  31. }
  32. /**
  33. * Returns a path like 'drawable-mdpi/icon.png'
  34. */
  35. function getAssetPathInDrawableFolder(asset): string {
  36. const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
  37. const drawbleFolder = assetPathUtils.getAndroidResourceFolderName(
  38. asset,
  39. scale,
  40. );
  41. const fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
  42. return drawbleFolder + '/' + fileName + '.' + asset.type;
  43. }
  44. class AssetSourceResolver {
  45. serverUrl: ?string;
  46. // where the jsbundle is being run from
  47. jsbundleUrl: ?string;
  48. // the asset to resolve
  49. asset: PackagerAsset;
  50. constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
  51. this.serverUrl = serverUrl;
  52. this.jsbundleUrl = jsbundleUrl;
  53. this.asset = asset;
  54. }
  55. isLoadedFromServer(): boolean {
  56. return !!this.serverUrl;
  57. }
  58. isLoadedFromFileSystem(): boolean {
  59. return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
  60. }
  61. defaultAsset(): ResolvedAssetSource {
  62. if (this.isLoadedFromServer()) {
  63. return this.assetServerURL();
  64. }
  65. if (Platform.OS === 'android') {
  66. return this.isLoadedFromFileSystem()
  67. ? this.drawableFolderInBundle()
  68. : this.resourceIdentifierWithoutScale();
  69. } else {
  70. return this.scaledAssetURLNearBundle();
  71. }
  72. }
  73. /**
  74. * Returns an absolute URL which can be used to fetch the asset
  75. * from the devserver
  76. */
  77. assetServerURL(): ResolvedAssetSource {
  78. invariant(!!this.serverUrl, 'need server to load from');
  79. return this.fromSource(
  80. this.serverUrl +
  81. getScaledAssetPath(this.asset) +
  82. '?platform=' +
  83. Platform.OS +
  84. '&hash=' +
  85. this.asset.hash,
  86. );
  87. }
  88. /**
  89. * Resolves to just the scaled asset filename
  90. * E.g. 'assets/AwesomeModule/icon@2x.png'
  91. */
  92. scaledAssetPath(): ResolvedAssetSource {
  93. return this.fromSource(getScaledAssetPath(this.asset));
  94. }
  95. /**
  96. * Resolves to where the bundle is running from, with a scaled asset filename
  97. * E.g. 'file:///sdcard/bundle/assets/AwesomeModule/icon@2x.png'
  98. */
  99. scaledAssetURLNearBundle(): ResolvedAssetSource {
  100. const path = this.jsbundleUrl || 'file://';
  101. return this.fromSource(
  102. // Assets can have relative paths outside of the project root.
  103. // When bundling them we replace `../` with `_` to make sure they
  104. // don't end up outside of the expected assets directory.
  105. path + getScaledAssetPath(this.asset).replace(/\.\.\//g, '_'),
  106. );
  107. }
  108. /**
  109. * The default location of assets bundled with the app, located by
  110. * resource identifier
  111. * The Android resource system picks the correct scale.
  112. * E.g. 'assets_awesomemodule_icon'
  113. */
  114. resourceIdentifierWithoutScale(): ResolvedAssetSource {
  115. invariant(
  116. Platform.OS === 'android',
  117. 'resource identifiers work on Android',
  118. );
  119. return this.fromSource(
  120. assetPathUtils.getAndroidResourceIdentifier(this.asset),
  121. );
  122. }
  123. /**
  124. * If the jsbundle is running from a sideload location, this resolves assets
  125. * relative to its location
  126. * E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
  127. */
  128. drawableFolderInBundle(): ResolvedAssetSource {
  129. const path = this.jsbundleUrl || 'file://';
  130. return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
  131. }
  132. fromSource(source: string): ResolvedAssetSource {
  133. return {
  134. __packager_asset: true,
  135. width: this.asset.width,
  136. height: this.asset.height,
  137. uri: source,
  138. scale: AssetSourceResolver.pickScale(this.asset.scales, PixelRatio.get()),
  139. };
  140. }
  141. static pickScale(scales: Array<number>, deviceScale: number): number {
  142. // Packager guarantees that `scales` array is sorted
  143. for (let i = 0; i < scales.length; i++) {
  144. if (scales[i] >= deviceScale) {
  145. return scales[i];
  146. }
  147. }
  148. // If nothing matches, device scale is larger than any available
  149. // scales, so we return the biggest one. Unless the array is empty,
  150. // in which case we default to 1
  151. return scales[scales.length - 1] || 1;
  152. }
  153. }
  154. module.exports = AssetSourceResolver;