refactor: implement scalable asset manifest system for dynamic images

Created build-time script to generate TypeScript manifest from public/assets, avoiding import.meta.glob limitations. Images stay in public/ for scalability (supports hundreds), while components get type-safe auto-discovery. Manifest auto-regenerates before builds.
This commit is contained in:
plebeius
2025-11-01 19:39:22 +01:00
parent 967584294c
commit f26da13c77
6 changed files with 83 additions and 27 deletions
+3
View File
@@ -49,6 +49,9 @@
"zustand": "4.4.3"
},
"scripts": {
"generate:assets": "node scripts/generate-asset-manifest.js",
"prebuild": "yarn generate:assets",
"prestart": "yarn generate:assets",
"start": "vite",
"build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build",
"build:preload": "vite build --config electron/vite.preload.config.js",
+38
View File
@@ -0,0 +1,38 @@
import { readdirSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', 'public');
// Scan for banners
const bannersDir = join(publicDir, 'assets', 'banners');
const banners = readdirSync(bannersDir)
.filter((file) => /^banner-.*\.(jpg|jpeg|gif|png)$/i.test(file))
.map((file) => `assets/banners/${file}`);
// Scan for not-found images
const notFoundDir = join(publicDir, 'assets', 'not-found');
const notFoundImages = readdirSync(notFoundDir)
.filter((file) => /^not-found-.*\.(jpg|jpeg|gif|png)$/i.test(file))
.map((file) => `assets/not-found/${file}`);
// Generate TypeScript file
const output = `// Auto-generated file - do not edit manually
// Run 'node scripts/generate-asset-manifest.js' to regenerate
// This file is generated from public/assets/ directory
export const BANNERS = ${JSON.stringify(banners, null, 2)} as const;
export const NOT_FOUND_IMAGES = ${JSON.stringify(notFoundImages, null, 2)} as const;
`;
// Ensure directory exists
const generatedDir = join(__dirname, '..', 'src', 'generated');
mkdirSync(generatedDir, { recursive: true });
const outputPath = join(generatedDir, 'asset-manifest.ts');
writeFileSync(outputPath, output, 'utf8');
console.log(`✅ Generated asset manifest with ${banners.length} banners and ${notFoundImages.length} not-found images`);
+2 -9
View File
@@ -12,17 +12,10 @@ import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import { shouldShowSnow } from '../../lib/snow';
import Tooltip from '../tooltip';
import _ from 'lodash';
// Dynamically import all banner images at build time
const bannerModules = import.meta.glob('/public/assets/banners/banner-*.{jpg,jpeg,gif,png}', {
eager: true,
query: '?url',
import: 'default',
});
const bannerPaths = Object.values(bannerModules) as string[];
import { BANNERS } from '../../generated/asset-manifest';
const ImageBanner = () => {
const [banner] = useState(() => bannerPaths[Math.floor(Math.random() * bannerPaths.length)]);
const [banner] = useState(() => BANNERS[Math.floor(Math.random() * BANNERS.length)]);
return <img src={banner} alt='' />;
};
+25
View File
@@ -0,0 +1,25 @@
# Generated Files
This directory contains auto-generated TypeScript files. Do not edit these files manually.
## asset-manifest.ts
Auto-generated manifest of all banner and not-found images from `public/assets/`.
**To regenerate:**
```bash
yarn generate:assets
```
**Auto-regenerated:**
- Before every build (`yarn build`)
- Before dev server start (`yarn start`)
**To add new banners:**
1. Add files to `public/assets/banners/` following the pattern `banner-*.{jpg,jpeg,gif,png}`
2. Run `yarn generate:assets` (or it will auto-run on next build/start)
**To add new not-found images:**
1. Add files to `public/assets/not-found/` following the pattern `not-found-*.{jpg,jpeg,gif,png}`
2. Run `yarn generate:assets` (or it will auto-run on next build/start)
+13
View File
@@ -0,0 +1,13 @@
// Auto-generated file - do not edit manually
// Run 'node scripts/generate-asset-manifest.js' to regenerate
// This file is generated from public/assets/ directory
export const BANNERS = [
'assets/banners/banner-1.jpg',
'assets/banners/banner-2.jpg',
'assets/banners/banner-3.gif',
'assets/banners/banner-4.gif',
'assets/banners/banner-5.png',
] as const;
export const NOT_FOUND_IMAGES = ['assets/not-found/not-found-1.png'] as const;
+2 -18
View File
@@ -3,26 +3,10 @@ import { Link, useLocation } from 'react-router-dom';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { HomeLogo } from '../home';
import styles from './not-found.module.css';
// Dynamically import all not-found images at build time
const notFoundModules = import.meta.glob('/public/assets/not-found/not-found-*.{jpg,jpeg,gif,png}', {
eager: true,
query: '?url',
import: 'default',
});
const notFoundPaths = Object.values(notFoundModules) as string[];
import { NOT_FOUND_IMAGES } from '../../generated/asset-manifest';
const NotFoundImage = () => {
const [imagePath] = useState(() => {
if (notFoundPaths.length === 0) {
return null;
}
return notFoundPaths[Math.floor(Math.random() * notFoundPaths.length)];
});
if (!imagePath) {
return null;
}
const [imagePath] = useState(() => NOT_FOUND_IMAGES[Math.floor(Math.random() * NOT_FOUND_IMAGES.length)]);
return <img src={imagePath} alt='' />;
};