import { defineNuxtModule, useLogger } from "@nuxt/kit"; import { readdir, readFile, writeFile } from "fs/promises"; import { join, relative, parse } from "path"; import { existsSync, watch } from "fs"; type AssetsTree = { [key: string]: string | AssetsTree; }; const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]); export default defineNuxtModule({ meta: { name: "image-assets", configKey: "imageAssets", }, defaults: {}, async setup(_, nuxt) { const logger = useLogger("image-assets"); const publicImagesDir = join(nuxt.options.rootDir, "public/images"); const templateFile = join( nuxt.options.rootDir, "app/composables/useAssets.ts.in", ); const outputFile = join( nuxt.options.rootDir, "app/composables/useAssets.ts", ); const isImageFile = (filename: string): boolean => { const ext = parse(filename).ext.toLowerCase(); return IMAGE_EXTENSIONS.has(ext); }; const scanDirectory = async (dir: string): Promise => { const images: string[] = []; const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { images.push(...(await scanDirectory(fullPath))); } else if (isImageFile(entry.name)) { images.push(fullPath); } } return images; }; const toCamelCase = (str: string): string => { return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()); }; const buildAssetsTree = (images: string[], baseDir: string): AssetsTree => { const tree: AssetsTree = {}; for (const imagePath of images) { const relativePath = relative(baseDir, imagePath); const parts = relativePath.split("/"); const filename = parse(parts[parts.length - 1]!).name; let current = tree; for (let i = 0; i < parts.length - 1; i += 1) { const key = toCamelCase(parts[i]!); current[key] ??= {}; current = current[key] as AssetsTree; } current[toCamelCase(filename)] = `/images/${relativePath}`; } return tree; }; const generateAssetsObject = (tree: AssetsTree, indent = 0): string => { const spaces = " ".repeat(indent); const entries = Object.entries(tree); if (!entries.length) return "{}"; const lines = entries.map(([key, value]) => typeof value === "string" ? `${spaces} ${key}: createImage("${value}"),` : `${spaces} ${key}: ${generateAssetsObject(value, indent + 1)},`, ); return `{\n${lines.join("\n")}\n${spaces}}`; }; const generateAssetsFile = async () => { try { if (!existsSync(publicImagesDir)) { logger.warn("No public/images directory found"); return; } const images = await scanDirectory(publicImagesDir); const assetsTree = buildAssetsTree(images, publicImagesDir); const assetsObject = generateAssetsObject(assetsTree); const template = await readFile(templateFile, "utf-8"); const fileContent = template .replace("{{TOTAL}}", images.length.toString()) .replace("{{ASSETS}}", assetsObject); await writeFile(outputFile, fileContent, "utf-8"); logger.success(`Generated useAssets.ts with ${images.length} images`); } catch (error) { logger.error("Error generating assets file:", error); } }; nuxt.hook("build:before", async () => { await generateAssetsFile(); }); if (nuxt.options.dev) { nuxt.hook("ready", () => { watch(publicImagesDir, { recursive: true }, async (_, filePath) => { if (filePath && isImageFile(filePath)) { logger.info(`Detected change: ${filePath}`); await generateAssetsFile(); } }); }); } }, });