/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow * @format */ 'use strict'; const babelTypes = require('@babel/types'); const babylon = require('@babel/parser'); const nullthrows = require('nullthrows'); const template = require('@babel/template').default; import type {AssetDataFiltered, AssetDataWithoutFiles} from '../Assets'; import type {ModuleTransportLike} from '../shared/types.flow'; import type {File} from '@babel/types'; // Structure of the object: dir.name.scale = asset export type RemoteFileMap = { [string]: { [string]: { [number]: { handle: string, hash: string, ... }, ..., }, ..., }, __proto__: null, ... }; // Structure of the object: platform.dir.name.scale = asset export type PlatformRemoteFileMap = { [string]: RemoteFileMap, __proto__: null, ... }; type SubTree = ( moduleTransport: T, moduleTransportsByPath: Map, ) => Iterable; const assetPropertyBlockList = new Set(['files', 'fileSystemLocation', 'path']); function generateAssetCodeFileAst( assetRegistryPath: string, assetDescriptor: AssetDataWithoutFiles, ): File { const properDescriptor = filterObject( assetDescriptor, assetPropertyBlockList, ); // {...} const descriptorAst = babylon.parseExpression( JSON.stringify(properDescriptor), ); const t = babelTypes; // require('AssetRegistry').registerAsset({...}) const buildRequire = template.statement(` module.exports = require(ASSET_REGISTRY_PATH).registerAsset(DESCRIPTOR_AST) `); return t.file( t.program([ buildRequire({ ASSET_REGISTRY_PATH: t.stringLiteral(assetRegistryPath), DESCRIPTOR_AST: descriptorAst, }), ]), ); } /** * Generates the code involved in requiring an asset, but to be loaded remotely. * If the asset cannot be found within the map, then it falls back to the * standard asset. */ function generateRemoteAssetCodeFileAst( assetSourceResolverPath: string, assetDescriptor: AssetDataWithoutFiles, remoteServer: string, remoteFileMap: RemoteFileMap, ): ?File { const t = babelTypes; const file = remoteFileMap[assetDescriptor.fileSystemLocation]; const descriptor = file && file[assetDescriptor.name]; const data = {}; if (!descriptor) { return null; } for (const scale in descriptor) { data[+scale] = descriptor[+scale].handle; } // {2: 'path/to/image@2x', 3: 'path/to/image@3x', ...} const astData = babylon.parseExpression(JSON.stringify(data)); // URI to remote server const URI = t.stringLiteral(remoteServer); // Size numbers. const WIDTH = t.numericLiteral(nullthrows(assetDescriptor.width)); const HEIGHT = t.numericLiteral(nullthrows(assetDescriptor.height)); const buildRequire = template.statement(` module.exports = { "width": WIDTH, "height": HEIGHT, "uri": URI + OBJECT_AST[require(ASSET_SOURCE_RESOLVER_PATH).pickScale(SCALE_ARRAY)] }; `); return t.file( t.program([ buildRequire({ WIDTH, HEIGHT, URI, OBJECT_AST: astData, ASSET_SOURCE_RESOLVER_PATH: t.stringLiteral(assetSourceResolverPath), SCALE_ARRAY: t.arrayExpression( Object.keys(descriptor) .map(Number) .sort((a: number, b: number) => a - b) .map((scale: number) => t.numericLiteral(scale)), ), }), ]), ); } // Test extension against all types supported by image-size module. // If it's not one of these, we won't treat it as an image. function isAssetTypeAnImage(type: string): boolean { return ( ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'].indexOf( type, ) !== -1 ); } function filterObject( object: AssetDataWithoutFiles, blockList: Set, ): AssetDataFiltered { const copied = Object.assign({}, object); for (const key of blockList) { delete copied[key]; } return copied; } function createRamBundleGroups( ramGroups: $ReadOnlyArray, groupableModules: $ReadOnlyArray, subtree: SubTree, ): Map> { // build two maps that allow to lookup module data // by path or (numeric) module id; const byPath: Map = new Map(); const byId: Map = new Map(); groupableModules.forEach((m: T) => { byPath.set(m.sourcePath, m); byId.set(m.id, m.sourcePath); }); // build a map of group root IDs to an array of module IDs in the group const result: Map> = new Map( ramGroups.map((modulePath: string) => { const root = byPath.get(modulePath); if (root == null) { throw Error(`Group root ${modulePath} is not part of the bundle`); } return [ root.id, // `subtree` yields the IDs of all transitive dependencies of a module new Set(subtree(root, byPath)), ]; }), ); if (ramGroups.length > 1) { // build a map of all grouped module IDs to an array of group root IDs const all = new ArrayMap(); for (const [parent, children] of result) { for (const module of children) { all.get(module).push(parent); } } // find all module IDs that are part of more than one group const doubles = filter(all, ([, parents]) => parents.length > 1); for (const [moduleId, parents] of doubles) { const parentNames = parents.map(byId.get, byId); const lastName = parentNames.pop(); throw new Error( `Module ${byId.get(moduleId) || moduleId} belongs to groups ${parentNames.join(', ')}, and ${String( lastName, )}. Ensure that each module is only part of one group.`, ); } } return result; } function* filter( iterator: ArrayMap, predicate: ([A, Array]) => boolean, ): Generator<[A, Array], void, void> { for (const value of iterator) { if (predicate(value)) { yield value; } } } class ArrayMap extends Map> { get(key: K): Array { let array = super.get(key); if (!array) { array = []; this.set(key, array); } return array; } } module.exports = { createRamBundleGroups, generateAssetCodeFileAst, generateRemoteAssetCodeFileAst, isAssetTypeAnImage, };