import { readFile } from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; import { pathToFileURL } from 'node:url'; import { extractEntryAssetPath as extractEntryAssetPathFromHtml } from './verify-public-build.shared.mjs'; const DEFAULT_INDEX_PATH = '/index.html'; const DEFAULT_VERSION_PATH = '/__build_version__.json'; function normalizeBaseUrl(value) { const text = String(value || '').trim(); if (!text) { return ''; } return text.endsWith('/') ? text.slice(0, -1) : text; } async function fetchText(url) { const response = await fetch(url, { cache: 'no-store', headers: { 'cache-control': 'no-cache' }, }); if (!response.ok) { throw new Error(`Request failed: ${url} -> ${response.status}`); } return response.text(); } function extractBuildVersion(text) { if (!text) { return ''; } try { const parsed = JSON.parse(text); return String(parsed?.version || '').trim(); } catch { return ''; } } async function readLocalVersion(distDir) { const versionPath = path.join(distDir, '__build_version__.json'); return extractBuildVersion(await readFile(versionPath, 'utf8')); } async function readLocalEntry(distDir) { const indexHtmlPath = path.join(distDir, 'index.html'); return extractEntryAssetPathFromHtml(await readFile(indexHtmlPath, 'utf8')); } export async function verifyPublicBuild({ baseUrl = process.env.PUBLIC_APP_URL, distDir = path.resolve(process.cwd(), 'dist'), } = {}) { const normalizedBaseUrl = normalizeBaseUrl(baseUrl); if (!normalizedBaseUrl) { throw new Error('PUBLIC_APP_URL is required for verifyPublicBuild.'); } const localEntryAssetPath = await readLocalEntry(distDir); const localBuildVersion = await readLocalVersion(distDir); const remoteIndexHtml = await fetchText(`${normalizedBaseUrl}${DEFAULT_INDEX_PATH}`); const remoteVersionText = await fetchText(`${normalizedBaseUrl}${DEFAULT_VERSION_PATH}`); const remoteEntryAssetPath = extractEntryAssetPathFromHtml(remoteIndexHtml); const remoteBuildVersion = extractBuildVersion(remoteVersionText); if (!localEntryAssetPath || !remoteEntryAssetPath) { throw new Error('Unable to extract entry asset path from local or remote index.html.'); } if (localEntryAssetPath !== remoteEntryAssetPath || localBuildVersion !== remoteBuildVersion) { throw new Error( [ 'Public build does not match local dist.', `localEntry=${localEntryAssetPath}`, `remoteEntry=${remoteEntryAssetPath}`, `localVersion=${localBuildVersion || '--'}`, `remoteVersion=${remoteBuildVersion || '--'}`, ].join(' '), ); } return { baseUrl: normalizedBaseUrl, localEntryAssetPath, remoteEntryAssetPath, localBuildVersion, remoteBuildVersion, }; } async function main() { const result = await verifyPublicBuild({ baseUrl: process.argv[2] || process.env.PUBLIC_APP_URL, distDir: process.argv[3] ? path.resolve(process.cwd(), process.argv[3]) : path.resolve(process.cwd(), 'dist'), }); process.stdout.write( `Verified public build ${result.remoteBuildVersion} at ${result.baseUrl} using ${result.remoteEntryAssetPath}\n`, ); } const isDirectExecution = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url : false; if (isDirectExecution) { main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); }