/ scripts / gen-renderers.ts
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();