feat: cache atlas and update nuxt version to win on compilation time
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
import { defineNuxtModule, useLogger } from "@nuxt/kit";
|
import { defineNuxtModule, useLogger } from "@nuxt/kit";
|
||||||
import { readdir, readFile, writeFile } from "fs/promises";
|
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
||||||
import { join, relative, parse } from "path";
|
import { dirname, join, relative, parse } from "path";
|
||||||
import { existsSync, watch } from "fs";
|
import { existsSync, watch } from "fs";
|
||||||
import { execSync } from "child_process";
|
import { execFile } from "child_process";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import { promisify } from "util";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
type AtlasRect = {
|
type AtlasRect = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -79,6 +83,7 @@ export default defineNuxtModule({
|
|||||||
const templateFile = join(rootDir, "app/composables/useAssets.ts.in");
|
const templateFile = join(rootDir, "app/composables/useAssets.ts.in");
|
||||||
const outputFile = join(rootDir, "app/composables/useAssets.ts");
|
const outputFile = join(rootDir, "app/composables/useAssets.ts");
|
||||||
const atlasOutputPath = join(rootDir, "public/nds/atlas.webp");
|
const atlasOutputPath = join(rootDir, "public/nds/atlas.webp");
|
||||||
|
const atlasHashFile = join(rootDir, "node_modules/.cache/asset-generator/atlas-hash");
|
||||||
|
|
||||||
const loadImages = async (
|
const loadImages = async (
|
||||||
paths: string[],
|
paths: string[],
|
||||||
@@ -88,10 +93,8 @@ export default defineNuxtModule({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
paths.map(async (path) => {
|
paths.map(async (path) => {
|
||||||
try {
|
try {
|
||||||
const [metadata, buffer] = await Promise.all([
|
const buffer = await readFile(path);
|
||||||
sharp(path).metadata(),
|
const metadata = await sharp(buffer).metadata();
|
||||||
sharp(path).png().toBuffer(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (metadata.width && metadata.height) {
|
if (metadata.width && metadata.height) {
|
||||||
images.set(path, {
|
images.set(path, {
|
||||||
@@ -256,7 +259,10 @@ ${sp} }`;
|
|||||||
|
|
||||||
const generateAssets = async () => {
|
const generateAssets = async () => {
|
||||||
try {
|
try {
|
||||||
const imagePaths = await scanByExt(imagesDir, IMAGE_EXTENSIONS);
|
// sorted so packing (and therefore the atlas hash) is deterministic
|
||||||
|
const imagePaths = (
|
||||||
|
await scanByExt(imagesDir, IMAGE_EXTENSIONS)
|
||||||
|
).sort();
|
||||||
const modelPaths = await scanByExt(modelsDir, MODEL_EXTENSIONS);
|
const modelPaths = await scanByExt(modelsDir, MODEL_EXTENSIONS);
|
||||||
const audioPaths = await scanByExt(audioDir, AUDIO_EXTENSIONS);
|
const audioPaths = await scanByExt(audioDir, AUDIO_EXTENSIONS);
|
||||||
|
|
||||||
@@ -266,15 +272,30 @@ ${sp} }`;
|
|||||||
let atlasHash = "";
|
let atlasHash = "";
|
||||||
let atlasWidth = 0;
|
let atlasWidth = 0;
|
||||||
let atlasHeight = 0;
|
let atlasHeight = 0;
|
||||||
|
let atlasCached = false;
|
||||||
|
|
||||||
if (imagePaths.length > 0) {
|
if (imagePaths.length > 0) {
|
||||||
const imageMap = await loadImages(imagePaths);
|
const imageMap = await loadImages(imagePaths);
|
||||||
const images = Array.from(imageMap.values());
|
const images = imagePaths.flatMap((path) => imageMap.get(path) ?? []);
|
||||||
|
|
||||||
const { rects, width, height } = packImages(images);
|
const { rects, width, height } = packImages(images);
|
||||||
atlasWidth = width;
|
atlasWidth = width;
|
||||||
atlasHeight = height;
|
atlasHeight = height;
|
||||||
|
|
||||||
|
const imageTree = buildImageTree(imagePaths, rects);
|
||||||
|
imageCode = generateImageCode(imageTree);
|
||||||
|
|
||||||
|
// imageCode covers names and layout, buffers cover pixels
|
||||||
|
const hash = createHash("sha256").update(imageCode);
|
||||||
|
for (const img of images) hash.update(img.buffer);
|
||||||
|
atlasHash = hash.digest("hex").slice(0, 8);
|
||||||
|
|
||||||
|
const cachedHash = await readFile(atlasHashFile, "utf-8").catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
atlasCached = cachedHash === atlasHash && existsSync(atlasOutputPath);
|
||||||
|
|
||||||
|
if (!atlasCached) {
|
||||||
const atlasBuffer = await sharp({
|
const atlasBuffer = await sharp({
|
||||||
create: {
|
create: {
|
||||||
width,
|
width,
|
||||||
@@ -294,15 +315,18 @@ ${sp} }`;
|
|||||||
|
|
||||||
await writeFile(atlasOutputPath, atlasBuffer);
|
await writeFile(atlasOutputPath, atlasBuffer);
|
||||||
|
|
||||||
execSync(
|
await execFileAsync("magick", [
|
||||||
`magick "${atlasOutputPath}" -define webp:lossless=true -define webp:method=6 "${atlasOutputPath}"`,
|
atlasOutputPath,
|
||||||
{ stdio: "pipe" },
|
"-define",
|
||||||
);
|
"webp:lossless=true",
|
||||||
|
"-define",
|
||||||
|
"webp:method=6",
|
||||||
|
atlasOutputPath,
|
||||||
|
]);
|
||||||
|
|
||||||
atlasHash = Math.random().toString(36).slice(2, 10);
|
await mkdir(dirname(atlasHashFile), { recursive: true });
|
||||||
|
await writeFile(atlasHashFile, atlasHash, "utf-8");
|
||||||
const imageTree = buildImageTree(imagePaths, rects);
|
}
|
||||||
imageCode = generateImageCode(imageTree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelTree = buildModelTree(modelPaths);
|
const modelTree = buildModelTree(modelPaths);
|
||||||
@@ -319,9 +343,16 @@ ${sp} }`;
|
|||||||
.replace("{{MODELS}}", modelCode)
|
.replace("{{MODELS}}", modelCode)
|
||||||
.replace("{{AUDIO}}", audioCode);
|
.replace("{{AUDIO}}", audioCode);
|
||||||
|
|
||||||
|
// rewriting an identical file would still invalidate it in Vite
|
||||||
|
const previousCode = await readFile(outputFile, "utf-8").catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
if (previousCode !== code) {
|
||||||
await writeFile(outputFile, code, "utf-8");
|
await writeFile(outputFile, code, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`Generated assets with ${imagePaths.length} images (${atlasWidth}x${atlasHeight}), ${modelPaths.length} models and ${audioPaths.length} audio files`,
|
`Generated assets with ${imagePaths.length} images (${atlasWidth}x${atlasHeight}${atlasCached ? ", cached" : ""}), ${modelPaths.length} models and ${audioPaths.length} audio files`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error generating assets:", error);
|
logger.error("Error generating assets:", error);
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ export default defineNuxtConfig({
|
|||||||
redirectOn: "root",
|
redirectOn: "root",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ui: {
|
||||||
|
// JetBrains Mono is loaded from bunny.net in app.css
|
||||||
|
fonts: false,
|
||||||
|
experimental: {
|
||||||
|
componentDetection: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
image: {
|
image: {
|
||||||
quality: 80,
|
quality: 80,
|
||||||
format: ["webp"],
|
format: ["webp"],
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -20,29 +20,29 @@
|
|||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"gsap": "3.13.0",
|
"gsap": "3.13.0",
|
||||||
"pinia": "3.0.4",
|
"pinia": "3.0.4",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.3.3",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0",
|
||||||
"vue": "3.5.25",
|
"vue": "3.5.43",
|
||||||
"vue-router": "4.6.3",
|
"vue-router": "5.3.1",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.30.2",
|
"packageManager": "pnpm@10.30.2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/content": "3.8.2",
|
"@nuxt/content": "3.16.1",
|
||||||
"@nuxt/eslint": "1.10.0",
|
"@nuxt/eslint": "1.17.0",
|
||||||
"@nuxt/image": "2.0.0",
|
"@nuxt/image": "2.1.0",
|
||||||
"@nuxt/kit": "4.2.1",
|
"@nuxt/kit": "4.6.0",
|
||||||
"@nuxt/ui": "4.3.0",
|
"@nuxt/ui": "4.11.3",
|
||||||
"@nuxtjs/i18n": "10.2.1",
|
"@nuxtjs/i18n": "10.6.0",
|
||||||
"@nuxtjs/mdc": "0.18.4",
|
"@nuxtjs/mdc": "0.23.2",
|
||||||
"@pinia/nuxt": "0.11.3",
|
"@pinia/nuxt": "0.11.3",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/three": "^0.182.0",
|
"@types/three": "^0.182.0",
|
||||||
"better-sqlite3": "12.4.1",
|
"better-sqlite3": "12.11.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"nuxt": "4.2.1",
|
"nuxt": "4.6.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.35.5",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19603
pnpm-lock.yaml
generated
19603
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user