mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Wire project docs sync into pre-build step, add comprehensive dark theme CSS variables, integrate ProjectSwitcher into sidebar, and add custom Cloudflare Pages deployment script with batched uploads and retry logic.
167 lines
4.8 KiB
JavaScript
167 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Custom CF Pages deployment script that handles large file counts
|
|
* by batching uploads in smaller chunks to avoid EPIPE errors.
|
|
*/
|
|
import { createHash } from 'crypto';
|
|
import { readdir, readFile, stat } from 'fs/promises';
|
|
import { join, relative } from 'path';
|
|
|
|
const ACCOUNT_ID = '94a3e3f299092abb1feda2a7481ea845';
|
|
const PROJECT_NAME = 'hanzo-docs';
|
|
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || 'bBEtGaPXpGSmvC5w2WbpE2V7zx08VDWspCS-E30g';
|
|
const BASE_URL = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT_NAME}`;
|
|
const BATCH_SIZE = 50; // files per upload batch
|
|
const MAX_RETRIES = 3;
|
|
|
|
async function getAllFiles(dir) {
|
|
const files = [];
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...await getAllFiles(fullPath));
|
|
} else {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
async function hashFile(filePath) {
|
|
const content = await readFile(filePath);
|
|
const hash = createHash('sha256').update(content).digest('hex');
|
|
return { path: filePath, hash, content, size: content.length };
|
|
}
|
|
|
|
async function apiCall(url, options = {}) {
|
|
const resp = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Authorization': `Bearer ${API_TOKEN}`,
|
|
...options.headers,
|
|
},
|
|
});
|
|
const data = await resp.json();
|
|
if (!data.success) {
|
|
throw new Error(`CF API error: ${JSON.stringify(data.errors)}`);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function createDeployment(hashes) {
|
|
// Step 1: Create deployment with manifest
|
|
const manifest = {};
|
|
for (const { relativePath, hash } of hashes) {
|
|
manifest[`/${relativePath}`] = hash;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('manifest', JSON.stringify(manifest));
|
|
formData.append('branch', 'dev');
|
|
|
|
console.log(`Creating deployment with ${Object.keys(manifest).length} files...`);
|
|
const data = await apiCall(`${BASE_URL}/deployments`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
return data.result;
|
|
}
|
|
|
|
async function uploadBatch(files, jwt) {
|
|
const formData = new FormData();
|
|
for (const file of files) {
|
|
formData.append(file.hash, new Blob([file.content]), file.relativePath);
|
|
}
|
|
|
|
const resp = await fetch('https://api.cloudflare.com/client/v4/pages/assets/upload', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${jwt}`,
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const text = await resp.text();
|
|
throw new Error(`Upload failed (${resp.status}): ${text}`);
|
|
}
|
|
|
|
return resp.json();
|
|
}
|
|
|
|
async function main() {
|
|
const outDir = process.argv[2] || 'apps/docs/out';
|
|
|
|
console.log(`Scanning ${outDir}...`);
|
|
const allFiles = await getAllFiles(outDir);
|
|
console.log(`Found ${allFiles.length} files`);
|
|
|
|
// Hash all files
|
|
console.log('Hashing files...');
|
|
const fileInfos = await Promise.all(
|
|
allFiles.map(async (filePath) => {
|
|
const info = await hashFile(filePath);
|
|
return {
|
|
...info,
|
|
relativePath: relative(outDir, filePath),
|
|
};
|
|
})
|
|
);
|
|
|
|
// Create deployment (gets JWT token and list of missing files)
|
|
const deployment = await createDeployment(fileInfos);
|
|
console.log(`Deployment ID: ${deployment.id}`);
|
|
console.log(`URL: ${deployment.url}`);
|
|
|
|
const jwt = deployment.jwt;
|
|
if (!jwt) {
|
|
console.log('No JWT returned - all files already uploaded!');
|
|
return;
|
|
}
|
|
|
|
// Find which files need uploading
|
|
const missingHashes = new Set(deployment.required_hashes || []);
|
|
const filesToUpload = missingHashes.size > 0
|
|
? fileInfos.filter(f => missingHashes.has(f.hash))
|
|
: fileInfos; // upload all if no required_hashes returned
|
|
|
|
console.log(`Need to upload ${filesToUpload.length} files (${fileInfos.length - filesToUpload.length} already cached)`);
|
|
|
|
// Upload in batches
|
|
const batches = [];
|
|
for (let i = 0; i < filesToUpload.length; i += BATCH_SIZE) {
|
|
batches.push(filesToUpload.slice(i, i + BATCH_SIZE));
|
|
}
|
|
|
|
let uploaded = 0;
|
|
for (let i = 0; i < batches.length; i++) {
|
|
const batch = batches[i];
|
|
let retries = 0;
|
|
while (retries < MAX_RETRIES) {
|
|
try {
|
|
await uploadBatch(batch, jwt);
|
|
uploaded += batch.length;
|
|
process.stdout.write(`\rUploaded ${uploaded}/${filesToUpload.length} (batch ${i + 1}/${batches.length})`);
|
|
break;
|
|
} catch (err) {
|
|
retries++;
|
|
if (retries >= MAX_RETRIES) {
|
|
console.error(`\nFailed batch ${i + 1} after ${MAX_RETRIES} retries: ${err.message}`);
|
|
throw err;
|
|
}
|
|
console.warn(`\nRetrying batch ${i + 1} (attempt ${retries + 1})...`);
|
|
await new Promise(r => setTimeout(r, 1000 * retries));
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`\nDeployment complete: ${deployment.url}`);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Deploy failed:', err.message);
|
|
process.exit(1);
|
|
});
|