Get rid of AssetServer

Reviewed By: mjesun

Differential Revision: D6488612

fbshipit-source-id: e6144f0e143c0008d47342077b4a2c6c15765edc
This commit is contained in:
Rafael Oleza 2017-12-06 07:21:57 -08:00 committed by Facebook Github Bot
parent 5bff35f09e
commit 758ce871e6
12 changed files with 240 additions and 336 deletions

View File

@ -1,177 +0,0 @@
/**
* Copyright (c) 2013-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.
*
* @emails oncall+javascript_foundation
* @format
*/
'use strict';
jest.mock('fs');
jest.mock('image-size');
const AssetServer = require('../');
const fs = require('fs');
require('image-size').mockReturnValue({
width: 300,
height: 200,
});
describe('AssetServer', () => {
describe('assetServer.get', () => {
it('should work for the simple case', () => {
const server = new AssetServer({
projectRoots: ['/root'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'b image',
'b@2x.png': 'b2 image',
},
},
});
return Promise.all([
server.get('imgs/b.png'),
server.get('imgs/b@1x.png'),
]).then(resp => resp.forEach(data => expect(data).toBe('b image')));
});
it('should work for the simple case with platform ext', () => {
const server = new AssetServer({
projectRoots: ['/root'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b.ios.png': 'b ios image',
'b.android.png': 'b android image',
'c.png': 'c general image',
'c.android.png': 'c android image',
},
},
});
return Promise.all([
server
.get('imgs/b.png', 'ios')
.then(data => expect(data).toBe('b ios image')),
server
.get('imgs/b.png', 'android')
.then(data => expect(data).toBe('b android image')),
server
.get('imgs/c.png', 'android')
.then(data => expect(data).toBe('c android image')),
server
.get('imgs/c.png', 'ios')
.then(data => expect(data).toBe('c general image')),
server
.get('imgs/c.png')
.then(data => expect(data).toBe('c general image')),
]);
});
it('should work for the simple case with jpg', () => {
const server = new AssetServer({
projectRoots: ['/root'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'png image',
'b.jpg': 'jpeg image',
},
},
});
return Promise.all([
server.get('imgs/b.jpg'),
server.get('imgs/b.png'),
]).then(data => expect(data).toEqual(['jpeg image', 'png image']));
});
it('should pick the bigger one', () => {
const server = new AssetServer({
projectRoots: ['/root'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
},
},
});
return server
.get('imgs/b@3x.png')
.then(data => expect(data).toBe('b4 image'));
});
it('should pick the bigger one with platform ext', () => {
const server = new AssetServer({
projectRoots: ['/root'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
'b@1x.ios.png': 'b1 ios image',
'b@2x.ios.png': 'b2 ios image',
'b@4x.ios.png': 'b4 ios image',
'b@4.5x.ios.png': 'b4.5 ios image',
},
},
});
return Promise.all([
server.get('imgs/b@3x.png').then(data => expect(data).toBe('b4 image')),
server
.get('imgs/b@3x.png', 'ios')
.then(data => expect(data).toBe('b4 ios image')),
]);
});
it('should support multiple project roots', () => {
const server = new AssetServer({
projectRoots: ['/root', '/root2'],
});
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'b image',
},
},
root2: {
newImages: {
imgs: {
'b@1x.png': 'b1 image',
},
},
},
});
return server
.get('newImages/imgs/b.png')
.then(data => expect(data).toBe('b1 image'));
});
});
});

View File

