I think that's not possible. Instead, I'ld go for a tool which generates the code with all desired require
statements.
Here's what I came up with:
In tools/assetBundler.ts
:
import { resolve as resolvePath, relative as relativePath } from 'path';
import * as fs from 'fs';
const generatedFolderName = 'generated';
const ignorePattern = /^\.|\w*.(ts|js)$/m;
/**
* Generates `require` statements for all assets in a new file, so they will get bundled.
*/
const run = async () => {
try {
const inputFolder: string = resolvePath('.', 'app', 'assets');
const targetFolder: string = resolvePath('.', 'app', 'assets', 'assetManager', generatedFolderName);
const fileNames = await getAllFileNames(inputFolder);
await ensureEmptyFolder(targetFolder);
const mapEntries: string[] = [];
for (const fileName of fileNames)
mapEntries.push(`['${fileName}', {asset: require('${fileName.replace('app/assets', '../..')}')}],`);
const bundlerContent = `/**
* This file was automatically generated by the asset bundler script.
* DO NOT MODIFY IT BY HAND. Instead, modify the source asset and the data files,
* and run this script to regenerate this file.
*/
export const assetsByName = new Map<string, {asset: any}>([
${mapEntries.join('\n')}
]);\n`;
return fs.promises.writeFile(resolvePath(targetFolder, 'assetMap.ts'), bundlerContent);
} catch (e: unknown) {
console.log("Couldn't generate asset bundler:", e);
}
};
/**
* Based on https://stackoverflow.com/a/45130990/5767484, this method returns a list of all files names in the given folder and subfolders.
* @param dir the folder where to find the files in
* @returns the list of all files found
*/
const getAllFileNames: (dir: string) => Promise<string[]> = async (dir) => {
const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
dirents.map((dirent) => {
let fileName: string | null = null;
if (!ignorePattern.test(dirent.name))
fileName = relativePath(process.cwd(), resolvePath(dir, dirent.name));
return dirent.isDirectory() ? getAllFileNames(resolvePath(dir, dirent.name)) : [fileName];
})
);
return files.flat().filter(notEmpty);
};
/**
* Makes sure the given folder exists and is empty. Note that existing files in the folder we be deleted in case `enforceEmpty` isn't set to true.
* @param folder to be created and emptied
* @param enforceEmpty if `true`, existing files in the folder will be deleted
*/
const ensureEmptyFolder = async (folder: string, enforceEmpty: boolean = true) =>
fs.promises
.mkdir(folder, { recursive: true })
.then((_) => (enforceEmpty ? fs.promises.rm(folder, { recursive: true, force: true }) : undefined))
.then((_) => fs.promises.mkdir(folder))
.catch((e) => {
if (enforceEmpty || e.code !== 'EEXIST') throw e;
});
/** Filter for `null` and `undefined` providing type-safety.*/
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue => value != null
(async () => run())();
To run this tool, you can create npm scripts in your package.json
like:
"scripts": {
"asset-bundler": "ts-node tools/generateAssetBundler.ts",
"bundle-start": "npm run asset-bundler && expo start "
}
This way you'll get a file like
export const assetsByName = new Map<string, { asset: any }>([
['app/assets/salutt_splash_video_no_tagline.mp4', { asset: require('../../salutt_splash_video_no_tagline.mp4') }],
[ ... ],
...
]);
in app/assets/assetManager/assetMap.ts
, and you can import your assets by their qualified name, e.g.
const splashVid = assetsByName.get('app/assets/salutt_splash_video_no_tagline.mp4')`
You can create an assetManager
which holds the assetsByName
map
internally if you need convenience functions etc.
Note that my code is in TypeScript - I'ld heavily recommend you to use TypeScript, but it can by converted to JavaScript rather easily by deleting some type declarations. Also be careful about the pathes. They might differ a bit for you, so you might have to adjust them accordingly.