#!/usr/bin/env ts-node /** * PDF Datasheet Generator (React-PDF) * * Uses the same Excel-driven data model as the legacy generator, but renders * PDFs via `@react-pdf/renderer` for maintainable layout and pagination. */ import * as fs from 'fs'; import * as path from 'path'; import type { ProductData } from './pdf/model/types'; import { generateDatasheetPdfBuffer } from './pdf/react-pdf/generate-datasheet-pdf'; import { generateFileName, normalizeValue } from './pdf/model/utils'; const CONFIG = { productsFile: path.join(process.cwd(), 'data/processed/products.json'), outputDir: path.join(process.cwd(), 'public/datasheets'), chunkSize: 10, } as const; function ensureOutputDir(): void { if (!fs.existsSync(CONFIG.outputDir)) { fs.mkdirSync(CONFIG.outputDir, { recursive: true }); } } async function readProductsStream(): Promise { console.log('Reading products.json...'); return new Promise((resolve, reject) => { const stream = fs.createReadStream(CONFIG.productsFile, { encoding: 'utf8' }); let data = ''; stream.on('data', chunk => { data += chunk; }); stream.on('end', () => { try { const products = JSON.parse(data) as ProductData[]; console.log(`Loaded ${products.length} products`); resolve(products); } catch (error) { reject(new Error(`Failed to parse JSON: ${error}`)); } }); stream.on('error', error => reject(new Error(`Failed to read file: ${error}`))); }); } async function processChunk(products: ProductData[], chunkIndex: number, totalChunks: number): Promise { console.log(`\nProcessing chunk ${chunkIndex + 1}/${totalChunks} (${products.length} products)...`); for (const product of products) { try { const locale = (product.locale || 'en') as 'en' | 'de'; const buffer = await generateDatasheetPdfBuffer({ product, locale }); const fileName = generateFileName(product, locale); fs.writeFileSync(path.join(CONFIG.outputDir, fileName), buffer); console.log(`āœ“ ${locale.toUpperCase()}: ${fileName}`); await new Promise(resolve => setTimeout(resolve, 25)); } catch (error) { console.error(`āœ— Failed to process product ${product.id}:`, error); } } } async function processProductsInChunks(): Promise { console.log('Starting PDF generation (React-PDF)'); ensureOutputDir(); const allProducts = await readProductsStream(); if (allProducts.length === 0) { console.log('No products found'); return; } // Dev convenience: generate only one locale / one product subset. // IMPORTANT: apply filters BEFORE PDF_LIMIT so the limit works within the filtered set. let products = allProducts; const onlyLocale = normalizeValue(String(process.env.PDF_LOCALE || '')).toLowerCase(); if (onlyLocale === 'de' || onlyLocale === 'en') { products = products.filter(p => (p.locale || 'en') === onlyLocale); } const match = normalizeValue(String(process.env.PDF_MATCH || '')).toLowerCase(); if (match) { products = products.filter(p => { const hay = [p.slug, p.translationKey, p.sku, p.name] .filter(Boolean) .join(' ') .toLowerCase(); return hay.includes(match); }); } const limit = Number(process.env.PDF_LIMIT || '0'); products = Number.isFinite(limit) && limit > 0 ? products.slice(0, limit) : products; const enProducts = products.filter(p => (p.locale || 'en') === 'en'); const deProducts = products.filter(p => (p.locale || 'en') === 'de'); console.log(`Found ${enProducts.length} EN + ${deProducts.length} DE products`); const totalChunks = Math.ceil(products.length / CONFIG.chunkSize); for (let i = 0; i < totalChunks; i++) { const chunk = products.slice(i * CONFIG.chunkSize, (i + 1) * CONFIG.chunkSize); await processChunk(chunk, i, totalChunks); } console.log('\nāœ… PDF generation completed!'); console.log(`Generated ${enProducts.length} EN + ${deProducts.length} DE PDFs`); console.log(`Output: ${CONFIG.outputDir}`); } async function main(): Promise { const start = Date.now(); try { await processProductsInChunks(); console.log(`\nTime: ${((Date.now() - start) / 1000).toFixed(2)}s`); } catch (error) { console.error('Fatal error:', error); process.exit(1); } } main().catch(console.error); export { main as generatePDFDatasheets };