UNPKG

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
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))); };