314 lines
8.0 KiB
JavaScript
Executable File
314 lines
8.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Screenshot all templates by starting each dev server, capturing screenshots, and stopping.
|
|
*
|
|
* Usage:
|
|
* node scripts/screenshot-all-templates.mjs [template...]
|
|
* node scripts/screenshot-all-templates.mjs # all templates
|
|
* node scripts/screenshot-all-templates.mjs blog # just blog
|
|
* node scripts/screenshot-all-templates.mjs blog marketing # blog and marketing
|
|
*/
|
|
|
|
import { spawn, execSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, "..");
|
|
|
|
const TEMPLATES = {
|
|
blog: { dir: "templates/blog", port: 4321 },
|
|
marketing: { dir: "templates/marketing", port: 4322 },
|
|
portfolio: { dir: "templates/portfolio", port: 4323 },
|
|
};
|
|
|
|
function loadConfig() {
|
|
const configPath = join(ROOT, "templates", "screenshots.json");
|
|
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
}
|
|
|
|
/** Check if server is responding via HTTP */
|
|
async function isServerReady(port) {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
const response = await fetch(`http://localhost:${port}/`, {
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeout);
|
|
return response.ok || response.status === 404; // 404 is fine, server is up
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Wait for server to respond, with timeout */
|
|
async function waitForServer(port, timeoutMs = 60000) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
if (await isServerReady(port)) {
|
|
return true;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Check if port has processes (via lsof) */
|
|
function hasProcessOnPort(port) {
|
|
try {
|
|
const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
|
|
return result.trim().length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Wait for port to be free */
|
|
async function waitForPortFree(port, timeoutMs = 10000) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
if (!hasProcessOnPort(port)) {
|
|
return true;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Kill all processes listening on a port (macOS/Linux) */
|
|
function killProcessesOnPort(port) {
|
|
try {
|
|
// Get PIDs listening on this port
|
|
const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
|
|
const pids = result
|
|
.trim()
|
|
.split("\n")
|
|
.filter((p) => p);
|
|
|
|
for (const pid of pids) {
|
|
try {
|
|
process.kill(Number(pid), "SIGTERM");
|
|
} catch {
|
|
// Process may already be dead
|
|
}
|
|
}
|
|
|
|
// If still running after 2s, force kill
|
|
if (pids.length > 0) {
|
|
setTimeout(() => {
|
|
for (const pid of pids) {
|
|
try {
|
|
process.kill(Number(pid), "SIGKILL");
|
|
} catch {
|
|
// Process may already be dead
|
|
}
|
|
}
|
|
}, 2000);
|
|
}
|
|
} catch {
|
|
// lsof may not be available or no processes found
|
|
}
|
|
}
|
|
|
|
function startDevServer(templateDir, port) {
|
|
return new Promise((resolve, reject) => {
|
|
// Run astro dev directly in the template directory
|
|
const proc = spawn("pnpm", ["exec", "astro", "dev", "--port", String(port)], {
|
|
cwd: join(ROOT, templateDir),
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
detached: false,
|
|
});
|
|
|
|
let output = "";
|
|
|
|
const onData = (data) => {
|
|
output += data.toString();
|
|
process.stdout.write(data); // Show output for debugging
|
|
};
|
|
|
|
proc.stdout.on("data", onData);
|
|
proc.stderr.on("data", onData);
|
|
|
|
proc.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
|
|
proc.on("exit", (code) => {
|
|
// If process exits before we resolve, that's an error
|
|
reject(new Error(`Dev server exited with code ${code}`));
|
|
});
|
|
|
|
// Wait for server to respond via HTTP
|
|
waitForServer(port, 60000)
|
|
.then((ready) => {
|
|
if (ready) {
|
|
// Remove exit handler since we're resolving successfully
|
|
proc.removeAllListeners("exit");
|
|
// Re-add a silent exit handler
|
|
proc.on("exit", () => {});
|
|
resolve({ proc, port });
|
|
} else {
|
|
proc.kill();
|
|
reject(new Error(`Timeout waiting for server on port ${port}`));
|
|
}
|
|
return undefined;
|
|
})
|
|
.catch(reject);
|
|
});
|
|
}
|
|
|
|
function runScreenshots(template, url) {
|
|
return new Promise((resolve, reject) => {
|
|
const proc = spawn("node", [join(ROOT, "scripts", "screenshot-templates.mjs"), template, url], {
|
|
cwd: ROOT,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
proc.on("error", reject);
|
|
proc.on("exit", (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Screenshot script exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function stopDevServer({ proc, port }) {
|
|
// Kill the process tree
|
|
try {
|
|
proc.kill("SIGTERM");
|
|
} catch {
|
|
// May already be dead
|
|
}
|
|
|
|
// Also kill anything on the port (catches child processes)
|
|
killProcessesOnPort(port);
|
|
|
|
// Wait for port to actually be free
|
|
const closed = await waitForPortFree(port, 10000);
|
|
if (!closed) {
|
|
console.warn(`Warning: Port ${port} still in use after stopping server`);
|
|
// Force kill anything still on the port
|
|
killProcessesOnPort(port);
|
|
await waitForPortFree(port, 5000);
|
|
}
|
|
}
|
|
|
|
/** Run bootstrap (reset db and seed) for a template */
|
|
async function bootstrapTemplate(templateDir) {
|
|
return new Promise((resolve, reject) => {
|
|
console.log(`Bootstrapping ${templateDir}...`);
|
|
const proc = spawn("pnpm", ["bootstrap"], {
|
|
cwd: join(ROOT, templateDir),
|
|
stdio: "inherit",
|
|
});
|
|
|
|
proc.on("error", reject);
|
|
proc.on("exit", (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Bootstrap exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function processTemplate(template) {
|
|
const config = TEMPLATES[template];
|
|
if (!config) {
|
|
console.error(`Unknown template: ${template}`);
|
|
console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
|
|
return false;
|
|
}
|
|
|
|
const screenshotsConfig = loadConfig();
|
|
if (!screenshotsConfig[template]) {
|
|
console.error(`No screenshot config for template: ${template}`);
|
|
return false;
|
|
}
|
|
|
|
// Make sure port is free before starting
|
|
if (hasProcessOnPort(config.port)) {
|
|
console.log(`Port ${config.port} is in use, killing existing processes...`);
|
|
killProcessesOnPort(config.port);
|
|
await waitForPortFree(config.port, 5000);
|
|
}
|
|
|
|
console.log(`\n${"=".repeat(60)}`);
|
|
console.log(`${template} (${config.dir})`);
|
|
console.log("=".repeat(60));
|
|
|
|
let server;
|
|
try {
|
|
// Bootstrap first to ensure database has seed content
|
|
await bootstrapTemplate(config.dir);
|
|
|
|
console.log(`Starting dev server...`);
|
|
server = await startDevServer(config.dir, config.port);
|
|
console.log(`Dev server ready at http://localhost:${config.port}\n`);
|
|
|
|
await runScreenshots(template, `http://localhost:${config.port}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.error(`Failed to process ${template}:`, err.message);
|
|
return false;
|
|
} finally {
|
|
if (server) {
|
|
console.log(`Stopping ${template} dev server...`);
|
|
await stopDevServer(server);
|
|
// Extra pause to ensure cleanup
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
const args = process.argv.slice(2);
|
|
const templates = args.length > 0 ? args : Object.keys(TEMPLATES);
|
|
|
|
// Validate all templates first
|
|
for (const template of templates) {
|
|
if (!TEMPLATES[template]) {
|
|
console.error(`Unknown template: ${template}`);
|
|
console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log(`\nScreenshotting templates: ${templates.join(", ")}`);
|
|
|
|
const results = { success: [], failed: [] };
|
|
|
|
for (const template of templates) {
|
|
const success = await processTemplate(template);
|
|
if (success) {
|
|
results.success.push(template);
|
|
} else {
|
|
results.failed.push(template);
|
|
}
|
|
}
|
|
|
|
console.log(`\n${"=".repeat(60)}`);
|
|
console.log("Summary");
|
|
console.log("=".repeat(60));
|
|
|
|
if (results.success.length > 0) {
|
|
console.log(`Succeeded: ${results.success.join(", ")}`);
|
|
}
|
|
if (results.failed.length > 0) {
|
|
console.log(`Failed: ${results.failed.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
run().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|