feat(assets): load 3d models as well
This commit is contained in:
145
modules/asset-generator.ts
Normal file
145
modules/asset-generator.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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 = [".png", ".webp"];
|
||||
const MODELS_EXTENSIONS = [".gltf"];
|
||||
const ASSETS_EXTENSIONS = [...IMAGE_EXTENSIONS, ...MODELS_EXTENSIONS];
|
||||
|
||||
export default defineNuxtModule({
|
||||
meta: {
|
||||
name: "asset-generator",
|
||||
configKey: "assetGenerator",
|
||||
},
|
||||
defaults: {},
|
||||
async setup(_, nuxt) {
|
||||
const logger = useLogger("asset-generator");
|
||||
const publicDir = join(nuxt.options.rootDir, "public");
|
||||
const templateFile = join(
|
||||
nuxt.options.rootDir,
|
||||
"app/composables/useAssets.ts.in",
|
||||
);
|
||||
const outputFile = join(
|
||||
nuxt.options.rootDir,
|
||||
"app/composables/useAssets.ts",
|
||||
);
|
||||
|
||||
const hasExt = (filename: string, exts: string[]): boolean => {
|
||||
const ext = parse(filename).ext.toLowerCase();
|
||||
return exts.includes(ext);
|
||||
};
|
||||
|
||||
const scanDirectory = async (dir: string): Promise<string[]> => {
|
||||
const assets: string[] = [];
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const assetFiles = await scanDirectory(fullPath);
|
||||
assets.push(...assetFiles);
|
||||
} else if (hasExt(fullPath, ASSETS_EXTENSIONS)) {
|
||||
assets.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return assets;
|
||||
};
|
||||
|
||||
const toCamelCase = (str: string): string => {
|
||||
return str
|
||||
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
||||
.replaceAll(".", "_");
|
||||
};
|
||||
|
||||
const buildAssetsTree = (assets: string[], baseDir: string): AssetsTree => {
|
||||
const tree: AssetsTree = {};
|
||||
|
||||
for (const assetPath of assets) {
|
||||
const relativePath = relative(baseDir, assetPath);
|
||||
const parts = relativePath.split("/");
|
||||
const filename = parse(parts[parts.length - 1]!).name;
|
||||
|
||||
let current = tree;
|
||||
// start at 1 to skip images/, models/
|
||||
for (let i = 1; i < parts.length - 1; i += 1) {
|
||||
const key = toCamelCase(parts[i]!);
|
||||
current[key] ??= {};
|
||||
current = current[key] as AssetsTree;
|
||||
}
|
||||
|
||||
current[toCamelCase(filename)] = `/${relativePath}`;
|
||||
}
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
const generatorFn = (filePath: string): string => {
|
||||
if (hasExt(filePath, IMAGE_EXTENSIONS)) return "createImage";
|
||||
if (hasExt(filePath, MODELS_EXTENSIONS)) return "createModel";
|
||||
throw new Error(`No matching generator for '${filePath}'`);
|
||||
};
|
||||
|
||||
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}: ${generatorFn(value)}("${value}"),`
|
||||
: `${spaces} ${key}: ${generateAssetsObject(value, indent + 1)},`,
|
||||
);
|
||||
|
||||
return `{\n${lines.join("\n")}\n${spaces}}`;
|
||||
};
|
||||
|
||||
const generateAssetsFile = async () => {
|
||||
try {
|
||||
if (!existsSync(publicDir)) {
|
||||
logger.warn("No public directory found");
|
||||
return;
|
||||
}
|
||||
|
||||
const assets = await scanDirectory(publicDir);
|
||||
const assetsTree = buildAssetsTree(assets, publicDir);
|
||||
const assetsObject = generateAssetsObject(assetsTree);
|
||||
|
||||
const template = await readFile(templateFile, "utf-8");
|
||||
const fileContent = template
|
||||
.replace("{{TOTAL}}", assets.length.toString())
|
||||
.replace("{{ASSETS}}", assetsObject);
|
||||
|
||||
await writeFile(outputFile, fileContent, "utf-8");
|
||||
logger.success(`Generated useAssets.ts with ${assets.length} assets`);
|
||||
} catch (error) {
|
||||
logger.error("Error generating assets file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
nuxt.hook("build:before", async () => {
|
||||
await generateAssetsFile();
|
||||
});
|
||||
|
||||
if (nuxt.options.dev) {
|
||||
nuxt.hook("ready", () => {
|
||||
watch(publicDir, { recursive: true }, async (_, filePath) => {
|
||||
if (filePath && hasExt(filePath, ASSETS_EXTENSIONS)) {
|
||||
logger.info(`Detected change: ${filePath}`);
|
||||
await generateAssetsFile();
|
||||
}
|
||||
});
|
||||
|
||||
watch(templateFile, async () => {
|
||||
logger.info("Template file changed");
|
||||
await generateAssetsFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user