← vibe-resume
import { chromium } from "playwright-core";
import { mkdir, readFile, unlink } from "node:fs/promises";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { assertPdfContainsRenderedText } from "./pdf-text-check.mjs";
import { createBrowserLaunchError } from "./export-errors.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const outputArg = process.argv[2];
const inputArg = process.argv[3] || process.env.RESUME_HTML;
const inputHtml = inputArg
? path.resolve(repoRoot, inputArg)
: path.join(repoRoot, "templates", "internship-employment", "standard-one-page", "index.html");
const outputPdf = path.resolve(repoRoot, outputArg || "export/vibe-resume-demo.pdf");
const defaultExportWidth = 1080;
function playwrightCacheRoots() {
const xdgCache = process.env.XDG_CACHE_HOME || path.join(process.env.HOME || "", ".cache");
const roots = [
process.env.PLAYWRIGHT_BROWSERS_PATH,
path.join(process.env.HOME || "", "Library", "Caches", "ms-playwright"),
path.join(xdgCache, "ms-playwright")
];
return [...new Set(roots.filter(Boolean))].filter((root) => existsSync(root));
}
function findInPlaywrightCache(prefix, relativePaths) {
for (const cacheRoot of playwrightCacheRoots()) {
const versions = readdirSync(cacheRoot)
.filter((name) => name.startsWith(prefix))
.sort()
.reverse();
for (const version of versions) {
for (const segments of relativePaths) {
const candidate = path.join(cacheRoot, version, ...segments);
if (existsSync(candidate)) return candidate;
}
}
}
return undefined;
}
function findPlaywrightHeadlessShell() {
const relativePaths =
process.platform === "darwin"
? [
["chrome-headless-shell-mac-arm64", "chrome-headless-shell"],
["chrome-headless-shell-mac-x64", "chrome-headless-shell"]
]
: [["chrome-linux", "headless_shell"]];
return findInPlaywrightCache("chromium_headless_shell-", relativePaths);
}
function findPlaywrightChromium() {
if (process.platform !== "linux") return undefined;
return findInPlaywrightCache("chromium-", [
["chrome-linux64", "chrome"],
["chrome-linux", "chrome"]
]);
}
const chromeCandidates = [
process.env.CHROME_PATH,
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
findPlaywrightHeadlessShell(),
chromium.executablePath(),
findPlaywrightChromium(),
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable"
].filter(Boolean);
const executablePath = chromeCandidates.find((candidate) => existsSync(candidate));
if (!executablePath) {
throw new Error(
"No Chromium executable found. Set CHROME_PATH to a Chrome/Chromium binary and rerun ./export-pdf.sh."
);
}
await mkdir(path.dirname(outputPdf), { recursive: true });
let browser;
try {
browser = await chromium.launch({
executablePath,
headless: true
});
} catch (error) {
throw createBrowserLaunchError(error, executablePath);
}
try {
const page = await browser.newPage({
deviceScaleFactor: 1,
viewport: {
width: 1200,
height: 2200
}
});
await page.emulateMedia({ media: "screen" });
await page.goto(pathToFileURL(inputHtml).href, { waitUntil: "networkidle" });
await page.evaluate(() => document.fonts?.ready);
const expectedTextLength = await page.evaluate(() =>
(document.body.innerText || "").replace(/\s+/g, "").length
);
const requestedExportWidth = await page.evaluate(() =>
Number.parseFloat(document.documentElement.dataset.exportWidth || document.body.dataset.exportWidth)
);
const exportWidth = Number.isFinite(requestedExportWidth)
? requestedExportWidth
: defaultExportWidth;
await page.addStyleTag({
content: `
html, body {
background: #fff !important;
margin: 0 !important;
padding: 0 !important;
}
.toolbar {
display: none !important;
}
.page {
border: 0 !important;
box-shadow: none !important;
margin: 0 !important;
min-height: 0 !important;
overflow: visible !important;
width: ${exportWidth}px !important;
}
.resume-book {
margin: 0 !important;
width: ${exportWidth}px !important;
}
.resume-page {
border: 0 !important;
box-shadow: none !important;
height: var(--page-height) !important;
margin: 0 !important;
min-height: var(--page-height) !important;
overflow: hidden !important;
width: ${exportWidth}px !important;
}
`
});
const pageSize = await page.evaluate(() => {
const resumePages = [...document.querySelectorAll(".resume-page")];
if (resumePages.length > 0) {
const firstRect = resumePages[0].getBoundingClientRect();
const heights = resumePages.map((pageEl) => Math.ceil(pageEl.getBoundingClientRect().height));
const pageHeight = Math.max(...heights);
const overflowingPages = resumePages
.map((pageEl, index) => ({
page: index + 1,
available: pageEl.clientHeight,
required: pageEl.scrollHeight
}))
.filter(({ available, required }) => required > available + 2);
if (overflowingPages.length > 0) {
const details = overflowingPages
.map(({ page, available, required }) => `page ${page}: ${required}px required, ${available}px available`)
.join("; ");
throw new Error(`Resume content overflows its fixed page: ${details}`);
}
for (const pageEl of resumePages) {
pageEl.style.height = `${pageHeight}px`;
pageEl.style.minHeight = `${pageHeight}px`;
}
return {
mode: "paged",
pages: resumePages.length,
width: Math.ceil(firstRect.width),
height: pageHeight
};
}
const pageEl = document.querySelector(".page");
if (!pageEl) {
throw new Error("Could not find .page element.");
}
const rect = pageEl.getBoundingClientRect();
const lastChild = pageEl.lastElementChild;
const lastRect = lastChild?.getBoundingClientRect();
const contentBottom = lastRect ? lastRect.bottom - rect.top : pageEl.scrollHeight;
const paddingBottom = Number.parseFloat(getComputedStyle(pageEl).paddingBottom) || 0;
return {
mode: "continuous",
pages: 1,
width: Math.ceil(rect.width),
height: Math.ceil(Math.max(pageEl.scrollHeight, contentBottom + paddingBottom))
};
});
const pdfHeight = pageSize.mode === "paged" ? pageSize.height : pageSize.height + 35;
await page.addStyleTag({
content: `
@page {
margin: 0;
size: ${pageSize.width}px ${pdfHeight}px;
}
.resume-page {
break-after: page;
page-break-after: always;
}
.resume-page:last-child {
break-after: auto;
page-break-after: auto;
}
`
});
await page.setViewportSize({
width: pageSize.width,
height: pdfHeight
});
await page.pdf({
path: outputPdf,
width: `${pageSize.width}px`,
height: `${pdfHeight}px`,
margin: {
top: "0",
right: "0",
bottom: "0",
left: "0"
},
preferCSSPageSize: true,
printBackground: true,
scale: 1
});
try {
const pdfBuffer = await readFile(outputPdf);
assertPdfContainsRenderedText(pdfBuffer, expectedTextLength);
} catch (error) {
await unlink(outputPdf).catch(() => {});
throw error;
}
console.log(`PDF exported: ${path.relative(repoRoot, outputPdf)}`);
console.log(`Input HTML: ${path.relative(repoRoot, inputHtml)}`);
console.log(`Layout: ${pageSize.mode} (${pageSize.pages} page${pageSize.pages === 1 ? "" : "s"})`);
console.log(`Rendered page size: ${pageSize.width}px x ${pageSize.height}px`);
console.log(`PDF page size: ${pageSize.width}px x ${pdfHeight}px`);
console.log(`Chromium: ${executablePath}`);
} finally {
await browser.close();
}