feat: prerender crawlable portfolio artifacts

This commit is contained in:
2026-09-02 02:10:26 +03:00
parent 967a3d4d6c
commit 3dd4a818d0
13 changed files with 1167 additions and 12 deletions

21
scripts/prerender.d.mts Normal file
View File

@@ -0,0 +1,21 @@
import type { ProductionInputs } from '../src/content/types.ts';
export interface ViteManifestEntry {
file: string;
src?: string;
isEntry?: boolean;
css?: string[];
imports?: string[];
}
export type ViteManifest = Record<string, ViteManifestEntry>;
export function parseExactConfigArgument(arguments_: string[]): string;
export function assertExpectedDistInputs(distRoot: string): Promise<void>;
export function assemblePrerenderedHtml(
template: string,
manifest: ViteManifest,
markup: string,
inputs: Readonly<ProductionInputs>,
): string;
export function prerender(configPath: string): Promise<void>;

199
scripts/prerender.mjs Normal file
View File

@@ -0,0 +1,199 @@
import { cp, lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { loadProductionInputs } from '../src/content/load-production-inputs.ts';
import { createPageMetadata, createStaticHostArtifacts } from '../src/content/metadata.ts';
import { serializeJsonLd } from '../src/security/serialize-json-ld.ts';
const scriptPath = fileURLToPath(import.meta.url);
const repositoryRoot = resolve(dirname(scriptPath), '..');
const escapeHtml = (value) => value
.replace(/&/gu, '&amp;')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;')
.replace(/"/gu, '&quot;');
const exactlyOne = (source, expression, label) => {
const matches = source.match(expression) ?? [];
if (matches.length !== 1) throw new Error(`Expected exactly one ${label}.`);
};
export const parseExactConfigArgument = (arguments_) => {
if (arguments_.length !== 2 || arguments_[0] !== '--config') {
throw new Error('Prerender accepts exactly --config <path>.');
}
const configPath = arguments_[1];
if (
configPath.length === 0 ||
configPath.startsWith('/') ||
configPath.includes('\\') ||
configPath.split('/').some((part) => part === '' || part === '.' || part === '..') ||
!/^config\/[a-z0-9][a-z0-9._-]*\.json$/u.test(configPath)
) {
throw new Error('Config must be a safe repository-relative JSON path inside config/.');
}
return configPath;
};
const manifestEntry = (manifest) => {
const entries = Object.values(manifest).filter((entry) => entry.isEntry === true);
if (entries.length !== 1) throw new Error('Vite manifest must contain exactly one client entry.');
const entry = entries[0];
if (typeof entry.file !== 'string' || !/^assets\/[a-zA-Z0-9._-]+\.js$/u.test(entry.file)) {
throw new Error('Vite manifest client entry is unsafe.');
}
return entry;
};
const metadataMarkup = (metadata) => {
const image = metadata.openGraph.image === undefined
? ''
: `\n <meta property="og:image" content="${escapeHtml(metadata.openGraph.image)}" />`;
return [
` <meta name="description" content="${escapeHtml(metadata.description)}" />`,
` <meta name="robots" content="${escapeHtml(metadata.robots)}" />`,
` <link rel="canonical" href="${escapeHtml(metadata.canonicalUrl)}" />`,
` <meta property="og:type" content="${metadata.openGraph.type}" />`,
` <meta property="og:title" content="${escapeHtml(metadata.openGraph.title)}" />`,
` <meta property="og:description" content="${escapeHtml(metadata.openGraph.description)}" />`,
` <meta property="og:url" content="${escapeHtml(metadata.openGraph.url)}" />${image}`,
` <script type="application/ld+json">${metadata.serializedJsonLd}</script>`,
].join('\n');
};
export const assemblePrerenderedHtml = (template, manifest, markup, inputs) => {
const entry = manifestEntry(manifest);
const entryUrl = `/${entry.file}`;
exactlyOne(template, /<title>[^<]*<\/title>/gu, 'title');
exactlyOne(template, /<div id="app"><\/div>/gu, 'empty app root');
exactlyOne(template, /<script type="module"[^>]*><\/script>/gu, 'client module script');
exactlyOne(template, /<!--portfolio-head-->/gu, 'portfolio head marker');
exactlyOne(template, /<!--portfolio-body-->/gu, 'portfolio body marker');
const metadata = createPageMetadata(inputs.content, inputs.manifest, inputs.site);
const snapshot = serializeJsonLd(inputs);
const preloads = [entry.file, ...(entry.imports ?? []).map((key) => manifest[key]?.file)]
.filter((file, index, files) => typeof file === 'string' && files.indexOf(file) === index)
.map((file) => ` <link rel="modulepreload" href="/${file}" />`)
.join('\n');
const withoutModule = template.replace(/\s*<script type="module"[^>]*><\/script>/u, '');
const withHead = withoutModule
.replace(/<title>[^<]*<\/title>/u, `<title>${escapeHtml(metadata.title)}</title>`)
.replace('<!--portfolio-head-->', `${preloads}\n${metadataMarkup(metadata)}`)
.replace('<div id="app"></div>', `<div id="app">${markup}</div>`);
const clientBootstrap = [
`<script id="portfolio-inputs" type="application/json">${snapshot}</script>`,
`<script type="module" crossorigin src="${entryUrl}"></script>`,
].join('\n ');
return withHead.replace('<!--portfolio-body-->', clientBootstrap);
};
const assertDirectoryMembers = async (root, expected, label) => {
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
if (!expected.has(entry.name)) throw new Error(`Unexpected build output in ${label}: ${entry.name}`);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked build output in ${label}: ${entry.name}`);
}
for (const name of expected) {
if (!entries.some((entry) => entry.name === name)) throw new Error(`Missing build output in ${label}: ${name}`);
}
return entries;
};
const manifestAssetFiles = (manifest) => {
const files = new Set();
for (const entry of Object.values(manifest)) {
for (const file of [entry.file, ...(entry.css ?? []), ...(entry.assets ?? [])]) {
if (typeof file !== 'string' || !/^assets\/[a-zA-Z0-9._-]+$/u.test(file)) {
throw new Error('Vite manifest contains an unsafe output path.');
}
files.add(file.slice('assets/'.length));
}
}
return files;
};
export const assertExpectedDistInputs = async (distRoot) => {
const allowed = new Set([
'client', 'assets', 'index.html', 'robots.txt', 'sitemap.xml', '_headers.example', '_redirects.example',
]);
const rootEntries = await readdir(distRoot, { withFileTypes: true });
for (const entry of rootEntries) {
if (!allowed.has(entry.name)) throw new Error(`Unexpected build output: ${entry.name}`);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked build output: ${entry.name}`);
}
const clientStats = await lstat(resolve(distRoot, 'client'));
if (clientStats.isSymbolicLink() || !clientStats.isDirectory()) {
throw new Error('Client output must be a non-symlink directory.');
}
await assertDirectoryMembers(resolve(distRoot, 'client'), new Set(['.vite', 'assets', 'index.html']), 'client');
await assertDirectoryMembers(resolve(distRoot, 'client/.vite'), new Set(['manifest.json']), 'client/.vite');
const manifest = JSON.parse(await readFile(resolve(distRoot, 'client/.vite/manifest.json'), 'utf8'));
await assertDirectoryMembers(resolve(distRoot, 'client/assets'), manifestAssetFiles(manifest), 'client/assets');
if (rootEntries.some((entry) => entry.name === 'assets')) {
if (!rootEntries.some((entry) => entry.name === 'index.html')) {
throw new Error('Existing root assets require their generated index.html inventory.');
}
const existingHtml = await readFile(resolve(distRoot, 'index.html'), 'utf8');
const referencedAssets = new Set(
[...existingHtml.matchAll(/(?:src|href)="\/assets\/([a-zA-Z0-9._-]+)"/gu)].map((match) => match[1]),
);
await assertDirectoryMembers(resolve(distRoot, 'assets'), referencedAssets, 'assets');
}
};
const assertInsideRepository = (path) => {
const pathRelativeToRoot = relative(repositoryRoot, path);
if (pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) {
throw new Error('Build input escaped the repository root.');
}
};
export const prerender = async (configPath) => {
const distRoot = resolve(repositoryRoot, 'dist');
const clientRoot = resolve(distRoot, 'client');
const configAbsolutePath = resolve(repositoryRoot, configPath);
assertInsideRepository(configAbsolutePath);
await assertExpectedDistInputs(distRoot);
const inputs = await loadProductionInputs({
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
assetManifestJson: pathToFileURL(resolve(repositoryRoot, 'content/public/asset-manifest.json')),
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}${sep}`),
siteConfigJson: pathToFileURL(configAbsolutePath),
});
const [{ render }, template, manifestSource] = await Promise.all([
import(pathToFileURL(resolve(repositoryRoot, 'dist-ssr/entry-server.js')).href),
readFile(resolve(clientRoot, 'index.html'), 'utf8'),
readFile(resolve(clientRoot, '.vite/manifest.json'), 'utf8'),
]);
if (typeof render !== 'function') throw new Error('SSR entry does not export render().');
const manifest = JSON.parse(manifestSource);
const markup = await render('/', inputs);
const html = assemblePrerenderedHtml(template, manifest, markup, inputs);
const metadata = createPageMetadata(inputs.content, inputs.manifest, inputs.site);
const artifacts = createStaticHostArtifacts(metadata, inputs.site);
for (const path of ['assets', 'index.html', 'robots.txt', 'sitemap.xml', '_headers.example', '_redirects.example']) {
await rm(resolve(distRoot, path), { recursive: true, force: true });
}
await cp(resolve(clientRoot, 'assets'), resolve(distRoot, 'assets'), { recursive: true, errorOnExist: true });
await Promise.all([
writeFile(resolve(distRoot, 'index.html'), html),
writeFile(resolve(distRoot, 'robots.txt'), artifacts.robots),
writeFile(resolve(distRoot, '_headers.example'), artifacts.headers),
writeFile(resolve(distRoot, '_redirects.example'), artifacts.redirects),
...(artifacts.sitemap === undefined
? []
: [writeFile(resolve(distRoot, 'sitemap.xml'), artifacts.sitemap)]),
]);
await rm(clientRoot, { recursive: true });
};
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {
const configPath = parseExactConfigArgument(process.argv.slice(2));
await prerender(configPath);
}

View File

@@ -0,0 +1,7 @@
import type { ProductionInputs } from '../src/content/types.ts';
export function verifyArtifactDirectory(
root: string,
inputs: Readonly<ProductionInputs>,
): Promise<void>;
export function verifyArtifacts(configPath: string): Promise<void>;

View File

@@ -0,0 +1,293 @@
import { createHash } from 'node:crypto';
import { lstat, readFile, readdir } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { loadProductionInputs } from '../src/content/load-production-inputs.ts';
import { parseExactConfigArgument } from './prerender.mjs';
const scriptPath = fileURLToPath(import.meta.url);
const repositoryRoot = resolve(dirname(scriptPath), '..');
const canonicalSectionIds = [
'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact',
];
const prohibitedSurfaces = [
/__PORTFOLIO_INPUTS__/iu,
/(?:evidenceId|sourcePath|sourceFingerprint|approvalOwner|privacyClassification|denylist|originalFilename)/iu,
/sourceMappingURL/iu,
];
const escapeRegularExpression = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
const escapeHtml = (value) => value
.replace(/&/gu, '&amp;')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;')
.replace(/"/gu, '&quot;');
const readRequiredText = async (root, name) => {
try {
return await readFile(resolve(root, name), 'utf8');
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
throw new Error(`Missing required artifact: ${name}`);
}
throw error;
}
};
const listArtifacts = async (root) => {
const rootStats = await lstat(root);
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
throw new Error('Artifact root must be a non-symlink directory.');
}
const files = [];
for (const entry of await readdir(root, { withFileTypes: true })) {
const entryPath = resolve(root, entry.name);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked artifact: ${entry.name}`);
if (entry.isDirectory()) {
if (entry.name !== 'assets') throw new Error(`Unexpected artifact directory: ${entry.name}`);
for (const asset of await readdir(entryPath, { withFileTypes: true })) {
if (asset.isSymbolicLink() || !asset.isFile()) {
throw new Error(`Unexpected linked or nested artifact: assets/${asset.name}`);
}
files.push(`assets/${asset.name}`);
}
continue;
}
if (!entry.isFile()) throw new Error(`Unexpected special artifact: ${entry.name}`);
files.push(entry.name);
}
return files.sort();
};
const assertArtifactInventory = (files, html, inputs) => {
const expectedRootFiles = new Set(['index.html', 'robots.txt', '_headers.example', '_redirects.example']);
if (inputs.site.environment === 'production') expectedRootFiles.add('sitemap.xml');
const referencedAssets = new Set(
[...html.matchAll(/(?:src|href)="\/assets\/([a-zA-Z0-9._-]+)"/gu)].map((match) => `assets/${match[1]}`),
);
for (const asset of inputs.manifest.assets) referencedAssets.add(`assets/${asset.fileName}`);
for (const file of files) {
if (file.endsWith('.map')) throw new Error(`Source map artifact is prohibited: ${file}`);
if (file.startsWith('assets/')) {
if (!referencedAssets.has(file)) throw new Error(`Unexpected artifact: ${file}`);
} else if (!expectedRootFiles.has(file)) {
if (inputs.site.environment === 'preview' && file === 'sitemap.xml') {
throw new Error('Preview artifacts must not publish a sitemap.');
}
throw new Error(`Unexpected artifact: ${file}`);
}
}
for (const required of [...expectedRootFiles, ...referencedAssets]) {
if (!files.includes(required)) throw new Error(`Missing required artifact: ${required}`);
}
};
const assertNoProhibitedSurfaces = async (root, files) => {
for (const file of files) {
const bytes = await readFile(resolve(root, file));
const text = bytes.toString('utf8');
for (const pattern of prohibitedSurfaces) {
if (pattern.test(text)) throw new Error(`Prohibited surface detected in ${file}.`);
}
}
};
const exactlyOneMatch = (text, expression, label) => {
const matches = [...text.matchAll(expression)];
if (matches.length !== 1) throw new Error(`Expected exactly one ${label}.`);
return matches[0];
};
const assertSemanticHtml = (html, inputs) => {
if (!/<html lang="en">/u.test(html)) throw new Error('Final HTML must declare lang=en.');
const h1 = exactlyOneMatch(html, /<h1\b[^>]*>([\s\S]*?)<\/h1>/gu, 'H1');
if (h1[1] !== escapeHtml(inputs.content.identity.name)) throw new Error('H1 identity does not match public content.');
const adjacentIdentity = `<h1>${escapeHtml(inputs.content.identity.name)}</h1><p>${escapeHtml(inputs.content.identity.role)}</p>`;
if (!html.includes(adjacentIdentity)) throw new Error('The canonical role must be immediately adjacent to the H1.');
for (const id of canonicalSectionIds) {
const sectionMatches = html.match(new RegExp(`<section id="${escapeRegularExpression(id)}"(?:\\s|>)`, 'gu')) ?? [];
if (sectionMatches.length !== 1) throw new Error(`Expected exactly one section: ${id}`);
const navigationMatches = html.match(new RegExp(`href="#${escapeRegularExpression(id)}"`, 'gu')) ?? [];
if (navigationMatches.length !== 1) throw new Error(`Expected one native navigation link for section: ${id}`);
const label = inputs.content.navigation.find((item) => item.id === id)?.label;
if (label === undefined || !html.includes(`<h2 id="${id}-heading">${escapeHtml(label.toUpperCase())}</h2>`)) {
throw new Error(`Missing canonical section heading: ${id}`);
}
}
const requiredVisibleCopy = [
inputs.content.systemsOperations.title,
inputs.content.infrastructure.title,
...inputs.content.infrastructure.bands.flatMap((band) => [band.title, band.summary]),
...inputs.content.projects.selected.flatMap((project) => [project.title, project.summary]),
...inputs.content.toolkit.flatMap((group) => [group.title, ...group.technologies.map((technology) => technology.name)]),
inputs.content.contact.email,
];
for (const copy of requiredVisibleCopy) {
if (!html.includes(escapeHtml(copy))) throw new Error(`Missing required semantic evidence: ${copy}`);
}
if (!html.includes(`href="mailto:${escapeHtml(inputs.content.contact.email)}"`)) {
throw new Error('Missing visible mailto contact link.');
}
const moduleScript = exactlyOneMatch(html, /<script type="module"[^>]*src="\/assets\/[a-zA-Z0-9._-]+\.js"[^>]*><\/script>/gu, 'client module script');
const snapshot = exactlyOneMatch(
html,
/<script id="portfolio-inputs" type="application\/json">([\s\S]*?)<\/script>/gu,
'inert input snapshot',
);
let parsedSnapshot;
try {
parsedSnapshot = JSON.parse(snapshot[1]);
} catch {
throw new Error('The inert input snapshot is malformed.');
}
if (JSON.stringify(parsedSnapshot) !== JSON.stringify(inputs)) {
throw new Error('Hydration snapshot does not match the server input bytes and meaning.');
}
if (snapshot.index >= moduleScript.index || h1.index >= moduleScript.index) {
throw new Error('Semantic HTML and inert snapshot must precede the client module script.');
}
};
const assertMetadata = (html, inputs) => {
const expectedTitle = escapeHtml(inputs.content.metadata.title);
const title = exactlyOneMatch(html, /<title>([\s\S]*?)<\/title>/gu, 'page title');
if (title[1] !== expectedTitle) throw new Error('Page title does not match public content.');
const canonical = exactlyOneMatch(html, /<link rel="canonical" href="([^"]+)" \/>/gu, 'canonical link');
if (canonical[1] !== inputs.site.canonicalUrl) throw new Error('Canonical URL does not match site config.');
const description = exactlyOneMatch(html, /<meta name="description" content="([^"]+)" \/>/gu, 'meta description');
if (description[1] !== escapeHtml(inputs.content.metadata.description)) {
throw new Error('Meta description does not match public content.');
}
for (const property of ['og:type', 'og:title', 'og:description', 'og:url']) {
exactlyOneMatch(html, new RegExp(`<meta property="${property}" content="[^"]+" \\/>`, 'gu'), `${property} metadata`);
}
const ogImages = [...html.matchAll(/<meta property="og:image"/gu)];
if (inputs.content.metadata.socialPreviewAssetId === undefined && ogImages.length !== 0) {
throw new Error('Open Graph image must be omitted without an approved manifest asset.');
}
const jsonLdMatch = exactlyOneMatch(
html,
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gu,
'JSON-LD block',
);
let jsonLd;
try {
jsonLd = JSON.parse(jsonLdMatch[1]);
} catch {
throw new Error('JSON-LD is malformed.');
}
if (jsonLd['@type'] !== 'ProfilePage' || jsonLd.mainEntity?.['@type'] !== 'Person') {
throw new Error('JSON-LD must contain one ProfilePage with one nested Person.');
}
const person = jsonLd.mainEntity;
if (
person.name !== inputs.content.identity.name ||
person.jobTitle !== inputs.content.identity.role ||
person.url !== inputs.site.canonicalUrl ||
jsonLd.url !== inputs.site.canonicalUrl
) {
throw new Error('JSON-LD identity or canonical URL is inconsistent.');
}
const expectedSameAs = inputs.content.contact.social.map((link) => link.url);
if (JSON.stringify(person.sameAs ?? []) !== JSON.stringify(expectedSameAs)) {
throw new Error('JSON-LD sameAs contains an unverified or missing profile.');
}
if (jsonLdMatch[1].toLowerCase().includes('</script')) throw new Error('JSON-LD contains a script-closing hazard.');
return jsonLdMatch[1];
};
const assertSecurityHeaders = (headers, inputs, serializedJsonLd) => {
const required = [
"default-src 'self'", "script-src 'self'", "style-src 'self'", "img-src 'self'", "media-src 'self'",
"font-src 'self'", "connect-src 'none'", "object-src 'none'", "base-uri 'none'", "frame-ancestors 'none'",
"form-action 'none'", 'upgrade-insecure-requests', 'X-Content-Type-Options: nosniff',
'Referrer-Policy: no-referrer', 'X-Frame-Options: DENY',
'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
];
for (const value of required) {
if (!headers.includes(value)) throw new Error(`Missing security header policy: ${value}`);
}
const expectedHash = `sha256-${createHash('sha256').update(serializedJsonLd).digest('base64')}`;
const hashes = headers.match(/sha256-[A-Za-z0-9+/=]+/gu) ?? [];
if (hashes.length !== 1 || hashes[0] !== expectedHash) {
throw new Error('CSP hash does not match the actual JSON-LD bytes.');
}
if (inputs.site.environment === 'preview') {
if (!headers.includes('Cache-Control: private, no-store') ||
!headers.includes('X-Robots-Tag: noindex, nofollow, noarchive')) {
throw new Error('Preview headers must be private and noindex.');
}
if (headers.includes('Strict-Transport-Security')) throw new Error('Preview headers must omit HSTS.');
} else {
if (!headers.includes('Strict-Transport-Security: max-age=31536000')) {
throw new Error('Production headers must include HSTS.');
}
if (headers.includes('X-Robots-Tag') || headers.includes('private, no-store')) {
throw new Error('Production headers must omit preview directives.');
}
}
};
const assertCrawlArtifacts = async (root, inputs, headers) => {
const robots = await readRequiredText(root, 'robots.txt');
const redirects = await readRequiredText(root, '_redirects.example');
if (/https?:\/\//iu.test(redirects)) throw new Error('Redirect example must not invent a host.');
if (inputs.site.environment === 'preview') {
if (robots !== 'User-agent: *\nDisallow: /\n' || /sitemap/iu.test(robots)) {
throw new Error('Preview robots must disallow crawling without advertising a sitemap.');
}
return;
}
const sitemapUrl = new URL('/sitemap.xml', inputs.site.canonicalUrl).href;
if (robots !== `User-agent: *\nAllow: /\nSitemap: ${sitemapUrl}\n`) {
throw new Error('Production robots policy must allow the canonical page and advertise its sitemap.');
}
const sitemap = await readRequiredText(root, 'sitemap.xml');
if (!sitemap.includes(`<loc>${inputs.site.canonicalUrl}</loc>`)) {
throw new Error('Production sitemap must contain the canonical site URL.');
}
if (!headers.includes('Cache-Control: public, max-age=0, must-revalidate')) {
throw new Error('Production HTML requires revalidation caching.');
}
};
export const verifyArtifactDirectory = async (root, inputs) => {
const html = await readRequiredText(root, 'index.html');
const files = await listArtifacts(root);
assertArtifactInventory(files, html, inputs);
await assertNoProhibitedSurfaces(root, files);
assertSemanticHtml(html, inputs);
const serializedJsonLd = assertMetadata(html, inputs);
const headers = await readRequiredText(root, '_headers.example');
assertSecurityHeaders(headers, inputs, serializedJsonLd);
await assertCrawlArtifacts(root, inputs, headers);
};
const assertInsideRepository = (path) => {
const pathRelativeToRoot = relative(repositoryRoot, path);
if (pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) {
throw new Error('Verification input escaped the repository root.');
}
};
export const verifyArtifacts = async (configPath) => {
const configAbsolutePath = resolve(repositoryRoot, configPath);
assertInsideRepository(configAbsolutePath);
const inputs = await loadProductionInputs({
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
assetManifestJson: pathToFileURL(resolve(repositoryRoot, 'content/public/asset-manifest.json')),
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}${sep}`),
siteConfigJson: pathToFileURL(configAbsolutePath),
});
await verifyArtifactDirectory(resolve(repositoryRoot, 'dist'), inputs);
};
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {
const configPath = parseExactConfigArgument(process.argv.slice(2));
await verifyArtifacts(configPath);
}