@ -1,88 +0,0 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
const AssetPaths = require('../node-haste/lib/AssetPaths');
const denodeify = require('denodeify');
const fs = require('fs');
const path = require('path');
const {findRoot, getAbsoluteAssetRecord} = require('./util');
export type AssetData = {|
__packager_asset: boolean,
fileSystemLocation: string,
httpServerLocation: string,
width: ?number,
height: ?number,
scales: Array<number>,
files: Array<string>,
hash: string,
name: string,
type: string,
|};
const readFile = denodeify(fs.readFile);
class AssetServer {
_roots: $ReadOnlyArray<string>;
constructor(options: {|+projectRoots: $ReadOnlyArray<string>|}) {
this._roots = options.projectRoots;
}
get(assetPath: string, platform: ?string = null): Promise<Buffer> {
const assetData = AssetPaths.parse(
assetPath,
new Set(platform != null ? [platform] : []),
);
return this._getAssetRecord(assetPath, platform).then(record => {
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
return readFile(record.files[record.files.length - 1]);
});
}
/**
* Given a request for an image by path. That could contain a resolution
* postfix, we need to find that image (or the closest one to it's resolution)
* in one of the project roots:
*
* 1. We first parse the directory of the asset
* 2. We check to find a matching directory in one of the project roots
* 3. We then build a map of all assets and their scales in this directory
* 4. Then try to pick platform-specific asset records
* 5. Then pick the closest resolution (rounding up) to the requested one
*/
async _getAssetRecord(
assetPath: string,
platform: ?string = null,
): Promise<{|
files: Array<string>,
scales: Array<number>,
|}> {
const dir = await findRoot(this._roots, path.dirname(assetPath), assetPath);
return await getAbsoluteAssetRecord(
path.join(dir, path.basename(assetPath)),
platform,
);
}
}
module.exports = AssetServer;

View File

