react-native-asset
Version:
Linking and unlinking of assets in your react-native app, works for fonts and sounds
228 lines (227 loc) • 9.85 kB
JavaScript
import * as dntShim from "./_dnt.shims.js";
import * as path from "./deps/jsr.io/@std/path/1.1.4/mod.js";
import { getConfig } from "./get-config.js";
import copyAssetsIos from "./copy-assets/ios.js";
import cleanAssetsIos from "./clean-assets/ios.js";
import copyAssetsAndroid from "./copy-assets/android.js";
import cleanAssetsAndroid from "./clean-assets/android.js";
import getManifest from "./manifest/index.js";
export const linkAssets = async ({ rootPath, platforms, shouldUnlink = true, }) => {
const cwd = dntShim.Deno.cwd();
const clearDuplicated = (files) => Array.from(new Map(files.map((f) => [path.parse(f.path).base + "|" + f.sha1, f]))
.values());
const filesToIgnore = [
".DS_Store",
"Thumbs.db",
];
const filterFilesToIgnore = ({ path: asset }) => filesToIgnore.indexOf(path.basename(asset)) === -1;
const getAbsolute = ({ filePath, dirPath }) => (path.isAbsolute(filePath) ? filePath : path.resolve(dirPath, filePath));
const getRelative = ({ filePath, dirPath }) => (path.isAbsolute(filePath) ? path.relative(dirPath, filePath) : filePath);
const filterFileByFilesWhichNotExists = (files, { normalizeAbsolutePathsTo }) => (file) => {
const { path: filePath, sha1: fileSha1 } = file;
const relativeFilePath = getRelative({
filePath,
dirPath: normalizeAbsolutePathsTo,
});
return files
.map((otherFile) => ({
...otherFile,
path: getRelative({
filePath: otherFile.path,
dirPath: normalizeAbsolutePathsTo,
}),
}))
.findIndex((otherFile) => {
const { path: otherFileRelativePath, sha1: otherFileSha1 } = otherFile;
return (relativeFilePath === otherFileRelativePath &&
fileSha1 === otherFileSha1);
}) === -1;
};
const computeSha1 = async (filePath) => {
const data = await dntShim.Deno.readFile(filePath);
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
};
const absoluteRootPath = path.resolve(cwd, rootPath);
console.log(`Linking assets in ${absoluteRootPath}`);
// basic validation
const st = await dntShim.Deno.lstat(rootPath);
if (!st.isDirectory) {
throw new Error(`'rootPath' must be a valid path, got ${rootPath}`);
}
const st2 = await dntShim.Deno.lstat(absoluteRootPath);
if (!st2.isDirectory) {
throw new Error(`'rootPath' must be a valid path, got ${absoluteRootPath}`);
}
if (typeof shouldUnlink !== "boolean") {
throw new Error(`'shouldUnlink' must be a boolean, got ${typeof shouldUnlink}`);
}
if ([platforms.ios, platforms.android].find(({ assets }) => !Array.isArray(assets))) {
throw new Error("'platforms[\"platform\"].assets' must be an array");
}
const finalRootPath = path.isAbsolute(rootPath)
? rootPath
: path.resolve(cwd, rootPath);
// build platforms defaults
const mergedPlatforms = {
ios: {
enabled: platforms.ios.enabled,
assets: platforms.ios.assets,
},
android: {
enabled: platforms.android.enabled,
assets: platforms.android.assets,
},
};
// helper modules are statically imported at the top of this file
const config = await getConfig({ rootPath: finalRootPath });
const { android: { path: androidPath }, ios: { path: iosPath }, } = config;
const linkOptionsPerExt = {
...["otf", "ttf"].reduce((result, fontType) => ({
...result,
[fontType]: {
android: {
path: path.resolve(androidPath, "app", "src", "main", "assets", "fonts"),
},
ios: {
addFont: true,
},
},
}), {}),
...["png", "jpg", "gif"].reduce((result, imageType) => ({
...result,
[imageType]: {
android: {
path: path.resolve(androidPath, "app", "src", "main", "res", "drawable"),
},
ios: {
addFont: false,
},
},
}), {}),
mp3: {
android: {
path: path.resolve(androidPath, "app", "src", "main", "res", "raw"),
},
ios: {
addFont: false,
},
},
};
const otherLinkOptions = {
android: {
path: path.resolve(androidPath, "app", "src", "main", "assets", "custom"),
},
ios: {
addFont: false,
},
};
const linkPlatform = ({ rootPath: rp, shouldUnlink: su }) => async ({ name, manifest, config: platformConfig, linkOptionsPerExt: lopExt, otherLinkOptions: otherOptions, cleanAssets, copyAssets, assets: assetsPaths, }) => {
const prevRelativeAssets = await manifest.read();
let assets = [];
const loadAsset = async (assetMightNotAbsolute) => {
const asset = getAbsolute({
filePath: assetMightNotAbsolute,
dirPath: rp,
});
const stats = await dntShim.Deno.lstat(asset);
if (stats.isDirectory) {
for await (const dirent of dntShim.Deno.readDir(asset)) {
await loadAsset(path.resolve(asset, dirent.name));
}
}
else {
const sha1 = await computeSha1(asset);
assets = assets.concat({ path: asset, sha1 });
}
};
const loadAll = async () => {
await Promise.all(assetsPaths.map((p) => loadAsset(p)));
assets = clearDuplicated(assets);
};
// run loading synchronously for simplicity
// (keeps behavior similar to original)
await loadAll();
const fileFilters = []
.concat(Object.keys(lopExt).map((fileExt) => ({
name: fileExt,
filter: ({ path: filePath }) => path.extname(filePath) === `.${fileExt}`,
options: lopExt[fileExt],
})))
.concat({
name: "custom",
filter: ({ path: filePath }) => Object.keys(lopExt).indexOf(path.extname(filePath).substr(1)) ===
-1,
options: otherOptions,
});
for (const { name: fileConfigName, filter: fileConfigFilter, options } of fileFilters) {
const prevRelativeAssetsWithExt = prevRelativeAssets
.filter(fileConfigFilter)
.filter(filterFileByFilesWhichNotExists(assets, {
normalizeAbsolutePathsTo: rp,
}));
const assetsWithExt = assets
.filter(fileConfigFilter)
.filter(filterFileByFilesWhichNotExists(prevRelativeAssets, {
normalizeAbsolutePathsTo: rp,
}))
.filter(filterFilesToIgnore);
if (su && prevRelativeAssetsWithExt.length > 0) {
console.info(`Cleaning previously linked ${fileConfigName} assets from ${name} project, prevRelativeAssetsWithExt: ${prevRelativeAssetsWithExt.map((x) => x.path)}`);
// deno-lint-ignore no-await-in-loop -- sequential read/write to same plist file
await cleanAssets(prevRelativeAssetsWithExt.map(({ path: filePath }) => getAbsolute({ filePath, dirPath: rp })), platformConfig, options);
}
if (assetsWithExt.length > 0) {
console.info(`Linking ${fileConfigName} assets to ${name} project`);
// deno-lint-ignore no-await-in-loop -- sequential read/write to same plist file
await copyAssets(assetsWithExt.map(({ path: assetPath }) => assetPath), platformConfig, options);
}
}
await manifest.write(assets
.filter(filterFilesToIgnore)
.map((asset) => ({
...asset,
path: path.relative(rp, asset.path).split(path.SEPARATOR).join("/"),
}))
.sort((a, b) => a.path.localeCompare(b.path, "en")));
};
const platformsArray = [
{
name: "iOS",
enabled: mergedPlatforms.ios.enabled,
assets: mergedPlatforms.ios.assets,
manifest: getManifest(iosPath),
config: config.ios,
cleanAssets: cleanAssetsIos,
copyAssets: copyAssetsIos,
linkOptionsPerExt: {
otf: linkOptionsPerExt.otf.ios,
ttf: linkOptionsPerExt.ttf.ios,
mp3: linkOptionsPerExt.mp3.ios,
},
otherLinkOptions: otherLinkOptions.ios,
},
{
name: "Android",
enabled: mergedPlatforms.android.enabled,
assets: mergedPlatforms.android.assets,
manifest: getManifest(androidPath),
config: config.android,
cleanAssets: cleanAssetsAndroid,
copyAssets: copyAssetsAndroid,
linkOptionsPerExt: {
otf: linkOptionsPerExt.otf.android,
ttf: linkOptionsPerExt.ttf.android,
png: linkOptionsPerExt.png.android,
jpg: linkOptionsPerExt.jpg.android,
gif: linkOptionsPerExt.gif.android,
mp3: linkOptionsPerExt.mp3.android,
},
otherLinkOptions: otherLinkOptions.android,
},
];
await Promise.all(platformsArray
.filter(({ enabled, config: platformConfig }) => enabled && platformConfig.exists)
.map((p) => linkPlatform({ rootPath: finalRootPath, shouldUnlink })(p)));
};