* Copyright (c) 2024 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree.
*/
import fs from 'fs';
import pathUtils from 'path';
import type { AssetData } from 'metro';
import { Logger } from './io';
type Path = string;
type CopiedFiles = Record<Path, Path>;
const ENABLE_DEBUG_LOG = process.env.RNOH_BUNDLE_DEBUG === 'true';
function debugLog(message: string) {
if (ENABLE_DEBUG_LOG) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
console.log(`[${timestamp}] ${message}`);
}
}
const ALLOWED_SCALES: number[] = [1, 2, 3, 4];
function filterAssetScales(
scales: readonly number[],
): readonly number[] {
const result = scales.filter(scale => ALLOWED_SCALES.includes(scale));
if (result.length === 0 && scales.length > 0) {
const maxScale = ALLOWED_SCALES[ALLOWED_SCALES.length - 1];
for (const scale of scales) {
if (scale > maxScale) {
result.push(scale);
break;
}
}
if (result.length === 0) {
result.push(scales[scales.length - 1]);
}
}
return result;
}
export async function copyAssets(
logger: Logger,
assetsData: readonly AssetData[],
assetsDest: Path
): Promise<void> {
if (assetsDest == null) {
logger.warn((s) => 'Assets destination folder is not set, skipping...');
return;
}
const filesToCopy: CopiedFiles = {};
const addAssetToCopy = (asset: AssetData) => {
const validScales = filterAssetScales(asset.scales);
asset.scales.forEach((scale, idx) => {
if (!validScales.includes(scale)) {
return;
}
const src = asset.files[idx];
const dest = pathUtils.join(assetsDest, getAssetDestRelativePath(asset, scale));
filesToCopy[src] = dest;
});
};
assetsData.forEach(addAssetToCopy);
return copyFiles(logger, filesToCopy);
}
function getAssetDestRelativePath(asset: AssetData, scale: number = 1): string {
const suffix = scale === 1 ? '' : `@${scale}x`;
const fileName = getResourceIdentifier(asset);
return `${fileName + suffix}.${asset.type}`.replace(/\.\.\//g, '_');
}
function getResourceIdentifier(asset: AssetData): string {
const folderPath = getBasePath(asset);
return `${folderPath}/${asset.name}`.replace(/^assets\//, '');
}
function getBasePath(asset: AssetData): string {
const basePath = asset.httpServerLocation;
return basePath.startsWith('/') ? basePath.slice(1) : basePath;
}
function copyFiles(logger: Logger, fileDestBySrc: CopiedFiles) {
const fileSources = Object.keys(fileDestBySrc);
if (fileSources.length === 0) {
return Promise.resolve();
}
const assetFilesCount = fileSources.length;
return new Promise<void>((resolve, reject) => {
const copyNext = (error?: Error) => {
if (error) {
reject(error);
return;
}
if (fileSources.length === 0) {
logger.info(
() =>
`Copied ${assetFilesCount} ${
assetFilesCount === 1 ? 'asset' : 'assets'
}`
);
debugLog('All assets copied, resolving promise...');
resolve();
debugLog('Assets copy promise resolved');
} else {
const src = fileSources.shift()!;
const dest = fileDestBySrc[src];
copyFile(src, dest, copyNext);
}
};
copyNext();
});
}
function copyFile(
src: string,
dest: string,
onFinished: (error?: Error) => void
): void {
const destDir = pathUtils.dirname(dest);
fs.mkdir(destDir, { recursive: true }, (err?) => {
if (err) {
onFinished(err);
return;
}
const readStream = fs.createReadStream(src);
const writeStream = fs.createWriteStream(dest);
let finished = false;
const handleError = (err: Error) => {
if (!finished) {
finished = true;
console.error(`[Asset Copy Error] ${src} -> ${dest}:`, err.message);
readStream.destroy();
writeStream.destroy();
onFinished(err);
}
};
const handleFinish = () => {
if (!finished) {
finished = true;
onFinished();
}
};
readStream.on('error', handleError);
writeStream.on('error', handleError);
writeStream.on('finish', handleFinish);
readStream.pipe(writeStream);
});
}