@ -15,7 +15,7 @@
jest.mock('fs');
jest.mock('image-size');
const {getAssetData} = require('../util');
const {getAssetData, getAsset} = require('../');
const crypto = require('crypto');
const fs = require('fs');
@ -24,6 +24,129 @@ require('image-size').mockReturnValue({
height: 200,
});
describe('getAsset', () => {
it('should work for the simple case', () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'b image',
'b@2x.png': 'b2 image',
},
},
});
return Promise.all([
getAsset('imgs/b.png', ['/root']),
getAsset('imgs/b@1x.png', ['/root']),
]).then(resp => resp.forEach(data => expect(data).toBe('b image')));
});
it('should work for the simple case with platform ext', async () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b.ios.png': 'b ios image',
'b.android.png': 'b android image',
'c.png': 'c general image',
'c.android.png': 'c android image',
},
},
});
expect(
await Promise.all([
getAsset('imgs/b.png', ['/root'], 'ios'),
getAsset('imgs/b.png', ['/root'], 'android'),
getAsset('imgs/c.png', ['/root'], 'android'),
getAsset('imgs/c.png', ['/root'], 'ios'),
getAsset('imgs/c.png', ['/root']),
]),
).toEqual([
'b ios image',
'b android image',
'c android image',
'c general image',
'c general image',
]);
});
it('should work for the simple case with jpg', () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'png image',
'b.jpg': 'jpeg image',
},
},
});
return Promise.all([
getAsset('imgs/b.jpg', ['/root']),
getAsset('imgs/b.png', ['/root']),
]).then(data => expect(data).toEqual(['jpeg image', 'png image']));
});
it('should pick the bigger one', async () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
},
},
});
expect(await getAsset('imgs/b@3x.png', ['/root'])).toBe('b4 image');
});
it('should pick the bigger one with platform ext', async () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
'b@1x.ios.png': 'b1 ios image',
'b@2x.ios.png': 'b2 ios image',
'b@4x.ios.png': 'b4 ios image',
'b@4.5x.ios.png': 'b4.5 ios image',
},
},
});
expect(
await Promise.all([
getAsset('imgs/b@3x.png', ['/root']),
getAsset('imgs/b@3x.png', ['/root'], 'ios'),
]),
).toEqual(['b4 image', 'b4 ios image']);
});
it('should support multiple project roots', async () => {
fs.__setMockFilesystem({
root: {
imgs: {
'b.png': 'b image',
},
},
root2: {
newImages: {
imgs: {
'b@1x.png': 'b1 image',
},
},
},
});
expect(await getAsset('newImages/imgs/b.png', ['/root', '/root2'])).toBe(
'b1 image',
);
});
});
describe('getAssetData', () => {
it('should get assetData', () => {
fs.__setMockFilesystem({

View File

@ -24,9 +24,22 @@ const {isAssetTypeAnImage} = require('../Bundler/util');
const stat = denodeify(fs.stat);
const readDir = denodeify(fs.readdir);
const readFile = denodeify(fs.readFile);
import type {AssetPath} from '../node-haste/lib/AssetPaths';
import type {AssetData} from './';
export type AssetData = {|
__packager_asset: boolean,
fileSystemLocation: string,
httpServerLocation: string,
width: ?number,
height: ?number,
scales: Array<number>,
files: Array<string>,
hash: string,
name: string,
type: string,
|};
export type AssetInfo = {|
files: Array<string>,
@ -61,7 +74,7 @@ function buildAssetMap(
|},
> {
const platforms = new Set(platform != null ? [platform] : []);
const assets = files.map(getAssetDataFromName.bind(this, platforms));
const assets = files.map(file => AssetPaths.tryParse(file, platforms));
const map = new Map();
assets.forEach(function(asset, i) {
if (asset == null) {
@ -93,13 +106,6 @@ function buildAssetMap(
return map;
}
function getAssetDataFromName(
platforms: Set<string>,
file: string,
): ?AssetPath {
return AssetPaths.tryParse(file, platforms);
}
function getAssetKey(assetName, platform) {
if (platform != null) {
return `${assetName} : ${platform}`;
@ -184,6 +190,26 @@ async function findRoot(
);
}
async function getAssetRecord(
relativePath: string,
projectRoots: $ReadOnlyArray<string>,
platform: ?string = null,
): Promise<{|
files: Array<string>,
scales: Array<number>,
|}> {
const dir = await findRoot(
projectRoots,
path.dirname(relativePath),
relativePath,
);
return await getAbsoluteAssetRecord(
path.join(dir, path.basename(relativePath)),
platform,
);
}
async function getAbsoluteAssetInfo(
assetPath: string,
platform: ?string = null,
@ -235,6 +261,9 @@ async function getAssetData(
};
}
/**
* Returns all the associated files (for different resolutions) of an asset.
**/
async function getAssetFiles(
assetPath: string,
platform: ?string = null,
@ -244,9 +273,41 @@ async function getAssetFiles(
return assetData.files;
}
/**
* Return a buffer with the actual image given a request for an image by path.
* The relativePath can contain a resolution postfix, in this case we need to
* find that image (or the closest one to it's resolution) in one of the
* project roots:
*
* 1. We first parse the directory of the asset
* 2. We check to find a matching directory in one of the project roots
* 3. We then build a map of all assets and their scales in this directory
* 4. Then try to pick platform-specific asset records
* 5. Then pick the closest resolution (rounding up) to the requested one
*/
async function getAsset(
relativePath: string,
projectRoots: $ReadOnlyArray<string>,
platform: ?string = null,
): Promise<Buffer> {
const assetData = AssetPaths.parse(
relativePath,
new Set(platform != null ? [platform] : []),
);
const record = await getAssetRecord(relativePath, projectRoots, platform);
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
return readFile(record.files[record.files.length - 1]);
}
module.exports = {
findRoot,
getAbsoluteAssetRecord,
getAsset,
getAssetData,
getAssetFiles,
};

View File

@ -26,7 +26,6 @@ const {sep: pathSeparator} = require('path');
const VERSION = require('../../package.json').version;
import type AssetServer from '../AssetServer';
import type {HasteImpl} from '../node-haste/Module';
import type {MappingsMap, SourceMap} from '../lib/SourceMap';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
@ -92,7 +91,6 @@ export type PostProcessBundleSourcemap = ({
export type Options = {|
+assetExts: Array<string>,
+assetRegistryPath: string,
+assetServer: AssetServer,
+blacklistRE?: RegExp,
+cacheVersion: string,
+enableBabelRCLookup: boolean,
@ -124,7 +122,6 @@ class Bundler {
_transformer: Transformer;
_resolverPromise: Promise<Resolver>;
_projectRoots: $ReadOnlyArray<string>;
_assetServer: AssetServer;
_getTransformOptions: void | GetTransformOptions;
constructor(opts: Options) {
@ -221,8 +218,6 @@ class Bundler {
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
this._getTransformOptions = opts.getTransformOptions;
}

View File

@ -16,11 +16,11 @@ const DeltaPatcher = require('./DeltaPatcher');
const toLocalPath = require('../node-haste/lib/toLocalPath');
const {getAssetData} = require('../AssetServer/util');
const {getAssetData} = require('../Assets');
const {fromRawMappings} = require('../Bundler/source-map');
const {createRamBundleGroups} = require('../Bundler/util');
import type {AssetData} from '../AssetServer';
import type {AssetData} from '../Assets';
import type {MappingsMap} from '../lib/SourceMap';
import type {BundleOptions} from '../shared/types.flow';
import type {ModuleTransportLike} from '../shared/types.flow';

View File

@ -13,9 +13,9 @@
'use strict';
jest.mock('../../node-haste/lib/toLocalPath');
jest.mock('../../AssetServer/util');
jest.mock('../../Assets');
const {getAssetData} = require('../../AssetServer/util');
const {getAssetData} = require('../../Assets');
const toLocalPath = require('../../node-haste/lib/toLocalPath');
const CURRENT_TIME = 1482363367000;

View File

@ -20,7 +20,7 @@ jest
createWorker: jest.fn().mockReturnValue(jest.fn()),
}))
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../Assets')
.mock('../../node-haste/DependencyGraph')
.mock('../../Logger')
.mock('../../lib/GlobalTransformCache')
@ -29,7 +29,7 @@ jest
describe('processRequest', () => {
let Bundler;
let Server;
let AssetServer;
let getAsset;
let symbolicate;
let Serializers;
let DeltaBundler;
@ -40,7 +40,7 @@ describe('processRequest', () => {
jest.resetModules();
Bundler = require('../../Bundler');
Server = require('../');
AssetServer = require('../../AssetServer');
getAsset = require('../../Assets').getAsset;
symbolicate = require('../symbolicate');
Serializers = require('../../DeltaBundler/Serializers');
DeltaBundler = require('../../DeltaBundler');
@ -354,9 +354,7 @@ describe('processRequest', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
getAsset.mockReturnValue(Promise.resolve('i am image'));
server.processRequest(req, res);
res.end.mockImplementation(value => {
@ -369,13 +367,11 @@ describe('processRequest', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png?platform=ios'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
getAsset.mockReturnValue(Promise.resolve('i am image'));
server.processRequest(req, res);
res.end.mockImplementation(value => {
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(getAsset).toBeCalledWith('imgs/a.png', ['root'], 'ios');
expect(value).toBe('i am image');
done();
});
@ -389,13 +385,11 @@ describe('processRequest', () => {
const res = {end: jest.fn(), writeHead: jest.fn(), setHeader: jest.fn()};
const mockData = 'i am image';
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve(mockData),
);
getAsset.mockReturnValue(Promise.resolve(mockData));
server.processRequest(req, res);
res.end.mockImplementation(value => {
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(getAsset).toBeCalledWith('imgs/a.png', ['root'], 'ios');
expect(value).toBe(mockData.slice(0, 4));
done();
});
@ -407,14 +401,13 @@ describe('processRequest', () => {
});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
getAsset.mockReturnValue(Promise.resolve('i am image'));
server.processRequest(req, res);
res.end.mockImplementation(value => {
expect(AssetServer.prototype.get).toBeCalledWith(
expect(getAsset).toBeCalledWith(
'imgs/\u{4E3B}\u{9875}/logo.png',
['root'],
undefined,
);
expect(value).toBe('i am image');

View File

@ -12,7 +12,6 @@
'use strict';
const AssetServer = require('../AssetServer');
const Bundler = require('../Bundler');
const DeltaBundler = require('../DeltaBundler');
const MultipartResponse = require('./MultipartResponse');
@ -28,6 +27,8 @@ const path = require('path');
const symbolicate = require('./symbolicate');
const url = require('url');
const {getAsset} = require('../Assets');
import type {CustomError} from '../lib/formatBundlingError';
import type {HasteImpl} from '../node-haste/Module';
import type {IncomingMessage, ServerResponse} from 'http';
@ -41,7 +42,7 @@ import type {
} from '../Bundler';
import type {TransformCache} from '../lib/TransformCaching';
import type {SourceMap, Symbolicate} from './symbolicate';
import type {AssetData} from '../AssetServer';
import type {AssetData} from '../Assets';
import type {RamBundleInfo} from '../DeltaBundler/Serializers';
import type {PostProcessModules} from '../DeltaBundler';
const {
@ -100,7 +101,6 @@ class Server {
res: ServerResponse,
}>;
_fileChangeListeners: Array<(filePath: string) => mixed>;
_assetServer: AssetServer;
_bundler: Bundler;
_debouncedFileChangeHandler: (filePath: string) => mixed;
_reporter: Reporter;
@ -162,12 +162,7 @@ class Server {
this._fileChangeListeners = [];
this._platforms = new Set(this._opts.platforms);
this._assetServer = new AssetServer({
projectRoots: this._opts.projectRoots,
});
const bundlerOpts = Object.create(this._opts);
bundlerOpts.assetServer = this._assetServer;
bundlerOpts.globalTransformCache = options.globalTransformCache;
bundlerOpts.watch = this._opts.watch;
bundlerOpts.reporter = reporter;
@ -345,7 +340,7 @@ class Server {
return data;
}
_processSingleAssetRequest(req: IncomingMessage, res: ServerResponse) {
async _processSingleAssetRequest(req: IncomingMessage, res: ServerResponse) {
const urlObj = url.parse(decodeURI(req.url), true);
/* $FlowFixMe: could be empty if the url is invalid */
const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/);
@ -357,25 +352,27 @@ class Server {
}),
);
/* $FlowFixMe: query may be empty for invalid URLs */
this._assetServer.get(assetPath[1], urlObj.query.platform).then(
data => {
// Tell clients to cache this for 1 year.
// This is safe as the asset url contains a hash of the asset.
if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) {
res.setHeader('Cache-Control', 'max-age=31536000');
}
res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => {
log(createActionEndEntry(processingAssetRequestLogEntry));
});
},
error => {
console.error(error.stack);
res.writeHead(404);
res.end('Asset not found');
},
);
try {
const data = await getAsset(
assetPath[1],
this._opts.projectRoots,
/* $FlowFixMe: query may be empty for invalid URLs */
urlObj.query.platform,
);
// Tell clients to cache this for 1 year.
// This is safe as the asset url contains a hash of the asset.
if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) {
res.setHeader('Cache-Control', 'max-age=31536000');
}
res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => {
log(createActionEndEntry(processingAssetRequestLogEntry));
});
} catch (error) {
console.error(error.stack);
res.writeHead(404);
res.end('Asset not found');
}
}
_optionsHash(options: {}) {

View File

@ -11,7 +11,7 @@
*/
'use strict';
const {getAssetData} = require('./AssetServer/util');
const {getAssetData} = require('./Assets');
const {generateAssetCodeFileAst} = require('./Bundler/util');
import type {TransformOptions} from './JSTransformer/worker';

View File

@ -13,12 +13,12 @@
'use strict';
jest.mock('../../DeltaBundler/Serializers');
jest.mock('../../AssetServer/util');
jest.mock('../../Assets');
const getOrderedDependencyPaths = require('../getOrderedDependencyPaths');
const Serializers = require('../../DeltaBundler/Serializers');
const {getAssetFiles} = require('../../AssetServer/util');
const {getAssetFiles} = require('../../Assets');
describe('getOrderedDependencyPaths', () => {
const deltaBundler = {};

View File

@ -14,7 +14,7 @@
const Serializers = require('../DeltaBundler/Serializers');
const {getAssetFiles} = require('../AssetServer/util');
const {getAssetFiles} = require('../Assets');
import type {Options} from '../DeltaBundler/Serializers';
import type DeltaBundler from '../DeltaBundler';