Move the getAssetData() logic out of the Bundler

Reviewed By: davidaurelio

Differential Revision: D6183424

fbshipit-source-id: 47a658de25ac0943f4b82e7eb90c1018e9d0357d
This commit is contained in:
Rafael Oleza 2017-10-31 09:06:35 -07:00 committed by Facebook Github Bot
parent 2850627a66
commit 772d21f72d
9 changed files with 98 additions and 75 deletions

View File

@ -6,17 +6,24 @@
* LICENSE file in the root directory of this source tree. An additional grant * 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. * of patent rights can be found in the PATENTS file in the same directory.
* *
* @emails oncall+javascript_foundation
* @format * @format
*/ */
'use strict'; 'use strict';
jest.mock('fs'); jest.mock('fs');
jest.mock('image-size');
const AssetServer = require('../'); const AssetServer = require('../');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
require('image-size').mockReturnValue({
width: 300,
height: 200,
});
const {objectContaining} = jasmine; const {objectContaining} = jasmine;
describe('AssetServer', () => { describe('AssetServer', () => {
@ -195,12 +202,15 @@ describe('AssetServer', () => {
}, },
}); });
return server.getAssetData('imgs/b.png').then(data => { return server.getAssetData('/root/imgs/b.png').then(data => {
expect(data).toEqual( expect(data).toEqual(
objectContaining({ objectContaining({
__packager_asset: true,
type: 'png', type: 'png',
name: 'b', name: 'b',
scales: [1, 2, 4, 4.5], scales: [1, 2, 4, 4.5],
fileSystemLocation: '/root/imgs',
httpServerLocation: '/assets/imgs',
files: [ files: [
'/root/imgs/b@1x.png', '/root/imgs/b@1x.png',
'/root/imgs/b@2x.png', '/root/imgs/b@2x.png',
@ -229,12 +239,15 @@ describe('AssetServer', () => {
}, },
}); });
return server.getAssetData('imgs/b.jpg').then(data => { return server.getAssetData('/root/imgs/b.jpg').then(data => {
expect(data).toEqual( expect(data).toEqual(
objectContaining({ objectContaining({
__packager_asset: true,
type: 'jpg', type: 'jpg',
name: 'b', name: 'b',
scales: [1, 2, 4, 4.5], scales: [1, 2, 4, 4.5],
fileSystemLocation: '/root/imgs',
httpServerLocation: '/assets/imgs',
files: [ files: [
'/root/imgs/b@1x.jpg', '/root/imgs/b@1x.jpg',
'/root/imgs/b@2x.jpg', '/root/imgs/b@2x.jpg',
@ -275,18 +288,18 @@ describe('AssetServer', () => {
} }
return server return server
.getAssetData('imgs/b.jpg') .getAssetData('/root/imgs/b.jpg')
.then(data => .then(data =>
expect(data).toEqual(objectContaining({hash: hash.digest('hex')})), expect(data).toEqual(objectContaining({hash: hash.digest('hex')})),
); );
}); });
it('changes the hash when the passed-in file watcher emits an `all` event', () => { it('changes the hash when the passed-in file watcher emits an `all` event', () => {
return server.getAssetData('imgs/b.jpg').then(initialData => { return server.getAssetData('/root/imgs/b.jpg').then(initialData => {
mockFS.root.imgs['b@4x.jpg'] = 'updated data'; mockFS.root.imgs['b@4x.jpg'] = 'updated data';
server.onFileChange('all', '/root/imgs/b@4x.jpg'); server.onFileChange('all', '/root/imgs/b@4x.jpg');
return server return server
.getAssetData('imgs/b.jpg') .getAssetData('/root/imgs/b.jpg')
.then(data => expect(data.hash).not.toEqual(initialData.hash)); .then(data => expect(data.hash).not.toEqual(initialData.hash));
}); });
}); });

View File

@ -17,9 +17,34 @@ const AssetPaths = require('../node-haste/lib/AssetPaths');
const crypto = require('crypto'); const crypto = require('crypto');
const denodeify = require('denodeify'); const denodeify = require('denodeify');
const fs = require('fs'); const fs = require('fs');
const imageSize = require('image-size');
const path = require('path'); const path = require('path');
const toLocalPath = require('../node-haste/lib/toLocalPath');
import type {AssetData} from '../node-haste/lib/AssetPaths'; const {isAssetTypeAnImage} = require('../Bundler/util');
import type {AssetPath} from '../node-haste/lib/AssetPaths';
type AssetInfo = {|
files: Array<string>,
hash: string,
name: string,
scales: Array<number>,
type: string,
|};
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 stat = denodeify(fs.stat); const stat = denodeify(fs.stat);
const readDir = denodeify(fs.readdir); const readDir = denodeify(fs.readdir);
@ -57,16 +82,10 @@ class AssetServer {
}); });
} }
getAssetData( _getAssetInfo(
assetPath: string, assetPath: string,
platform: ?string = null, platform: ?string = null,
): Promise<{| ): Promise<AssetInfo> {
files: Array<string>,
hash: string,
name: string,
scales: Array<number>,
type: string,
|}> {
const nameData = AssetPaths.parse( const nameData = AssetPaths.parse(
assetPath, assetPath,
new Set(platform != null ? [platform] : []), new Set(platform != null ? [platform] : []),
@ -97,6 +116,37 @@ class AssetServer {
}); });
} }
async getAssetData(
assetPath: string,
platform: ?string = null,
): Promise<AssetData> {
const localPath = toLocalPath(this._roots, assetPath);
var assetUrlPath = path.join('/assets', path.dirname(localPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (path.sep === '\\') {
assetUrlPath = assetUrlPath.replace(/\\/g, '/');
}
const isImage = isAssetTypeAnImage(path.extname(assetPath).slice(1));
const assetData = await this._getAssetInfo(localPath, platform);
const dimensions = isImage ? imageSize(assetData.files[0]) : null;
const scale = assetData.scales[0];
return {
__packager_asset: true,
fileSystemLocation: path.dirname(assetPath),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetData.scales,
files: assetData.files,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
}
onFileChange(type: string, filePath: string) { onFileChange(type: string, filePath: string) {
this._hashes.delete(this._files.get(filePath)); this._hashes.delete(this._files.get(filePath));
} }
@ -164,6 +214,7 @@ class AssetServer {
// important: we want to resolve root + dir // important: we want to resolve root + dir
// to ensure the requested path doesn't traverse beyond root // to ensure the requested path doesn't traverse beyond root
const absPath = path.resolve(root, dir); const absPath = path.resolve(root, dir);
return stat(absPath).then( return stat(absPath).then(
fstat => { fstat => {
// keep asset requests from traversing files // keep asset requests from traversing files
@ -237,7 +288,7 @@ class AssetServer {
return map; return map;
} }
_getAssetDataFromName(platforms: Set<string>, file: string): ?AssetData { _getAssetDataFromName(platforms: Set<string>, file: string): ?AssetPath {
return AssetPaths.tryParse(file, platforms); return AssetPaths.tryParse(file, platforms);
} }
} }

View File

@ -228,6 +228,8 @@ describe('Bundler', function() {
describe('.bundle', () => { describe('.bundle', () => {
const mockAsset = { const mockAsset = {
__packager_asset: true,
fileSystemLocation: '/root/img',
scales: [1, 2, 3], scales: [1, 2, 3],
files: [ files: [
'/root/img/img.png', '/root/img/img.png',
@ -235,8 +237,11 @@ describe('Bundler', function() {
'/root/img/img@3x.png', '/root/img/img@3x.png',
], ],
hash: 'i am a hash', hash: 'i am a hash',
height: 100,
httpServerLocation: '/assets/img',
name: 'img', name: 'img',
type: 'png', type: 'png',
width: 50,
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -22,21 +22,14 @@ const Resolver = require('../Resolver');
const Bundle = require('./Bundle'); const Bundle = require('./Bundle');
const HMRBundle = require('./HMRBundle'); const HMRBundle = require('./HMRBundle');
const ModuleTransport = require('../lib/ModuleTransport'); const ModuleTransport = require('../lib/ModuleTransport');
const imageSize = require('image-size');
const path = require('path'); const path = require('path');
const denodeify = require('denodeify');
const defaults = require('../defaults'); const defaults = require('../defaults');
const toLocalPath = require('../node-haste/lib/toLocalPath'); const toLocalPath = require('../node-haste/lib/toLocalPath');
const createModuleIdFactory = require('../lib/createModuleIdFactory'); const createModuleIdFactory = require('../lib/createModuleIdFactory');
const {generateAssetTransformResult, isAssetTypeAnImage} = require('./util'); const {generateAssetTransformResult} = require('./util');
const { const {sep: pathSeparator} = require('path');
sep: pathSeparator,
join: joinPath,
dirname: pathDirname,
extname,
} = require('path');
const VERSION = require('../../package.json').version; const VERSION = require('../../package.json').version;
@ -89,8 +82,6 @@ export type ExtendedAssetDescriptor = AssetDescriptor & {
+files: Array<string>, +files: Array<string>,
}; };
const sizeOf = denodeify(imageSize);
const { const {
createActionStartEntry, createActionStartEntry,
createActionEndEntry, createActionEndEntry,
@ -791,41 +782,9 @@ class Bundler {
assetPlugins: Array<string>, assetPlugins: Array<string>,
platform: ?string = null, platform: ?string = null,
) { ) {
const localPath = toLocalPath(this._projectRoots, module.path);
var assetUrlPath = joinPath('/assets', pathDirname(localPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (pathSeparator === '\\') {
assetUrlPath = assetUrlPath.replace(/\\/g, '/');
}
const isImage = isAssetTypeAnImage(extname(module.path).slice(1));
return this._assetServer return this._assetServer
.getAssetData(localPath, platform) .getAssetData(module.path, platform)
.then(assetData => { .then(asset => {
return Promise.all([
isImage ? sizeOf(assetData.files[0]) : null,
assetData,
]);
})
.then(res => {
const dimensions = res[0];
const assetData = res[1];
const scale = assetData.scales[0];
const asset = {
__packager_asset: true,
fileSystemLocation: pathDirname(module.path),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetData.scales,
files: assetData.files,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
return this._applyAssetPlugins(assetPlugins, asset); return this._applyAssetPlugins(assetPlugins, asset);
}) })
.then(asset => { .then(asset => {

View File

@ -438,7 +438,6 @@ class Server {
return await getOrderedDependencyPaths( return await getOrderedDependencyPaths(
this._deltaBundler, this._deltaBundler,
this._assetServer, this._assetServer,
this._projectRoots,
bundleOptions, bundleOptions,
); );
} }

View File

@ -63,11 +63,11 @@ describe('getOrderedDependencyPaths', () => {
await getOrderedDependencyPaths(deltaBundler, assetsServer, ['/tmp'], {}), await getOrderedDependencyPaths(deltaBundler, assetsServer, ['/tmp'], {}),
).toEqual([ ).toEqual([
'/tmp/1.js', '/tmp/1.js',
'2.png@2x', '/tmp/2.png@2x',
'2.png@3x', '/tmp/2.png@3x',
'/tmp/3.js', '/tmp/3.js',
'4.png@2x', '/tmp/4.png@2x',
'4.png@3x', '/tmp/4.png@3x',
'/tmp/5.js', '/tmp/5.js',
]); ]);
}); });

View File

@ -14,8 +14,6 @@
const Serializers = require('../DeltaBundler/Serializers'); const Serializers = require('../DeltaBundler/Serializers');
const toLocalPath = require('../node-haste/lib/toLocalPath');
import type AssetsServer from '../AssetServer'; import type AssetsServer from '../AssetServer';
import type {Options} from '../DeltaBundler/Serializers'; import type {Options} from '../DeltaBundler/Serializers';
import type DeltaBundler from '../DeltaBundler'; import type DeltaBundler from '../DeltaBundler';
@ -23,7 +21,6 @@ import type DeltaBundler from '../DeltaBundler';
async function getOrderedDependencyPaths( async function getOrderedDependencyPaths(
deltaBundler: DeltaBundler, deltaBundler: DeltaBundler,
assetsServer: AssetsServer, assetsServer: AssetsServer,
projectRoots: $ReadOnlyArray<string>,
options: Options, options: Options,
): Promise<Array<string>> { ): Promise<Array<string>> {
const modules = await Serializers.getAllModules(deltaBundler, options); const modules = await Serializers.getAllModules(deltaBundler, options);
@ -34,7 +31,7 @@ async function getOrderedDependencyPaths(
return [module.path]; return [module.path];
} else { } else {
const assetData = await assetsServer.getAssetData( const assetData = await assetsServer.getAssetData(
toLocalPath(projectRoots, module.path), module.path,
options.platform, options.platform,
); );

View File

@ -15,7 +15,7 @@
const AssetPaths = require('./lib/AssetPaths'); const AssetPaths = require('./lib/AssetPaths');
const MapWithDefaults = require('./lib/MapWithDefaults'); const MapWithDefaults = require('./lib/MapWithDefaults');
import type {AssetData} from './lib/AssetPaths'; import type {AssetPath} from './lib/AssetPaths';
type Options = {| type Options = {|
/** /**
@ -116,7 +116,7 @@ class AssetResolutionCache {
return results; return results;
}; };
_isValidAsset(assetData: AssetData): boolean { _isValidAsset(assetData: AssetPath): boolean {
return this._opts.assetExtensions.has(assetData.type); return this._opts.assetExtensions.has(assetData.type);
} }
} }

View File

@ -15,8 +15,7 @@
const parsePlatformFilePath = require('./parsePlatformFilePath'); const parsePlatformFilePath = require('./parsePlatformFilePath');
const path = require('path'); const path = require('path');
export type AssetData = {| export type AssetPath = {|
// TODO: rename to "assetPath", what it actually is.
assetName: string, assetName: string,
name: string, name: string,
platform: ?string, platform: ?string,
@ -47,7 +46,7 @@ function parseBaseName(
* Return `null` if the `filePath` doesn't have a valid extension, required * Return `null` if the `filePath` doesn't have a valid extension, required
* to describe the type of an asset. * to describe the type of an asset.
*/ */
function tryParse(filePath: string, platforms: Set<string>): ?AssetData { function tryParse(filePath: string, platforms: Set<string>): ?AssetPath {
const result = parsePlatformFilePath(filePath, platforms); const result = parsePlatformFilePath(filePath, platforms);
const {dirPath, baseName, platform, extension} = result; const {dirPath, baseName, platform, extension} = result;
if (extension == null) { if (extension == null) {
@ -63,7 +62,7 @@ function tryParse(filePath: string, platforms: Set<string>): ?AssetData {
}; };
} }
function parse(filePath: string, platforms: Set<string>): AssetData { function parse(filePath: string, platforms: Set<string>): AssetPath {
const result = tryParse(filePath, platforms); const result = tryParse(filePath, platforms);
if (result == null) { if (result == null) {
throw new Error('invalid asset file path: `${filePath}'); throw new Error('invalid asset file path: `${filePath}');