mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Implement two different methods for fetching repository contents: 1. A new Cloudflare-compatible method using GitHub Contents API with batch processing 2. The existing zip-based method for non-Cloudflare environments The changes include better error handling, environment detection, and support for GitHub tokens from both Cloudflare context and process.env. Also added size limits and filtering for large files while allowing lock files.
242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
import { json } from '@remix-run/cloudflare';
|
|
import JSZip from 'jszip';
|
|
|
|
// Function to detect if we're running in Cloudflare
|
|
function isCloudflareEnvironment(context: any): boolean {
|
|
// Check if we're in production AND have Cloudflare Pages specific env vars
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
const hasCfPagesVars = !!(
|
|
context?.cloudflare?.env?.CF_PAGES ||
|
|
context?.cloudflare?.env?.CF_PAGES_URL ||
|
|
context?.cloudflare?.env?.CF_PAGES_COMMIT_SHA
|
|
);
|
|
|
|
return isProduction && hasCfPagesVars;
|
|
}
|
|
|
|
// Cloudflare-compatible method using GitHub Contents API
|
|
async function fetchRepoContentsCloudflare(repo: string, githubToken?: string) {
|
|
const baseUrl = 'https://api.github.com';
|
|
|
|
// Get repository info to find default branch
|
|
const repoResponse = await fetch(`${baseUrl}/repos/${repo}`, {
|
|
headers: {
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'bolt.diy-app',
|
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
|
},
|
|
});
|
|
|
|
if (!repoResponse.ok) {
|
|
throw new Error(`Repository not found: ${repo}`);
|
|
}
|
|
|
|
const repoData = (await repoResponse.json()) as any;
|
|
const defaultBranch = repoData.default_branch;
|
|
|
|
// Get the tree recursively
|
|
const treeResponse = await fetch(`${baseUrl}/repos/${repo}/git/trees/${defaultBranch}?recursive=1`, {
|
|
headers: {
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'bolt.diy-app',
|
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
|
},
|
|
});
|
|
|
|
if (!treeResponse.ok) {
|
|
throw new Error(`Failed to fetch repository tree: ${treeResponse.status}`);
|
|
}
|
|
|
|
const treeData = (await treeResponse.json()) as any;
|
|
|
|
// Filter for files only (not directories) and limit size
|
|
const files = treeData.tree.filter((item: any) => {
|
|
if (item.type !== 'blob') {
|
|
return false;
|
|
}
|
|
|
|
if (item.path.startsWith('.git/')) {
|
|
return false;
|
|
}
|
|
|
|
// Allow lock files even if they're large
|
|
const isLockFile =
|
|
item.path.endsWith('package-lock.json') ||
|
|
item.path.endsWith('yarn.lock') ||
|
|
item.path.endsWith('pnpm-lock.yaml');
|
|
|
|
// For non-lock files, limit size to 100KB
|
|
if (!isLockFile && item.size >= 100000) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Fetch file contents in batches to avoid overwhelming the API
|
|
const batchSize = 10;
|
|
const fileContents = [];
|
|
|
|
for (let i = 0; i < files.length; i += batchSize) {
|
|
const batch = files.slice(i, i + batchSize);
|
|
const batchPromises = batch.map(async (file: any) => {
|
|
try {
|
|
const contentResponse = await fetch(`${baseUrl}/repos/${repo}/contents/${file.path}`, {
|
|
headers: {
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'bolt.diy-app',
|
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
|
},
|
|
});
|
|
|
|
if (!contentResponse.ok) {
|
|
console.warn(`Failed to fetch ${file.path}: ${contentResponse.status}`);
|
|
return null;
|
|
}
|
|
|
|
const contentData = (await contentResponse.json()) as any;
|
|
const content = atob(contentData.content.replace(/\s/g, ''));
|
|
|
|
return {
|
|
name: file.path.split('/').pop() || '',
|
|
path: file.path,
|
|
content,
|
|
};
|
|
} catch (error) {
|
|
console.warn(`Error fetching ${file.path}:`, error);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const batchResults = await Promise.all(batchPromises);
|
|
fileContents.push(...batchResults.filter(Boolean));
|
|
|
|
// Add a small delay between batches to be respectful to the API
|
|
if (i + batchSize < files.length) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
}
|
|
|
|
return fileContents;
|
|
}
|
|
|
|
// Your existing method for non-Cloudflare environments
|
|
async function fetchRepoContentsZip(repo: string, githubToken?: string) {
|
|
const baseUrl = 'https://api.github.com';
|
|
|
|
// Get the latest release
|
|
const releaseResponse = await fetch(`${baseUrl}/repos/${repo}/releases/latest`, {
|
|
headers: {
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'bolt.diy-app',
|
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
|
},
|
|
});
|
|
|
|
if (!releaseResponse.ok) {
|
|
throw new Error(`GitHub API error: ${releaseResponse.status} - ${releaseResponse.statusText}`);
|
|
}
|
|
|
|
const releaseData = (await releaseResponse.json()) as any;
|
|
const zipballUrl = releaseData.zipball_url;
|
|
|
|
// Fetch the zipball
|
|
const zipResponse = await fetch(zipballUrl, {
|
|
headers: {
|
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
|
},
|
|
});
|
|
|
|
if (!zipResponse.ok) {
|
|
throw new Error(`Failed to fetch release zipball: ${zipResponse.status}`);
|
|
}
|
|
|
|
// Get the zip content as ArrayBuffer
|
|
const zipArrayBuffer = await zipResponse.arrayBuffer();
|
|
|
|
// Use JSZip to extract the contents
|
|
const zip = await JSZip.loadAsync(zipArrayBuffer);
|
|
|
|
// Find the root folder name
|
|
let rootFolderName = '';
|
|
zip.forEach((relativePath) => {
|
|
if (!rootFolderName && relativePath.includes('/')) {
|
|
rootFolderName = relativePath.split('/')[0];
|
|
}
|
|
});
|
|
|
|
// Extract all files
|
|
const promises = Object.keys(zip.files).map(async (filename) => {
|
|
const zipEntry = zip.files[filename];
|
|
|
|
// Skip directories
|
|
if (zipEntry.dir) {
|
|
return null;
|
|
}
|
|
|
|
// Skip the root folder itself
|
|
if (filename === rootFolderName) {
|
|
return null;
|
|
}
|
|
|
|
// Remove the root folder from the path
|
|
let normalizedPath = filename;
|
|
|
|
if (rootFolderName && filename.startsWith(rootFolderName + '/')) {
|
|
normalizedPath = filename.substring(rootFolderName.length + 1);
|
|
}
|
|
|
|
// Get the file content
|
|
const content = await zipEntry.async('string');
|
|
|
|
return {
|
|
name: normalizedPath.split('/').pop() || '',
|
|
path: normalizedPath,
|
|
content,
|
|
};
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
return results.filter(Boolean);
|
|
}
|
|
|
|
export async function loader({ request, context }: { request: Request; context: any }) {
|
|
const url = new URL(request.url);
|
|
const repo = url.searchParams.get('repo');
|
|
|
|
if (!repo) {
|
|
return json({ error: 'Repository name is required' }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
// Access environment variables from Cloudflare context or process.env
|
|
const githubToken = context?.cloudflare?.env?.GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
|
|
let fileList;
|
|
|
|
if (isCloudflareEnvironment(context)) {
|
|
fileList = await fetchRepoContentsCloudflare(repo, githubToken);
|
|
} else {
|
|
fileList = await fetchRepoContentsZip(repo, githubToken);
|
|
}
|
|
|
|
// Filter out .git files for both methods
|
|
const filteredFiles = fileList.filter((file: any) => !file.path.startsWith('.git'));
|
|
|
|
return json(filteredFiles);
|
|
} catch (error) {
|
|
console.error('Error processing GitHub template:', error);
|
|
console.error('Repository:', repo);
|
|
console.error('Error details:', error instanceof Error ? error.message : String(error));
|
|
|
|
return json(
|
|
{
|
|
error: 'Failed to fetch template files',
|
|
details: error instanceof Error ? error.message : String(error),
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|