Introduce custom asset resolver to resolveAssetSource(..)

Reviewed By: frantic

Differential Revision: D2989112

fb-gh-sync-id: a678d091aeb6904448c890653f57dd7944ce95c3
shipit-source-id: a678d091aeb6904448c890653f57dd7944ce95c3
This commit is contained in:
Zahan Malkani 2016-03-15 20:19:38 -07:00 committed by Facebook Github Bot 7
parent a97127b7bb
commit 2209131933
3 changed files with 250 additions and 99 deletions

View File

@ -0,0 +1,163 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule AssetSourceResolver
* @flow
*/
export type ResolvedAssetSource = {
__packager_asset: boolean,
width: number,
height: number,
uri: string,
scale: number,
};
import type { PackagerAsset } from 'AssetRegistry';
const PixelRatio = require('PixelRatio');
const Platform = require('Platform');
const assetPathUtils = require('../../local-cli/bundle/assetPathUtils');
const invariant = require('invariant');
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset): string {
var scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
var assetDir = assetPathUtils.getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset): string {
var scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
var drawbleFolder = assetPathUtils.getAndroidDrawableFolderName(asset, scale);
var fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
return drawbleFolder + '/' + fileName + '.' + asset.type;
}
class AssetSourceResolver {
serverUrl: ?string;
// where the bundle is being run from
bundlePath: ?string;
// the asset to resolve
asset: PackagerAsset;
constructor(serverUrl: ?string, bundlePath: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.bundlePath = bundlePath;
this.asset = asset;
}
isLoadedFromServer(): boolean {
return !!this.serverUrl;
}
isLoadedFromFileSystem(): boolean {
return !!this.bundlePath;
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem() ?
this.drawableFolderInBundle() :
this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetPathInBundle();
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(!!this.serverUrl, '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. '/sdcard/bundle/assets/AwesomeModule/icon@2x.png'
*/
scaledAssetPathInBundle(): ResolvedAssetSource {
const path = this.bundlePath || '';
return this.fromSource(path + getScaledAssetPath(this.asset));
}
/**
* 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(assetPathUtils.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.bundlePath || '';
return this.fromSource(
'file://' + path + getAssetPathInDrawableFolder(this.asset)
);
}
fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width,
height: this.asset.height,
uri: source,
scale: AssetSourceResolver.pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale(scales: Array<number>, deviceScale: number): number {
// Packager guarantees that `scales` array is sorted
for (var i = 0; i < scales.length; i++) {
if (scales[i] >= deviceScale) {
return scales[i];
}
}
// If nothing matches, device scale is larger than any available
// scales, so we return the biggest one. Unless the array is empty,
// in which case we default to 1
return scales[scales.length - 1] || 1;
}
}
module.exports = AssetSourceResolver;

View File

@ -10,6 +10,7 @@
jest
.dontMock('AssetRegistry')
.dontMock('AssetSourceResolver')
.dontMock('../resolveAssetSource')
.dontMock('../../../local-cli/bundle/assetPathUtils');
@ -217,6 +218,60 @@ describe('resolveAssetSource', () => {
});
});
describe('source resolver can be customized', () => {
beforeEach(() => {
NativeModules.SourceCode.scriptURL =
'file:///sdcard/Path/To/Simulator/main.bundle';
Platform.OS = 'android';
});
it('uses bundled source, event when js is sideloaded', () => {
resolveAssetSource.setCustomSourceTransformer(
(resolver) => resolver.resourceIdentifierWithoutScale(),
);
expectResolvesAsset({
__packager_asset: true,
fileSystemLocation: '/root/app/module/a',
httpServerLocation: '/assets/AwesomeModule/Subdir',
width: 100,
height: 200,
scales: [1],
hash: '5b6f00f',
name: '!@Logo#1_€',
type: 'png',
}, {
__packager_asset: true,
width: 100,
height: 200,
uri: 'awesomemodule_subdir_logo1_',
scale: 1,
});
});
it('allows any customization', () => {
resolveAssetSource.setCustomSourceTransformer(
(resolver) => resolver.fromSource('TEST')
);
expectResolvesAsset({
__packager_asset: true,
fileSystemLocation: '/root/app/module/a',
httpServerLocation: '/assets/AwesomeModule/Subdir',
width: 100,
height: 200,
scales: [1],
hash: '5b6f00f',
name: '!@Logo#1_€',
type: 'png',
}, {
__packager_asset: true,
width: 100,
height: 200,
uri: 'TEST',
scale: 1,
});
});
});
});
describe('resolveAssetSource.pickScale', () => {

View File

@ -13,23 +13,15 @@
*/
'use strict';
export type ResolvedAssetSource = {
__packager_asset: boolean,
width: number,
height: number,
uri: string,
scale: number,
};
import type { ResolvedAssetSource } from 'AssetSourceResolver';
var AssetRegistry = require('AssetRegistry');
var PixelRatio = require('PixelRatio');
var Platform = require('Platform');
var SourceCode = require('NativeModules').SourceCode;
var assetPathUtils = require('../../local-cli/bundle/assetPathUtils');
const AssetRegistry = require('AssetRegistry');
const AssetSourceResolver = require('AssetSourceResolver');
const { SourceCode } = require('NativeModules');
var _serverURL, _offlinePath;
let _customSourceTransformer, _serverURL, _bundleSourcePath;
function getDevServerURL() {
function getDevServerURL(): ?string {
if (_serverURL === undefined) {
var scriptURL = SourceCode.scriptURL;
var match = scriptURL && scriptURL.match(/^https?:\/\/.*?\//);
@ -41,119 +33,60 @@ function getDevServerURL() {
_serverURL = null;
}
}
return _serverURL;
}
function getOfflinePath() {
if (_offlinePath === undefined) {
function getBundleSourcePath(): ?string {
if (_bundleSourcePath === undefined) {
const scriptURL = SourceCode.scriptURL;
if (!scriptURL) {
// scriptURL is falsy, we have nothing to go on here
_offlinePath = '';
return _offlinePath;
_bundleSourcePath = null;
return _bundleSourcePath;
}
if (scriptURL.startsWith('assets://')) {
// running from within assets, no offline path to use
_offlinePath = '';
return _offlinePath;
_bundleSourcePath = null;
return _bundleSourcePath;
}
if (scriptURL.startsWith('file://')) {
// cut off the protocol
_offlinePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
_bundleSourcePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
} else {
_offlinePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
_bundleSourcePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
}
}
return _offlinePath;
return _bundleSourcePath;
}
function setCustomSourceTransformer(
transformer: (resolver: AssetSourceResolver) => ResolvedAssetSource,
): void {
_customSourceTransformer = transformer;
}
/**
* Returns the path at which the asset can be found in the archive
* `source` is either a number (opaque type returned by require('./foo.png'))
* or an `ImageSource` like { uri: '<http location || file path>' }
*/
function getPathInArchive(asset) {
var offlinePath = getOfflinePath();
if (Platform.OS === 'android') {
if (offlinePath) {
// E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
}
// E.g. 'assets_awesomemodule_icon'
// The Android resource system picks the correct scale.
return assetPathUtils.getAndroidResourceIdentifier(asset);
} else {
// E.g. '/assets/AwesomeModule/icon@2x.png'
return offlinePath + getScaledAssetPath(asset);
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
function getPathOnDevserver(devServerUrl, asset) {
return devServerUrl + getScaledAssetPath(asset) + '?platform=' + Platform.OS +
'&hash=' + asset.hash;
}
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset) {
var scale = pickScale(asset.scales, PixelRatio.get());
var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
var assetDir = assetPathUtils.getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset) {
var scale = pickScale(asset.scales, PixelRatio.get());
var drawbleFolder = assetPathUtils.getAndroidDrawableFolderName(asset, scale);
var fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
return drawbleFolder + '/' + fileName + '.' + asset.type;
}
function pickScale(scales: Array<number>, deviceScale: number): number {
// Packager guarantees that `scales` array is sorted
for (var i = 0; i < scales.length; i++) {
if (scales[i] >= deviceScale) {
return scales[i];
}
}
// If nothing matches, device scale is larger than any available
// scales, so we return the biggest one. Unless the array is empty,
// in which case we default to 1
return scales[scales.length - 1] || 1;
}
function resolveAssetSource(source: any): ?ResolvedAssetSource {
if (typeof source === 'object') {
return source;
}
var asset = AssetRegistry.getAssetByID(source);
if (asset) {
return assetToImageSource(asset);
if (!asset) {
return null;
}
return null;
}
function assetToImageSource(asset): ResolvedAssetSource {
var devServerURL = getDevServerURL();
return {
__packager_asset: true,
width: asset.width,
height: asset.height,
uri: devServerURL ? getPathOnDevserver(devServerURL, asset) : getPathInArchive(asset),
scale: pickScale(asset.scales, PixelRatio.get()),
};
const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
module.exports = resolveAssetSource;
module.exports.pickScale = pickScale;
module.exports.pickScale = AssetSourceResolver.pickScale;
module.exports.setCustomSourceTransformer = setCustomSourceTransformer;