| Server IP : 159.203.156.69 / Your IP : 216.73.217.172 Web Server : nginx/1.24.0 System : Linux main-ubuntu 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 User : root ( 0) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/tanviranik.com/node_modules/vinext/dist/cloudflare/ |
Upload File : |
{"version":3,"file":"tpr.js","names":[],"sources":["../../src/cloudflare/tpr.ts"],"sourcesContent":["/**\n * TPR: Traffic-aware Pre-Rendering\n *\n * Uses Cloudflare zone analytics to determine which pages actually get\n * traffic, and pre-renders only those during deploy. The pre-rendered\n * HTML is uploaded to KV in the same format ISR uses at runtime — no\n * runtime changes needed.\n *\n * Flow:\n * 1. Parse wrangler config to find custom domain and KV namespace\n * 2. Resolve the Cloudflare zone for the custom domain\n * 3. Query zone analytics (GraphQL) for top pages by request count\n * 4. Walk ranked list until coverage threshold is met\n * 5. Start the built production server locally\n * 6. Fetch each hot route to produce HTML\n * 7. Upload pre-rendered HTML to KV (same KVCacheEntry format ISR reads)\n *\n * TPR is an experimental feature enabled via --experimental-tpr. It\n * gracefully skips when no custom domain, no API token, no traffic data,\n * or no KV namespace is configured.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { VINEXT_REVALIDATE_HEADER } from \"../server/headers.js\";\nimport { isrCacheKey } from \"../server/isr-cache.js\";\nimport { ENTRY_PREFIX } from \"./kv-cache-handler.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type TPROptions = {\n /** Project root directory. */\n root: string;\n /** Traffic coverage percentage (0–100). Default: 90. */\n coverage: number;\n /** Hard cap on number of pages to pre-render. Default: 1000. */\n limit: number;\n /** Analytics lookback window in hours. Default: 24. */\n window: number;\n};\n\nexport type TPRResult = {\n /** Total unique page paths found in analytics. */\n totalPaths: number;\n /** Number of pages successfully pre-rendered and uploaded. */\n prerenderedCount: number;\n /** Actual traffic coverage achieved (percentage). */\n coverageAchieved: number;\n /** Wall-clock duration of the TPR step in milliseconds. */\n durationMs: number;\n /** If TPR was skipped, the reason. */\n skipped?: string;\n};\n\ntype TrafficEntry = {\n path: string;\n requests: number;\n};\n\ntype SelectedRoutes = {\n routes: TrafficEntry[];\n totalRequests: number;\n coveredRequests: number;\n coveragePercent: number;\n};\n\ntype PrerenderResult = {\n html: string;\n status: number;\n headers: Record<string, string>;\n};\n\ntype WranglerConfig = {\n accountId?: string;\n kvNamespaceId?: string;\n customDomain?: string;\n};\n\n// ─── Wrangler Config Parsing ─────────────────────────────────────────────────\n\n/**\n * Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:\n * account_id, VINEXT_CACHE KV namespace ID, and custom domain.\n */\nexport function parseWranglerConfig(root: string): WranglerConfig | null {\n // Try JSONC / JSON first\n for (const filename of [\"wrangler.jsonc\", \"wrangler.json\"]) {\n const filepath = path.join(root, filename);\n if (fs.existsSync(filepath)) {\n const content = fs.readFileSync(filepath, \"utf-8\");\n try {\n const json = JSON.parse(stripJsonComments(content));\n return extractFromJSON(json);\n } catch {\n continue;\n }\n }\n }\n\n // Try TOML\n const tomlPath = path.join(root, \"wrangler.toml\");\n if (fs.existsSync(tomlPath)) {\n const content = fs.readFileSync(tomlPath, \"utf-8\");\n return extractFromTOML(content);\n }\n\n return null;\n}\n\n/**\n * Strip single-line (//) and multi-line comments from JSONC while\n * preserving strings that contain slashes.\n */\nfunction stripJsonComments(str: string): string {\n let result = \"\";\n let inString = false;\n let inSingleLine = false;\n let inMultiLine = false;\n let escapeNext = false;\n\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n const next = str[i + 1];\n\n if (escapeNext) {\n if (!inSingleLine && !inMultiLine) result += ch;\n escapeNext = false;\n continue;\n }\n\n if (ch === \"\\\\\" && inString) {\n result += ch;\n escapeNext = true;\n continue;\n }\n\n if (inSingleLine) {\n if (ch === \"\\n\") {\n inSingleLine = false;\n result += ch;\n }\n continue;\n }\n\n if (inMultiLine) {\n if (ch === \"*\" && next === \"/\") {\n inMultiLine = false;\n i++;\n }\n continue;\n }\n\n if (ch === '\"' && !inString) {\n inString = true;\n result += ch;\n continue;\n }\n\n if (ch === '\"' && inString) {\n inString = false;\n result += ch;\n continue;\n }\n\n if (!inString && ch === \"/\" && next === \"/\") {\n inSingleLine = true;\n i++;\n continue;\n }\n\n if (!inString && ch === \"/\" && next === \"*\") {\n inMultiLine = true;\n i++;\n continue;\n }\n\n result += ch;\n }\n\n return result;\n}\n\nfunction extractFromJSON(config: Record<string, unknown>): WranglerConfig {\n const result: WranglerConfig = {};\n\n // account_id\n if (typeof config.account_id === \"string\") {\n result.accountId = config.account_id;\n }\n\n // KV namespace ID for VINEXT_CACHE\n if (Array.isArray(config.kv_namespaces)) {\n const vinextKV = config.kv_namespaces.find(\n (ns: Record<string, unknown>) =>\n ns && typeof ns === \"object\" && ns.binding === \"VINEXT_CACHE\",\n );\n if (vinextKV && typeof vinextKV.id === \"string\" && vinextKV.id !== \"<your-kv-namespace-id>\") {\n result.kvNamespaceId = vinextKV.id;\n }\n }\n\n // Custom domain — check routes[] and custom_domains[]\n const domain = extractDomainFromRoutes(config.routes) ?? extractDomainFromCustomDomains(config);\n if (domain) result.customDomain = domain;\n\n return result;\n}\n\nfunction extractDomainFromRoutes(routes: unknown): string | null {\n if (!Array.isArray(routes)) return null;\n\n for (const route of routes) {\n if (typeof route === \"string\") {\n const domain = cleanDomain(route);\n if (domain && !domain.includes(\"workers.dev\")) return domain;\n } else if (route && typeof route === \"object\") {\n const r = route as Record<string, unknown>;\n const pattern =\n typeof r.zone_name === \"string\"\n ? r.zone_name\n : typeof r.pattern === \"string\"\n ? r.pattern\n : null;\n if (pattern) {\n const domain = cleanDomain(pattern);\n if (domain && !domain.includes(\"workers.dev\")) return domain;\n }\n }\n }\n return null;\n}\n\nfunction extractDomainFromCustomDomains(config: Record<string, unknown>): string | null {\n // Workers Custom Domains: \"custom_domains\": [\"example.com\"]\n if (Array.isArray(config.custom_domains)) {\n for (const d of config.custom_domains) {\n if (typeof d === \"string\" && !d.includes(\"workers.dev\")) {\n return cleanDomain(d);\n }\n }\n }\n return null;\n}\n\n/** Strip protocol and trailing wildcards from a route pattern to get a bare domain. */\nfunction cleanDomain(raw: string): string | null {\n const cleaned = raw\n .replace(/^https?:\\/\\//, \"\")\n .replace(/\\/\\*$/, \"\")\n .replace(/\\/+$/, \"\")\n .split(\"/\")[0]; // Take only the host part\n return cleaned || null;\n}\n\n/**\n * Simple extraction of specific fields from wrangler.toml content.\n * Not a full TOML parser — just enough for the fields we need.\n */\nfunction extractFromTOML(content: string): WranglerConfig {\n const result: WranglerConfig = {};\n\n // account_id = \"...\"\n const accountMatch = content.match(/^account_id\\s*=\\s*\"([^\"]+)\"/m);\n if (accountMatch) result.accountId = accountMatch[1];\n\n // KV namespace with binding = \"VINEXT_CACHE\"\n // Look for [[kv_namespaces]] blocks\n const kvBlocks = content.split(/\\[\\[kv_namespaces\\]\\]/);\n for (let i = 1; i < kvBlocks.length; i++) {\n const block = kvBlocks[i].split(/\\[\\[/)[0]; // Take until next section\n const bindingMatch = block.match(/binding\\s*=\\s*\"([^\"]+)\"/);\n const idMatch = block.match(/\\bid\\s*=\\s*\"([^\"]+)\"/);\n if (\n bindingMatch?.[1] === \"VINEXT_CACHE\" &&\n idMatch?.[1] &&\n idMatch[1] !== \"<your-kv-namespace-id>\"\n ) {\n result.kvNamespaceId = idMatch[1];\n }\n }\n\n // routes — both string and table forms\n // route = \"example.com/*\"\n const routeMatch = content.match(/^route\\s*=\\s*\"([^\"]+)\"/m);\n if (routeMatch) {\n const domain = cleanDomain(routeMatch[1]);\n if (domain && !domain.includes(\"workers.dev\")) {\n result.customDomain = domain;\n }\n }\n\n // [[routes]] blocks\n if (!result.customDomain) {\n const routeBlocks = content.split(/\\[\\[routes\\]\\]/);\n for (let i = 1; i < routeBlocks.length; i++) {\n const block = routeBlocks[i].split(/\\[\\[/)[0];\n const patternMatch = block.match(/pattern\\s*=\\s*\"([^\"]+)\"/);\n if (patternMatch) {\n const domain = cleanDomain(patternMatch[1]);\n if (domain && !domain.includes(\"workers.dev\")) {\n result.customDomain = domain;\n break;\n }\n }\n }\n }\n\n return result;\n}\n\n// ─── Cloudflare API ──────────────────────────────────────────────────────────\n\n/**\n * Generate zone lookup candidates from shortest (2-part) to longest.\n * Tries the most common case first (e.g., \"example.com\") and progressively\n * adds labels for multi-part TLDs (e.g., \"co.uk\" → \"example.co.uk\").\n *\n * \"shop.example.com\" → [\"example.com\", \"shop.example.com\"]\n * \"shop.example.co.uk\" → [\"co.uk\", \"example.co.uk\", \"shop.example.co.uk\"]\n * \"example.com\" → [\"example.com\"]\n */\nexport function domainCandidates(domain: string): string[] {\n const parts = domain.split(\".\");\n const candidates: string[] = [];\n for (let i = parts.length - 2; i >= 0; i--) {\n candidates.push(parts.slice(i).join(\".\"));\n }\n return candidates;\n}\n\n/** Resolve zone ID from a domain name via the Cloudflare API. */\nasync function resolveZoneId(domain: string, apiToken: string): Promise<string | null> {\n // Try progressively longer domain candidates until one matches a zone.\n // This handles all public suffixes without a hardcoded TLD list —\n // for simple TLDs (.com, .io) the 2-part candidate hits on the first try;\n // for multi-part TLDs (.co.uk, .com.au) it takes one extra call.\n for (const candidate of domainCandidates(domain)) {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(candidate)}`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n \"Content-Type\": \"application/json\",\n },\n },\n );\n\n if (!response.ok) continue;\n\n const data = (await response.json()) as {\n success: boolean;\n result?: Array<{ id: string }>;\n };\n if (data.success && data.result?.length) {\n return data.result[0].id;\n }\n }\n\n return null;\n}\n\n/** Resolve the account ID associated with the API token. */\nasync function resolveAccountId(apiToken: string): Promise<string | null> {\n const response = await fetch(\"https://api.cloudflare.com/client/v4/accounts?per_page=1\", {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) return null;\n\n const data = (await response.json()) as {\n success: boolean;\n result?: Array<{ id: string }>;\n };\n if (!data.success || !data.result?.length) return null;\n\n return data.result[0].id;\n}\n\n// ─── Traffic Querying ────────────────────────────────────────────────────────\n\n/**\n * Query Cloudflare zone analytics for top page paths by request count\n * over the given time window.\n */\nasync function queryTraffic(\n zoneTag: string,\n apiToken: string,\n windowHours: number,\n): Promise<TrafficEntry[]> {\n const now = new Date();\n const start = new Date(now.getTime() - windowHours * 60 * 60 * 1000);\n\n const query = `{\n viewer {\n zones(filter: { zoneTag: \"${zoneTag}\" }) {\n httpRequestsAdaptiveGroups(\n limit: 10000\n orderBy: [sum_requests_DESC]\n filter: {\n datetime_geq: \"${start.toISOString()}\"\n datetime_lt: \"${now.toISOString()}\"\n requestSource: \"eyeball\"\n }\n ) {\n sum { requests }\n dimensions { clientRequestPath }\n }\n }\n }\n }`;\n\n const response = await fetch(\"https://api.cloudflare.com/client/v4/graphql\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ query }),\n });\n\n if (!response.ok) {\n throw new Error(`Zone analytics query failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as {\n errors?: Array<{ message: string }>;\n data?: {\n viewer?: {\n zones?: Array<{\n httpRequestsAdaptiveGroups?: Array<{\n sum: { requests: number };\n dimensions: { clientRequestPath: string };\n }>;\n }>;\n };\n };\n };\n\n if (data.errors?.length) {\n throw new Error(`Zone analytics error: ${data.errors[0].message}`);\n }\n\n const groups = data.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups;\n if (!groups || groups.length === 0) return [];\n\n return filterTrafficPaths(\n groups.map((g) => ({\n path: g.dimensions.clientRequestPath,\n requests: g.sum.requests,\n })),\n );\n}\n\n/** Filter out non-page requests (static assets, API routes, internal routes). */\nfunction filterTrafficPaths(entries: TrafficEntry[]): TrafficEntry[] {\n return entries.filter((e) => {\n if (!e.path.startsWith(\"/\")) return false;\n // Static assets\n if (/\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map|webp|avif)$/i.test(e.path))\n return false;\n // API routes\n if (e.path.startsWith(\"/api/\")) return false;\n // Internal routes\n if (e.path.startsWith(\"/_vinext/\") || e.path.startsWith(\"/_next/\")) return false;\n // RSC requests\n if (e.path.endsWith(\".rsc\")) return false;\n return true;\n });\n}\n\n// ─── Route Selection ─────────────────────────────────────────────────────────\n\n/**\n * Walk the ranked traffic list, accumulating request counts until the\n * coverage target is met or the hard cap is reached.\n */\nfunction selectRoutes(\n traffic: TrafficEntry[],\n coverageTarget: number,\n limit: number,\n): SelectedRoutes {\n const totalRequests = traffic.reduce((sum, e) => sum + e.requests, 0);\n if (totalRequests === 0) {\n return { routes: [], totalRequests: 0, coveredRequests: 0, coveragePercent: 0 };\n }\n\n const target = totalRequests * (coverageTarget / 100);\n const selected: TrafficEntry[] = [];\n let accumulated = 0;\n\n // Traffic is already sorted DESC by requests from the GraphQL query\n for (const entry of traffic) {\n if (accumulated >= target || selected.length >= limit) break;\n selected.push(entry);\n accumulated += entry.requests;\n }\n\n return {\n routes: selected,\n totalRequests,\n coveredRequests: accumulated,\n coveragePercent: (accumulated / totalRequests) * 100,\n };\n}\n\n// ─── Pre-rendering ───────────────────────────────────────────────────────────\n\n/** Pre-render port — high number to avoid collisions with dev servers. */\nconst PRERENDER_PORT = 19384;\n\n/** Max time to wait for the local server to start (ms). */\nconst SERVER_STARTUP_TIMEOUT = 30_000;\n\n/** Max concurrent fetch requests during pre-rendering. */\nconst FETCH_CONCURRENCY = 10;\n\n/**\n * Start a local production server, fetch each route to produce HTML,\n * and return the results. Pages that fail to render are skipped.\n */\nasync function prerenderRoutes(\n routes: string[],\n root: string,\n hostDomain?: string,\n): Promise<Map<string, PrerenderResult>> {\n const results = new Map<string, PrerenderResult>();\n let failedCount = 0;\n const port = PRERENDER_PORT;\n\n // Verify dist/ exists\n const distDir = path.join(root, \"dist\");\n if (!fs.existsSync(distDir)) {\n console.log(\" TPR: Skipping pre-render — dist/ directory not found\");\n return results;\n }\n\n // Start the local production server as a subprocess\n const serverProcess = startLocalServer(root, port);\n\n try {\n await waitForServer(port, SERVER_STARTUP_TIMEOUT);\n\n // Fetch routes in batches to limit concurrency\n for (let i = 0; i < routes.length; i += FETCH_CONCURRENCY) {\n const batch = routes.slice(i, i + FETCH_CONCURRENCY);\n const promises = batch.map(async (routePath) => {\n try {\n const response = await fetch(`http://127.0.0.1:${port}${routePath}`, {\n headers: {\n \"User-Agent\": \"vinext-tpr/1.0\",\n ...(hostDomain ? { Host: hostDomain } : {}),\n },\n redirect: \"manual\", // Don't follow redirects — cache the redirect itself\n });\n\n // Only cache successful responses (2xx and 3xx)\n if (response.status < 400) {\n const html = await response.text();\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n // Only keep relevant headers\n if (\n key === \"content-type\" ||\n key === \"cache-control\" ||\n key === VINEXT_REVALIDATE_HEADER ||\n key === \"location\"\n ) {\n headers[key] = value;\n }\n });\n results.set(routePath, {\n html,\n status: response.status,\n headers,\n });\n }\n } catch {\n // Skip pages that fail to render — they may depend on\n // request-specific data (cookies, headers, auth) that\n // isn't available during pre-rendering.\n failedCount++;\n }\n });\n\n await Promise.all(promises);\n }\n\n if (failedCount > 0) {\n console.log(` TPR: ${failedCount} page(s) failed to pre-render (skipped)`);\n }\n } finally {\n serverProcess.kill(\"SIGTERM\");\n // Give it a moment to clean up\n await new Promise<void>((resolve) => {\n serverProcess.on(\"exit\", resolve);\n setTimeout(resolve, 2000);\n });\n }\n\n return results;\n}\n\n/**\n * Spawn a subprocess running the vinext production server.\n * Uses the same Node.js binary and resolves prod-server.js relative\n * to the current module (works whether vinext is installed or linked).\n */\nfunction startLocalServer(root: string, port: number): ChildProcess {\n const prodServerPath = path.resolve(import.meta.dirname, \"..\", \"server\", \"prod-server.js\");\n const outDir = path.join(root, \"dist\");\n\n // Escape backslashes for Windows paths inside the JS string\n const escapedProdServer = prodServerPath.replace(/\\\\/g, \"\\\\\\\\\");\n const escapedOutDir = outDir.replace(/\\\\/g, \"\\\\\\\\\");\n\n const script = [\n `import(\"file://${escapedProdServer}\")`,\n `.then(m => m.startProdServer({ port: ${port}, host: \"127.0.0.1\", outDir: \"${escapedOutDir}\" }))`,\n `.catch(e => { console.error(\"[vinext-tpr] Server failed to start:\", e); process.exit(1); });`,\n ].join(\"\");\n\n const proc = spawn(process.execPath, [\"--input-type=module\", \"-e\", script], {\n cwd: root,\n stdio: \"pipe\",\n env: { ...process.env, NODE_ENV: \"production\" },\n });\n\n // Forward server errors to the parent's stderr for debugging\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n const msg = chunk.toString().trim();\n if (msg) console.error(` [tpr-server] ${msg}`);\n });\n\n return proc;\n}\n\n/** Poll the local server until it responds or the timeout is reached. */\nasync function waitForServer(port: number, timeoutMs: number): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < timeoutMs) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 2000);\n const response = await fetch(`http://127.0.0.1:${port}/`, {\n redirect: \"manual\",\n signal: controller.signal,\n });\n clearTimeout(timer);\n // Any response means the server is up\n await response.text(); // consume body\n return;\n } catch {\n await new Promise<void>((r) => setTimeout(r, 300));\n }\n }\n throw new Error(`Local production server failed to start within ${timeoutMs / 1000}s`);\n}\n\n// ─── KV Upload ───────────────────────────────────────────────────────────────\n\n/** KV bulk API accepts up to 10,000 pairs per request */\nconst KV_BATCH_SIZE = 10_000;\n\n/** Maximum KV expiration TTL: 30 days */\nconst MAX_KV_TTL_SECONDS = 30 * 24 * 3600;\n\n/**\n * Build KV bulk API pairs from pre-rendered entries.\n *\n * Key format matches the runtime KVCacheHandler exactly:\n * ENTRY_PREFIX + isrCacheKey(\"app\", pathname, buildId) + \":html\"\n * → \"cache:app:<buildId>:<pathname>:html\"\n */\nexport function buildTprKVPairs(\n entries: Map<string, PrerenderResult>,\n buildId: string | undefined,\n defaultRevalidateSeconds: number,\n): Array<{ key: string; value: string; expiration_ttl: number }> {\n const now = Date.now();\n const pairs: Array<{ key: string; value: string; expiration_ttl: number }> = [];\n\n for (const [routePath, result] of entries) {\n const revalidateHeader = result.headers[VINEXT_REVALIDATE_HEADER];\n const revalidateSeconds =\n revalidateHeader && !isNaN(Number(revalidateHeader))\n ? Number(revalidateHeader)\n : defaultRevalidateSeconds;\n\n const revalidateAt = revalidateSeconds > 0 ? now + revalidateSeconds * 1000 : null;\n\n // For revalidating entries: 30-day TTL matches runtime KVCacheHandler.set().\n // For non-revalidating entries: runtime uses no TTL (entries persist indefinitely),\n // but TPR uses a 24h fallback so pre-warmed entries don't accumulate forever.\n const kvTtl = revalidateSeconds > 0 ? MAX_KV_TTL_SECONDS : 24 * 3600;\n\n const entry = {\n value: {\n kind: \"APP_PAGE\" as const,\n html: result.html,\n headers: result.headers,\n status: result.status,\n },\n tags: [] as string[],\n lastModified: now,\n revalidateAt,\n };\n\n const cacheKey = ENTRY_PREFIX + isrCacheKey(\"app\", routePath, buildId) + \":html\";\n\n pairs.push({\n key: cacheKey,\n value: JSON.stringify(entry),\n expiration_ttl: kvTtl,\n });\n }\n\n return pairs;\n}\n\n/**\n * Upload pre-rendered pages to KV using the Cloudflare REST API.\n * Writes in the same KVCacheEntry format that KVCacheHandler reads\n * at runtime, so ISR serves these entries without any code changes.\n */\nasync function uploadToKV(\n entries: Map<string, PrerenderResult>,\n namespaceId: string,\n accountId: string,\n apiToken: string,\n defaultRevalidateSeconds: number,\n buildId?: string,\n): Promise<void> {\n const pairs = buildTprKVPairs(entries, buildId, defaultRevalidateSeconds);\n for (let i = 0; i < pairs.length; i += KV_BATCH_SIZE) {\n const batch = pairs.slice(i, i + KV_BATCH_SIZE);\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/bulk`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${apiToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(batch),\n },\n );\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(\n `KV bulk upload failed (batch ${Math.floor(i / KV_BATCH_SIZE) + 1}): ${response.status} — ${text}`,\n );\n }\n }\n}\n\n// ─── Main Entry ──────────────────────────────────────────────────────────────\n\n/** Default revalidation TTL for pre-rendered pages (1 hour). */\nconst DEFAULT_REVALIDATE_SECONDS = 3600;\n\n/**\n * Run the TPR pipeline: query traffic, select routes, pre-render, upload.\n *\n * Designed to be called between the build step and wrangler deploy in\n * the `vinext deploy` pipeline. Gracefully skips (never errors) when\n * the prerequisites aren't met.\n */\nexport async function runTPR(options: TPROptions): Promise<TPRResult> {\n const startTime = Date.now();\n const { root, coverage, limit, window: windowHours } = options;\n\n const skip = (reason: string): TPRResult => ({\n totalPaths: 0,\n prerenderedCount: 0,\n coverageAchieved: 0,\n durationMs: Date.now() - startTime,\n skipped: reason,\n });\n\n // ── 1. Check for API token ────────────────────────────────────\n const apiToken = process.env.CLOUDFLARE_API_TOKEN;\n if (!apiToken) {\n return skip(\"no CLOUDFLARE_API_TOKEN set\");\n }\n\n // ── 2. Parse wrangler config ──────────────────────────────────\n const wranglerConfig = parseWranglerConfig(root);\n if (!wranglerConfig) {\n return skip(\"could not parse wrangler config\");\n }\n\n // ── 3. Check for custom domain ────────────────────────────────\n if (!wranglerConfig.customDomain) {\n return skip(\"no custom domain — zone analytics unavailable\");\n }\n\n // ── 4. Check for KV namespace ─────────────────────────────────\n if (!wranglerConfig.kvNamespaceId) {\n return skip(\"no VINEXT_CACHE KV namespace configured\");\n }\n\n // ── 5. Resolve account ID ─────────────────────────────────────\n const accountId = wranglerConfig.accountId ?? (await resolveAccountId(apiToken));\n if (!accountId) {\n return skip(\"could not resolve Cloudflare account ID\");\n }\n\n // ── 6. Resolve zone ID ────────────────────────────────────────\n console.log(` TPR: Analyzing traffic for ${wranglerConfig.customDomain} (last ${windowHours}h)`);\n\n const zoneId = await resolveZoneId(wranglerConfig.customDomain, apiToken);\n if (!zoneId) {\n return skip(`could not resolve zone for ${wranglerConfig.customDomain}`);\n }\n\n // ── 7. Query traffic data ─────────────────────────────────────\n let traffic: TrafficEntry[];\n try {\n traffic = await queryTraffic(zoneId, apiToken, windowHours);\n } catch (err) {\n return skip(`analytics query failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n if (traffic.length === 0) {\n return skip(\"no traffic data available (first deploy?)\");\n }\n\n // ── 8. Select routes by coverage ──────────────────────────────\n const selection = selectRoutes(traffic, coverage, limit);\n\n console.log(\n ` TPR: ${traffic.length.toLocaleString()} unique paths — ` +\n `${selection.routes.length} pages cover ${Math.round(selection.coveragePercent)}% of traffic`,\n );\n\n if (selection.routes.length === 0) {\n return {\n totalPaths: traffic.length,\n prerenderedCount: 0,\n coverageAchieved: 0,\n durationMs: Date.now() - startTime,\n skipped: \"no pre-renderable routes after filtering\",\n };\n }\n\n // ── 9. Pre-render selected routes ─────────────────────────────\n console.log(` TPR: Pre-rendering ${selection.routes.length} pages...`);\n\n const routePaths = selection.routes.map((r) => r.path);\n let rendered: Map<string, PrerenderResult>;\n try {\n rendered = await prerenderRoutes(routePaths, root, wranglerConfig.customDomain);\n } catch (err) {\n return skip(`pre-rendering failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n if (rendered.size === 0) {\n return {\n totalPaths: traffic.length,\n prerenderedCount: 0,\n coverageAchieved: selection.coveragePercent,\n durationMs: Date.now() - startTime,\n skipped: \"all pages failed to pre-render (request-dependent?)\",\n };\n }\n\n // ── 10. Upload to KV ──────────────────────────────────────────\n // Read buildId from the BUILD_ID file written by vinext:build-id plugin.\n let buildId: string;\n try {\n buildId = fs.readFileSync(path.join(root, \"dist\", \"server\", \"BUILD_ID\"), \"utf-8\").trim();\n } catch {\n // BUILD_ID is written by vinext:build-id during every production build.\n // If missing, the build output is likely corrupted or incomplete.\n // Proceeding without buildId would write keys that never match runtime.\n console.warn(\n \" TPR: Could not read BUILD_ID from dist/server/ — KV keys will not match runtime. Skipping KV upload.\",\n );\n return skip(\"BUILD_ID not found in dist/server/ — build output may be incomplete\");\n }\n\n try {\n await uploadToKV(\n rendered,\n wranglerConfig.kvNamespaceId,\n accountId,\n apiToken,\n DEFAULT_REVALIDATE_SECONDS,\n buildId,\n );\n } catch (err) {\n return skip(`KV upload failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n const durationMs = Date.now() - startTime;\n console.log(\n ` TPR: Pre-rendered ${rendered.size} pages in ${(durationMs / 1000).toFixed(1)}s → KV cache`,\n );\n\n return {\n totalPaths: traffic.length,\n prerenderedCount: rendered.size,\n coverageAchieved: selection.coveragePercent,\n durationMs,\n };\n}\n"],"mappings":"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"}