create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
249 lines (218 loc) • 6.52 kB
Flow
/**
* 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
*/
;
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<T: ModuleTransportLike> = (
moduleTransport: T,
moduleTransportsByPath: Map<string, T>,
) => Iterable<number>;
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<string>,
): AssetDataFiltered {
const copied = Object.assign({}, object);
for (const key of blockList) {
delete copied[key];
}
return copied;
}
function createRamBundleGroups<T: ModuleTransportLike>(
ramGroups: $ReadOnlyArray<string>,
groupableModules: $ReadOnlyArray<T>,
subtree: SubTree<T>,
): Map<number, Set<number>> {
// build two maps that allow to lookup module data
// by path or (numeric) module id;
const byPath: Map<string, T> = new Map();
const byId: Map<number, string> = 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<number, Set<number>> = 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<A: number, B: number>(
iterator: ArrayMap<A, B>,
predicate: ([A, Array<B>]) => boolean,
): Generator<[A, Array<B>], void, void> {
for (const value of iterator) {
if (predicate(value)) {
yield value;
}
}
}
class ArrayMap<K, V> extends Map<K, Array<V>> {
get(key: K): Array<V> {
let array = super.get(key);
if (!array) {
array = [];
this.set(key, array);
}
return array;
}
}
module.exports = {
createRamBundleGroups,
generateAssetCodeFileAst,
generateRemoteAssetCodeFileAst,
isAssetTypeAnImage,
};