250 lines
7.0 KiB
JavaScript
250 lines
7.0 KiB
JavaScript
/**
|
|
* prerender.mjs — Genera HTML statico per le route pubbliche.
|
|
*
|
|
* Uso:
|
|
* pnpm build:static → quasar build + node scripts/prerender.mjs
|
|
*
|
|
* Output:
|
|
* dist/spa/index.html (route /)
|
|
* dist/spa/someroute/index.html (any additional public route)
|
|
*
|
|
* ─── Handler Go (esempio GoFiber) ────────────────────────────────────────────
|
|
*
|
|
* app.Static("/", "./dist/spa", fiber.Static{
|
|
* Index: "index.html",
|
|
* Browse: false,
|
|
* MaxAge: 0, // nessuna cache per l'HTML
|
|
* })
|
|
*
|
|
* // Fallback: serve index.html per qualsiasi percorso sconosciuto (routing client SPA)
|
|
* app.Use(func(c *fiber.Ctx) error {
|
|
* // Serve l'HTML prerenderizzato se esiste, altrimenti ripiega sulla shell SPA
|
|
* candidate := "./dist/spa" + c.Path() + "/index.html"
|
|
* if _, err := os.Stat(candidate); err == nil {
|
|
* return c.SendFile(candidate)
|
|
* }
|
|
* return c.SendFile("./dist/spa/index.html")
|
|
* })
|
|
*
|
|
* ─── Esempio net/http ─────────────────────────────────────────────────────────
|
|
*
|
|
* fs := http.FileServer(http.Dir("./dist/spa"))
|
|
* http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
* candidate := "./dist/spa" + path.Clean(r.URL.Path) + "/index.html"
|
|
* if _, err := os.Stat(candidate); err == nil {
|
|
* http.ServeFile(w, r, candidate)
|
|
* return
|
|
* }
|
|
* // Serve la root prerenderizzata o la shell SPA
|
|
* r2 := r.Clone(r.Context())
|
|
* r2.URL.Path = "/"
|
|
* fs.ServeHTTP(w, r2)
|
|
* })
|
|
*
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
import {
|
|
copyFileSync,
|
|
cpSync,
|
|
createReadStream,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { createServer } from 'node:http';
|
|
import { extname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
|
const DIST_DIR = resolve(__dirname, '../dist/spa');
|
|
const ENV_FILE = resolve(__dirname, '../.env');
|
|
const PORT = 4173;
|
|
|
|
/**
|
|
* Route pubbliche da prerenderizzare.
|
|
* Ogni voce produrra dist/spa{route}/index.html
|
|
*/
|
|
export const PUBLIC_ROUTES = [
|
|
'/',
|
|
// '/about',
|
|
// '/terms',
|
|
// '/privacy',
|
|
];
|
|
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'application/javascript',
|
|
'.mjs': 'application/javascript',
|
|
'.css': 'text/css',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.json': 'application/json',
|
|
'.woff2': 'font/woff2',
|
|
'.woff': 'font/woff',
|
|
'.ttf': 'font/ttf',
|
|
};
|
|
|
|
function startServer() {
|
|
const server = createServer((req, res) => {
|
|
const url = req.url?.split('?')[0] ?? '/';
|
|
let filePath = join(DIST_DIR, url === '/' ? 'index.html' : url);
|
|
|
|
// Fallback SPA: percorsi sconosciuti o senza estensione -> index.html
|
|
if (!existsSync(filePath) || !extname(filePath)) {
|
|
filePath = join(DIST_DIR, 'index.html');
|
|
}
|
|
|
|
const mime = MIME_TYPES[extname(filePath)] ?? 'application/octet-stream';
|
|
res.setHeader('Content-Type', mime);
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
|
|
createReadStream(filePath)
|
|
.on('error', () => {
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
})
|
|
.pipe(res);
|
|
});
|
|
|
|
return new Promise((resolve) => server.listen(PORT, () => resolve(server)));
|
|
}
|
|
|
|
function parseEnvFile(filePath) {
|
|
if (!existsSync(filePath)) {
|
|
return {};
|
|
}
|
|
|
|
const data = readFileSync(filePath, 'utf-8');
|
|
const env = {};
|
|
|
|
for (const line of data.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
const idx = trimmed.indexOf('=');
|
|
if (idx <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const key = trimmed.slice(0, idx).trim();
|
|
let value = trimmed.slice(idx + 1).trim();
|
|
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
|
|
env[key] = value;
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
function copyDirectoryContent(srcDir, destDir) {
|
|
mkdirSync(destDir, { recursive: true });
|
|
|
|
for (const entry of readdirSync(srcDir)) {
|
|
const srcPath = join(srcDir, entry);
|
|
const destPath = join(destDir, entry);
|
|
const stat = statSync(srcPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
} else {
|
|
copyFileSync(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
function copySpaToGoTarget() {
|
|
const envFromFile = parseEnvFile(ENV_FILE);
|
|
|
|
const goProjectDir = process.env.GO_PROJECT_DIR ?? envFromFile.GO_PROJECT_DIR;
|
|
const goSpaTargetDir =
|
|
process.env.GO_SPA_TARGET_DIR ??
|
|
envFromFile.GO_SPA_TARGET_DIR ??
|
|
(goProjectDir ? join(goProjectDir, 'spa') : null);
|
|
|
|
if (!goSpaTargetDir) {
|
|
console.log('│ copy : skipped (set GO_PROJECT_DIR or GO_SPA_TARGET_DIR in .env)');
|
|
return;
|
|
}
|
|
|
|
const resolvedTarget = resolve(goSpaTargetDir);
|
|
|
|
rmSync(resolvedTarget, { recursive: true, force: true });
|
|
mkdirSync(resolvedTarget, { recursive: true });
|
|
copyDirectoryContent(DIST_DIR, resolvedTarget);
|
|
|
|
console.log(`│ copy : ${DIST_DIR} -> ${resolvedTarget}`);
|
|
}
|
|
|
|
async function prerender() {
|
|
console.log('┌─ Static prerender ────────────────────────────────────────────');
|
|
console.log(`│ dist dir : ${DIST_DIR}`);
|
|
console.log(`│ routes : ${PUBLIC_ROUTES.join(', ')}`);
|
|
console.log('│');
|
|
|
|
if (!existsSync(DIST_DIR)) {
|
|
console.error('│ ERROR: dist/spa not found — run `pnpm build` first.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const server = await startServer();
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
|
|
for (const route of PUBLIC_ROUTES) {
|
|
process.stdout.write(`│ ${route.padEnd(30)} → `);
|
|
|
|
const page = await browser.newPage();
|
|
|
|
// Silenzia il rumore della console del browser
|
|
page.on('console', () => {});
|
|
|
|
await page.goto(`http://localhost:${PORT}${route}`, {
|
|
waitUntil: 'networkidle0',
|
|
timeout: 30_000,
|
|
});
|
|
|
|
const html = await page.content();
|
|
|
|
const outDir = route === '/' ? DIST_DIR : join(DIST_DIR, route);
|
|
mkdirSync(outDir, { recursive: true });
|
|
writeFileSync(join(outDir, 'index.html'), html, 'utf-8');
|
|
|
|
console.log(`${outDir}/index.html`);
|
|
await page.close();
|
|
}
|
|
|
|
await browser.close();
|
|
server.close();
|
|
|
|
copySpaToGoTarget();
|
|
|
|
console.log('│');
|
|
console.log('└─ Done ✓');
|
|
}
|
|
|
|
prerender().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|