cffy.in/scripts/build.js

201 lines
7.1 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const { execSync } = require('node:child_process');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const APPS_DIR = path.join(SRC_DIR, 'apps');
// Standard Navigation Template
// Note: We use absolute paths (/) so they work from any depth, assuming Cloudflare Pages root.
const NAV_TEMPLATE = `
<nav class="bg-white/70 backdrop-blur-sm border-b-2 border-amber-800 p-3 sticky top-0 z-20">
<div class="max-w-6xl mx-auto flex justify-between items-center font-impact text-amber-900">
<a href="/" class="text-2xl font-black hover:text-amber-700 transition-colors">cffyin</a>
<div class="flex gap-6 text-lg items-center">
<a href="/" class="hover:underline">Home</a>
<a href="/german-trainer/" class="hover:underline">German Trainer</a>
<a href="/hexarow/" class="hover:underline">Hex-a-Row</a>
<a href="/snapmark/" class="hover:underline">SnapMark</a>
<a href="/bridge-builder-math/" class="hover:underline">Bridge Builder</a>
</div>
</div>
</nav>
`;
function cleanDist() {
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true, force: true });
}
fs.mkdirSync(DIST_DIR);
}
function processHtml(filePath, outPath) {
let content = fs.readFileSync(filePath, 'utf-8');
// Simple regex-based injection.
// 1. Remove existing <nav>...</nav> (if any) to avoid duplication.
// 2. Inject new NAV_TEMPLATE at the start of <body>.
// Remove existing nav
content = content.replace(/<nav[\s\S]*?<\/nav>/i, '');
// Inject new nav
if (content.includes('<body')) {
content = content.replace(/<body([^>]*)>/i, `<body>\n${NAV_TEMPLATE}`);
} else {
// Fallback if no body tag found (unlikely for valid HTML)
content = NAV_TEMPLATE + content;
}
// Inject Apps Grid if placeholder exists
if (content.includes('<!-- APPS_GRID -->')) {
const appsGridHtml = generateAppsGridHtml();
content = content.replace('<!-- APPS_GRID -->', appsGridHtml);
}
fs.writeFileSync(outPath, content);
console.log(`Processed ${filePath} -> ${outPath}`);
}
function generateAppsGridHtml() {
if (!fs.existsSync(APPS_DIR)) return '';
const apps = fs.readdirSync(APPS_DIR).filter(item => {
const itemPath = path.join(APPS_DIR, item);
return !item.startsWith('.') && fs.statSync(itemPath).isDirectory();
});
return apps.map(app => {
const metadataPath = path.join(APPS_DIR, app, 'metadata.json');
let metadata = {
title: app,
description: 'A cool app.',
emoji: '📱',
color: 'gray',
path: `/${app}/`
};
if (fs.existsSync(metadataPath)) {
try {
const data = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
metadata = { ...metadata, ...data };
} catch (e) {
console.error(`Error reading metadata for ${app}:`, e);
}
}
return `
<div class="group">
<a href="${metadata.path}" class="block bg-${metadata.color}-400 border-4 border-${metadata.color}-600 shadow-lg p-6 space-y-4 transition-all duration-300 transform rotate-1 group-hover:rotate-0 group-hover:scale-105">
<div class="text-center">
<div class="text-6xl mb-3">${metadata.emoji}</div>
<h4 class="font-impact text-3xl font-black text-gray-900 mb-2">${metadata.title}</h4>
<p class="text-gray-800 font-bold text-lg">${metadata.description}</p>
</div>
<div class="flex justify-center">
<div class="px-4 py-2 text-sm font-black border-2 bg-green-400 border-green-700 text-green-900">ONLINE</div>
</div>
</a>
</div>
`;
}).join('\n');
}
function buildLanding() {
const landingSrc = path.join(SRC_DIR, 'landing', 'index.html');
const landingDist = path.join(DIST_DIR, 'index.html');
if (fs.existsSync(landingSrc)) {
processHtml(landingSrc, landingDist);
} else {
console.error("Landing page not found!");
}
}
function buildApps() {
if (!fs.existsSync(APPS_DIR)) return;
const apps = fs.readdirSync(APPS_DIR).filter(item => {
const itemPath = path.join(APPS_DIR, item);
return !item.startsWith('.') && fs.statSync(itemPath).isDirectory();
});
apps.forEach(app => {
const appSrc = path.join(APPS_DIR, app);
const appDist = path.join(DIST_DIR, app);
fs.mkdirSync(appDist, { recursive: true });
// Check if it's a Vite app (has package.json and vite.config.ts)
if (fs.existsSync(path.join(appSrc, 'package.json')) && fs.existsSync(path.join(appSrc, 'vite.config.ts'))) {
console.log(`Building Vite app: ${app}...`);
try {
// Install dependencies if needed
if (!fs.existsSync(path.join(appSrc, 'node_modules'))) {
console.log(`Installing dependencies for ${app}...`);
execSync('npm install', { cwd: appSrc, stdio: 'inherit' });
}
// Build
execSync('npm run build', { cwd: appSrc, stdio: 'inherit' });
// Copy dist contents
const viteDist = path.join(appSrc, 'dist');
if (fs.existsSync(viteDist)) {
fs.cpSync(viteDist, appDist, { recursive: true });
} else {
console.error(`Vite build failed to produce 'dist' for ${app}`);
}
} catch (e) {
console.error(`Failed to build ${app}:`, e);
}
} else {
// Standard HTML app
console.log(`Processing HTML app: ${app}...`);
const files = fs.readdirSync(appSrc);
files.forEach(file => {
const srcFile = path.join(appSrc, file);
const distFile = path.join(appDist, file);
if (file.endsWith('.html')) {
processHtml(srcFile, distFile);
} else {
fs.cpSync(srcFile, distFile, { recursive: true });
}
});
}
});
}
function createRedirect(filename, target) {
const content = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=${target}" />
<script>window.location.href = "${target}";</script>
<title>Redirecting...</title>
</head>
<body>
<p>If you are not redirected automatically, follow this <a href="${target}">link</a>.</p>
</body>
</html>`;
const filePath = path.join(DIST_DIR, filename);
fs.writeFileSync(filePath, content);
console.log(`Created static redirect: ${filename} -> ${target}`);
}
function main() {
console.log("Starting Build...");
cleanDist();
buildLanding();
buildApps();
// Create static redirects for legacy paths
createRedirect('game.html', '/hexarow/');
createRedirect('german.html', '/german-trainer/');
console.log("Build Complete!");
}
main();