UNPKG

veko

Version:

Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR

642 lines (540 loc) 17.7 kB
/** * Veko Intelligent Page Builder * Handles building pages based on their type with smart optimizations * * @module veko/core/builder */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { PageTypes, PageSymbols, PageTypesManager } = require('./page-types'); const { PageRouter } = require('./router'); /** * Build result * @typedef {Object} BuildResult * @property {boolean} success * @property {string} [html] * @property {Object} [assets] * @property {number} [buildTime] * @property {string} [error] */ /** * Intelligent Page Builder */ class PageBuilder { constructor(vsv, options = {}) { this.vsv = vsv; this.options = { outDir: options.outDir || 'dist', staticDir: options.staticDir || 'dist/static', cacheDir: options.cacheDir || '.veko/build', minify: options.minify !== false, sourceMaps: options.sourceMaps || false, analyze: options.analyze || false, parallel: options.parallel || 4, ...options }; // Initialize managers this.pageTypes = new PageTypesManager(vsv, options); this.router = new PageRouter({ pagesDir: options.pagesDir || 'pages', ultraDirs: options.ultra?.dirs || [], ultraFiles: options.ultra?.files || [], ultraPatterns: options.ultra?.patterns || [] }); // Build state this.buildManifest = { version: 1, builtAt: null, pages: [], assets: [], chunks: [] }; // Dependency graph this.dependencyGraph = new Map(); // Build statistics this.stats = { totalPages: 0, staticPages: 0, ssgPages: 0, dynamicPages: 0, ultraPages: 0, totalTime: 0, errors: [] }; } /** * Initialize builder */ async init() { await this.router.init(); return this; } /** * Full production build */ async build() { const startTime = Date.now(); console.log('\n ⚡ Veko Build\n'); // Create output directories this.ensureDirectories(); // Initialize router await this.init(); // Get all routes const allRoutes = this.router.getAllRoutes(); this.stats.totalPages = allRoutes.length; console.log(` Building ${allRoutes.length} pages...\n`); // Group routes by type const routesByType = { [PageTypes.STATIC]: [], [PageTypes.SSG]: [], [PageTypes.DYNAMIC]: [], [PageTypes.ULTRA]: [] }; for (const route of allRoutes) { routesByType[route.type].push(route); } // Build static pages await this.buildStaticPages(routesByType[PageTypes.STATIC]); // Build SSG pages await this.buildSSGPages(routesByType[PageTypes.SSG]); // Prepare dynamic pages (generate server handlers) await this.prepareDynamicPages(routesByType[PageTypes.DYNAMIC]); // Prepare ultra pages (minimal build, full lazy) await this.prepareUltraPages(routesByType[PageTypes.ULTRA]); // Generate assets and chunks await this.generateAssets(); // Write build manifest this.buildManifest.builtAt = new Date().toISOString(); await this.writeManifest(); this.stats.totalTime = Date.now() - startTime; // Print summary this.printBuildSummary(); return this.stats; } /** * Ensure output directories exist */ ensureDirectories() { const dirs = [ this.options.outDir, this.options.staticDir, this.options.cacheDir, path.join(this.options.outDir, 'server'), path.join(this.options.outDir, 'client'), path.join(this.options.outDir, 'assets') ]; for (const dir of dirs) { const fullPath = path.join(process.cwd(), dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } } } /** * Build static pages */ async buildStaticPages(routes) { if (routes.length === 0) return; console.log(` ${PageSymbols.static} Building ${routes.length} static pages...`); for (const route of routes) { try { const result = await this.buildStaticPage(route); if (result.success) { this.stats.staticPages++; this.buildManifest.pages.push({ path: route.path, type: PageTypes.STATIC, file: this.getOutputPath(route.path), hash: result.hash }); } else { this.stats.errors.push({ route: route.path, error: result.error }); } } catch (error) { this.stats.errors.push({ route: route.path, error: error.message }); } } } /** * Build a single static page */ async buildStaticPage(route) { const startTime = Date.now(); try { // Compile component const compiled = await this.vsv.compiler.compileFile(route.file); // Render to HTML const html = await this.vsv.renderer.render(compiled, {}); // Process Tailwind if enabled let processedHtml = html; if (this.vsv.tailwind) { processedHtml = await this.processTailwind(html); } // Minify if enabled if (this.options.minify) { processedHtml = this.minifyHtml(processedHtml); } // Write to static directory const outputPath = this.getOutputPath(route.path); const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(outputPath, processedHtml); return { success: true, html: processedHtml, hash: this.hashContent(processedHtml), buildTime: Date.now() - startTime }; } catch (error) { return { success: false, error: error.message }; } } /** * Build SSG pages */ async buildSSGPages(routes) { if (routes.length === 0) return; console.log(` ${PageSymbols.ssg} Building ${routes.length} SSG pages...`); for (const route of routes) { try { // Handle dynamic SSG routes (getStaticPaths) if (route.dynamic) { const paths = await this.getStaticPaths(route); for (const pathParams of paths) { const result = await this.buildSSGPage(route, pathParams); if (result.success) { this.stats.ssgPages++; this.buildManifest.pages.push({ path: this.interpolatePath(route.path, pathParams), type: PageTypes.SSG, file: this.getOutputPath(this.interpolatePath(route.path, pathParams)), hash: result.hash, revalidate: result.revalidate }); } } } else { const result = await this.buildSSGPage(route, {}); if (result.success) { this.stats.ssgPages++; this.buildManifest.pages.push({ path: route.path, type: PageTypes.SSG, file: this.getOutputPath(route.path), hash: result.hash, revalidate: result.revalidate }); } } } catch (error) { this.stats.errors.push({ route: route.path, error: error.message }); } } } /** * Get static paths for dynamic SSG route */ async getStaticPaths(route) { try { const component = await this.vsv.loadComponent(route.file); if (component.getStaticPaths) { const result = await component.getStaticPaths(); return result.paths || []; } return [{}]; } catch (error) { console.warn(` Warning: Could not get static paths for ${route.path}`); return []; } } /** * Build a single SSG page */ async buildSSGPage(route, params) { const startTime = Date.now(); try { const component = await this.vsv.loadComponent(route.file); // Execute getStaticProps let props = {}; let revalidate = null; if (component.getStaticProps) { const result = await component.getStaticProps({ params }); props = result.props || result; revalidate = result.revalidate; } // Compile and render const compiled = await this.vsv.compiler.compileFile(route.file); const html = await this.vsv.renderer.render(compiled, props); // Process and write let processedHtml = html; if (this.vsv.tailwind) { processedHtml = await this.processTailwind(html); } if (this.options.minify) { processedHtml = this.minifyHtml(processedHtml); } const outputPath = this.getOutputPath(this.interpolatePath(route.path, params)); const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(outputPath, processedHtml); return { success: true, html: processedHtml, hash: this.hashContent(processedHtml), revalidate, buildTime: Date.now() - startTime }; } catch (error) { return { success: false, error: error.message }; } } /** * Prepare dynamic pages (server-side rendering) */ async prepareDynamicPages(routes) { if (routes.length === 0) return; console.log(` ${PageSymbols.dynamic} Preparing ${routes.length} dynamic pages...`); for (const route of routes) { try { // Precompile the component const compiled = await this.vsv.compiler.compileFile(route.file); // Generate server handler const handler = this.generateDynamicHandler(route, compiled); // Write handler to server directory const handlerPath = path.join( process.cwd(), this.options.outDir, 'server', this.sanitizePath(route.path) + '.js' ); const handlerDir = path.dirname(handlerPath); if (!fs.existsSync(handlerDir)) { fs.mkdirSync(handlerDir, { recursive: true }); } fs.writeFileSync(handlerPath, handler); this.stats.dynamicPages++; this.buildManifest.pages.push({ path: route.path, type: PageTypes.DYNAMIC, handler: handlerPath, component: route.component }); } catch (error) { this.stats.errors.push({ route: route.path, error: error.message }); } } } /** * Generate dynamic page handler */ generateDynamicHandler(route, compiled) { return ` // Auto-generated dynamic page handler for ${route.path} // Generated at: ${new Date().toISOString()} const compiledComponent = ${JSON.stringify(compiled.server)}; module.exports = { path: '${route.path}', component: '${route.component}', async render(req, vsv) { // Load component const component = await vsv.loadComponent('${route.file}'); // Get server props let props = {}; if (component.getServerProps) { props = await component.getServerProps({ req, params: req.params || {}, query: req.query || {} }); } // Render return vsv.renderer.render(compiledComponent, props); } }; `; } /** * Prepare ultra-dynamic pages (lazy build on demand) */ async prepareUltraPages(routes) { if (routes.length === 0) return; console.log(` ${PageSymbols.ultra} Preparing ${routes.length} ultra-dynamic pages...`); for (const route of routes) { try { // Only analyze dependencies, don't build const dependencies = await this.analyzeDependencies(route.file); // Store minimal info for lazy building this.buildManifest.pages.push({ path: route.path, type: PageTypes.ULTRA, source: route.file, component: route.component, dependencies, lazyBuild: true }); this.stats.ultraPages++; } catch (error) { this.stats.errors.push({ route: route.path, error: error.message }); } } } /** * Analyze component dependencies */ async analyzeDependencies(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const dependencies = []; // Find component imports const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { const [, name, importPath] = match; // Skip external modules if (!importPath.startsWith('.') && !importPath.startsWith('/')) { continue; } dependencies.push({ name, path: importPath, resolved: this.resolveImportPath(filePath, importPath) }); } return dependencies; } /** * Resolve import path to absolute path */ resolveImportPath(fromFile, importPath) { const dir = path.dirname(fromFile); const extensions = ['.jsv', '.tsv', '.jsx', '.tsx', '.js', '.ts']; for (const ext of extensions) { const resolved = path.resolve(dir, importPath + ext); if (fs.existsSync(resolved)) { return resolved; } const indexResolved = path.resolve(dir, importPath, 'index' + ext); if (fs.existsSync(indexResolved)) { return indexResolved; } } return path.resolve(dir, importPath); } /** * Generate assets (CSS, JS bundles) */ async generateAssets() { console.log(' 📦 Generating assets...'); // Collect all CSS // Tailwind generation handled per-page // Generate client runtime bundle const clientRuntime = this.vsv.runtime.getClientRuntime(); const runtimePath = path.join( process.cwd(), this.options.outDir, 'client', 'runtime.js' ); fs.writeFileSync(runtimePath, clientRuntime); this.buildManifest.assets.push({ type: 'js', name: 'runtime', path: 'client/runtime.js', size: clientRuntime.length }); } /** * Process Tailwind CSS for HTML */ async processTailwind(html) { if (!this.vsv.tailwind) return html; const css = this.vsv.tailwind.generate(html); // Inject CSS into HTML return html.replace('</head>', `<style>${css}</style></head>`); } /** * Minify HTML */ minifyHtml(html) { return html .replace(/\s+/g, ' ') .replace(/>\s+</g, '><') .replace(/<!--[\s\S]*?-->/g, '') .trim(); } /** * Get output file path for a route */ getOutputPath(routePath) { const sanitized = this.sanitizePath(routePath); return path.join( process.cwd(), this.options.staticDir, sanitized, 'index.html' ); } /** * Sanitize route path for file system */ sanitizePath(routePath) { return routePath .replace(/^\//, '') .replace(/\/$/, '') .replace(/:/g, '_') || 'index'; } /** * Interpolate dynamic path with params */ interpolatePath(routePath, params) { let result = routePath; for (const [key, value] of Object.entries(params)) { result = result.replace(`:${key}`, value); } return result; } /** * Hash content for cache busting */ hashContent(content) { return crypto.createHash('md5').update(content).digest('hex').slice(0, 8); } /** * Write build manifest */ async writeManifest() { const manifestPath = path.join( process.cwd(), this.options.outDir, 'build-manifest.json' ); fs.writeFileSync(manifestPath, JSON.stringify(this.buildManifest, null, 2)); } /** * Print build summary */ printBuildSummary() { console.log('\n Build Summary:'); console.log(' ─────────────────────────────────────────────'); console.log(` ${PageSymbols.static} Static: ${this.stats.staticPages} pages`); console.log(` ${PageSymbols.ssg} SSG: ${this.stats.ssgPages} pages`); console.log(` ${PageSymbols.dynamic} Dynamic: ${this.stats.dynamicPages} pages`); console.log(` ${PageSymbols.ultra} Ultra: ${this.stats.ultraPages} pages`); console.log(' ─────────────────────────────────────────────'); console.log(` Total: ${this.stats.totalPages} pages`); console.log(` Time: ${this.stats.totalTime}ms`); if (this.stats.errors.length > 0) { console.log(`\n ⚠ Errors: ${this.stats.errors.length}`); for (const err of this.stats.errors) { console.log(` - ${err.route}: ${err.error}`); } } console.log('\n ✓ Build complete!\n'); } } module.exports = { PageBuilder };