gen-renderers.ts
1 /** 2 * THIS SCRIPT IS CURRENTLY UNUSED 3 * 4 * Block renderer generation will be integrated later when needed. 5 * The API client and metadata models are now generated by gen-openapi.js. 6 */ 7 8 import fs from 'fs'; 9 import path from 'path'; 10 import https from 'https'; 11 import http from 'http'; 12 13 // Define the expected structure for an entry in the schema index 14 interface BlockSchemaInfo { 15 type: string; 16 version: number; // Or string if versions are like "1.0" 17 url?: string; // Optional URL from the schema index itself 18 latest_url?: string; // Optional latest URL 19 // Potentially add other metadata if needed later 20 } 21 22 // --- Helper function with support for redirects --- 23 // Function to fetch data using native http/https 24 function fetchData(url: string, maxRedirects: number = 5): Promise<string> { 25 return new Promise((resolve, reject) => { 26 const makeRequest = (currentUrl: string, redirectsLeft: number) => { 27 const client = currentUrl.startsWith('https') ? https : http; 28 29 console.log(` Fetching: ${currentUrl}`); 30 client.get(currentUrl, (res) => { 31 if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { 32 // Got a redirect 33 if (redirectsLeft === 0) { 34 reject(new Error(`Too many redirects (followed ${maxRedirects})`)); 35 return; 36 } 37 38 // Construct absolute URL if relative 39 const redirectUrl = /^https?:\/\//.test(res.headers.location) 40 ? res.headers.location 41 : new URL(res.headers.location, new URL(currentUrl).origin).toString(); 42 43 console.log(` Redirected to: ${redirectUrl}`); 44 makeRequest(redirectUrl, redirectsLeft - 1); 45 return; 46 } 47 48 if (res.statusCode !== 200) { 49 reject(new Error(`Failed to fetch: ${res.statusCode}`)); 50 return; 51 } 52 53 let data = ''; 54 res.on('data', (chunk) => { 55 data += chunk; 56 }); 57 58 res.on('end', () => { 59 resolve(data); 60 }); 61 }).on('error', (err) => { 62 reject(err); 63 }); 64 }; 65 66 makeRequest(url, maxRedirects); 67 }); 68 } 69 // --- End helper function --- 70 71 // Define the input schema index file path 72 const schemaIndexPath = path.resolve(process.cwd(), 'schemas', 'index.json'); 73 74 // Define the fallback fetch URL (allow override via environment variable) 75 const schemaIndexFetchUrl = process.env.SCHEMA_INDEX_URL || `${process.env.FASTAPI_URL || 'http://localhost:8000'}/api/v1/schemas/index.json`; 76 77 // Define the output directory for renderer components 78 const renderersDir = path.resolve(process.cwd(), 'src', 'components', 'block_renderers'); 79 80 // Define the output path for the manifest file 81 const manifestPath = path.resolve(process.cwd(), 'src', 'lib', 'blockRendererRegistry.data.ts'); 82 83 // Define the template for the generated renderer component 84 const rendererTemplate = (type: string, version: number | string) => { 85 const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); 86 return `import React from 'react'; 87 import { BaseBlockRendererProps } from '@/lib/blockRendererRegistry'; 88 import type { ${capitalizedType}Metadata } from '@/data/block_metadata/${type}'; 89 import { ${capitalizedType}MetadataSchema } from '@/data/block_metadata/${type}.zod'; 90 91 /** 92 * Props specific to the ${capitalizedType} renderer component 93 */ 94 export interface ${capitalizedType}V${version}Props extends BaseBlockRendererProps { 95 data: { 96 metadata: ${capitalizedType}Metadata; 97 [key: string]: unknown; 98 }; 99 } 100 101 /** 102 * Renderer for ${capitalizedType} blocks (version ${version}) 103 */ 104 const ${capitalizedType}V${version}Renderer: React.FC<${capitalizedType}V${version}Props> = (props) => { 105 const { data, blockId, blockType, blockVersion } = props; 106 107 // Access metadata with proper typing 108 const { metadata } = data; 109 110 return ( 111 <div className="block-renderer content-block ${type.toLowerCase()}-block border p-4 rounded-lg"> 112 <div className="flex items-start justify-between mb-2"> 113 <h3 className="text-lg font-serif font-semibold"> 114 {/* Use appropriate property from metadata */} 115 {data.text || \`${capitalizedType} \${blockId}\`} 116 </h3> 117 <div className="text-xs text-muted-foreground"> 118 {blockType} v{blockVersion} 119 </div> 120 </div> 121 122 {/* Add your component implementation here */} 123 {/* This is a generated stub that should be customized */} 124 125 <details className="mt-4 text-xs"> 126 <summary className="cursor-pointer text-muted-foreground">View raw data</summary> 127 <pre className="mt-2 p-3 bg-muted rounded-md overflow-auto text-xs"> 128 {JSON.stringify(data, null, 2)} 129 </pre> 130 </details> 131 </div> 132 ); 133 }; 134 135 export default ${capitalizedType}V${version}Renderer; 136 `; 137 }; 138 139 async function generateRenderers() { 140 console.log(`Starting renderer generation...`); 141 142 let schemaIndex: BlockSchemaInfo[]; 143 let schemaSource: string; 144 145 try { 146 // Check if schema index file exists locally 147 if (fs.existsSync(schemaIndexPath)) { 148 console.log(`Reading schema index from local file: ${schemaIndexPath}`); 149 const fileContent = await fs.promises.readFile(schemaIndexPath, 'utf-8'); 150 const fileData = JSON.parse(fileContent); 151 if (fileData && Array.isArray(fileData.schemas)) { 152 schemaIndex = fileData.schemas; 153 } else { 154 console.warn(`⚠️ Local schema file does not contain a 'schemas' array. Assuming no schemas.`); 155 schemaIndex = []; 156 } 157 schemaSource = 'local file'; 158 console.log(`Successfully read schema index from local file.`); 159 } else { 160 console.log(`Local schema index not found at ${schemaIndexPath}.`); 161 console.log(`Attempting to fetch schema index from: ${schemaIndexFetchUrl}`); 162 try { 163 // Use the new fetchData helper 164 const fetchedContent = await fetchData(schemaIndexFetchUrl); 165 const responseData = JSON.parse(fetchedContent); 166 167 if (responseData && Array.isArray(responseData.schemas)) { 168 schemaIndex = responseData.schemas; 169 } else { 170 console.warn(`⚠️ Fetched data does not contain a 'schemas' array. Assuming no schemas.`); 171 schemaIndex = []; 172 } 173 schemaSource = 'fetched URL'; 174 console.log(`Successfully fetched and parsed schema index from URL.`); 175 176 } catch (fetchError) { 177 console.error(`❌ Failed to fetch schema index from ${schemaIndexFetchUrl}:`, fetchError); 178 console.warn(`⚠️ Proceeding without schema index. No renderers will be generated.`); 179 schemaIndex = []; // Ensure schemaIndex is an empty array if fetch fails 180 schemaSource = 'fetch failed'; 181 } 182 } 183 184 // Ensure the output directory exists 185 if (!fs.existsSync(renderersDir)) { 186 console.log(`Creating renderers directory: ${renderersDir}`); 187 await fs.promises.mkdir(renderersDir, { recursive: true }); 188 } else { 189 console.log(`Renderers directory already exists: ${renderersDir}`); 190 } 191 192 // Generate renderer stubs 193 let generatedCount = 0; 194 let skippedCount = 0; 195 let generatedRenderers: { key: string; path: string }[] = []; 196 197 for (const schemaInfo of schemaIndex) { 198 // Skip base schema as it's not a specific block type 199 if (schemaInfo.type === 'base') continue; 200 201 const { type, version } = schemaInfo; 202 // Sanitize type name for component/file naming (e.g., remove spaces, special chars) 203 // Basic example: Capitalize first letter, ensure alphanumeric 204 const componentType = type.charAt(0).toUpperCase() + type.slice(1).replace(/[^a-zA-Z0-9]/g, ''); 205 const fileName = `${componentType}.v${version}.tsx`; 206 const filePath = path.join(renderersDir, fileName); 207 const relativeImportPath = `../components/block_renderers/${fileName.replace('.tsx', '')}`; 208 const registryKey = `${componentType}_v${version}`; 209 210 // Add to manifest list regardless of whether it's generated or skipped 211 generatedRenderers.push({ key: registryKey, path: relativeImportPath }); 212 213 console.log(`Generating/overwriting stub for ${type} v${version} -> ${fileName}`); 214 const componentContent = rendererTemplate(type, version); 215 await fs.promises.writeFile(filePath, componentContent, 'utf-8'); 216 generatedCount++; 217 } 218 219 // Generate the manifest file 220 console.log(`Generating renderer manifest file: ${manifestPath}`); 221 const manifestContent = ` 222 // This file is auto-generated by scripts/gen-renderers.ts 223 // Do not edit this file directly. 224 225 export const rendererManifest: Record<string, string> = { 226 ${generatedRenderers.map(r => ` '${r.key}': '${r.path}',`).join('\n')} 227 }; 228 `.trim() + '\n'; // Ensure trailing newline 229 230 await fs.promises.writeFile(manifestPath, manifestContent, 'utf-8'); 231 console.log(`✅ Successfully generated renderer manifest with ${generatedRenderers.length} entries.`); 232 233 console.log(`✅ Successfully generated ${generatedCount} renderer stubs in ${renderersDir} (source: ${schemaSource})`); 234 235 } catch (error) { 236 console.error(`❌ Error generating renderers:`, error); 237 process.exit(1); // Exit with error code 238 } 239 } 240 241 // Uncomment this line when you want to use this script 242 generateRenderers();