UNPKG

veko

Version:

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

480 lines (403 loc) 13.8 kB
/** * Veko Page Types System * Supports 4 types of pages: * ○ Static - Prerendered as static content * ● SSG - Prerendered as static HTML (uses getStaticProps) * ƒ Dynamic - Server-rendered on demand * ⚡ Ultra - Lazy-built on demand with selective component bundling * * @module veko/core/page-types */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); /** * Page type constants */ const PageTypes = { STATIC: 'static', // ○ Prerendered content SSG: 'ssg', // ● Static generation with getStaticProps DYNAMIC: 'dynamic', // ƒ Server-rendered on demand ULTRA: 'ultra' // ⚡ Lazy-built with selective bundling }; /** * Page type symbols for display */ const PageSymbols = { [PageTypes.STATIC]: '○', [PageTypes.SSG]: '●', [PageTypes.DYNAMIC]: 'ƒ', [PageTypes.ULTRA]: '⚡' }; /** * Page metadata interface * @typedef {Object} PageMeta * @property {string} type - Page type (static, ssg, dynamic, ultra) * @property {string} path - Route path * @property {string} component - Component name * @property {string} file - Source file path * @property {Object} [config] - Additional config */ /** * Page Types Manager * Handles page classification and rendering strategies */ class PageTypesManager { constructor(vsv, options = {}) { this.vsv = vsv; this.options = { pagesDir: options.pagesDir || 'pages', cacheDir: options.cacheDir || '.veko/pages', staticDir: options.staticDir || '.veko/static', ...options }; // Page registry this.pages = new Map(); this.staticCache = new Map(); this.ssgCache = new Map(); this.ultraBuildQueue = new Map(); // Build stats this.stats = { static: 0, ssg: 0, dynamic: 0, ultra: 0, totalBuildTime: 0 }; } /** * Detect page type from file conventions * - *.static.jsv / *.static.tsv -> Static * - *.ssg.jsv / *.ssg.tsv -> SSG * - *.dynamic.jsv / *.dynamic.tsv -> Dynamic * - *.ultra.jsv / *.ultra.tsv -> Ultra Dynamic * - Default based on exports (getStaticProps = SSG, getServerProps = Dynamic) */ detectPageType(filePath, content = null) { const basename = path.basename(filePath); // Check file naming convention if (basename.includes('.static.')) return PageTypes.STATIC; if (basename.includes('.ssg.')) return PageTypes.SSG; if (basename.includes('.dynamic.')) return PageTypes.DYNAMIC; if (basename.includes('.ultra.')) return PageTypes.ULTRA; // Check content for function exports if (content) { if (/export\s+(async\s+)?function\s+getStaticProps/.test(content)) { return PageTypes.SSG; } if (/export\s+(async\s+)?function\s+getServerProps/.test(content)) { return PageTypes.DYNAMIC; } if (/export\s+(async\s+)?function\s+getUltraConfig/.test(content)) { return PageTypes.ULTRA; } } // Default to static return PageTypes.STATIC; } /** * Register a page with its type */ registerPage(routePath, filePath, type = null) { const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null; const detectedType = type || this.detectPageType(filePath, content); const pageMeta = { type: detectedType, path: routePath, file: filePath, component: this.getComponentName(filePath), hash: content ? this.hashContent(content) : null, registeredAt: Date.now() }; this.pages.set(routePath, pageMeta); this.stats[detectedType]++; return pageMeta; } /** * Get component name from file path */ getComponentName(filePath) { const basename = path.basename(filePath); // Remove type suffix and extension return basename .replace(/\.(static|ssg|dynamic|ultra)/, '') .replace(/\.(jsv|tsv|jsx|tsx)$/, ''); } /** * Hash content for cache invalidation */ hashContent(content) { return crypto.createHash('md5').update(content).digest('hex').slice(0, 8); } /** * Build static page at build time */ async buildStaticPage(pageMeta, props = {}) { const startTime = Date.now(); try { const { html, assets } = await this.vsv.renderPage(pageMeta.file, props); // Store in static cache this.staticCache.set(pageMeta.path, { html, assets, builtAt: Date.now(), hash: pageMeta.hash }); // Write to static directory const staticPath = path.join(process.cwd(), this.options.staticDir, pageMeta.path, 'index.html'); const staticDir = path.dirname(staticPath); if (!fs.existsSync(staticDir)) { fs.mkdirSync(staticDir, { recursive: true }); } fs.writeFileSync(staticPath, html); this.stats.totalBuildTime += Date.now() - startTime; return { success: true, html, buildTime: Date.now() - startTime }; } catch (error) { return { success: false, error: error.message }; } } /** * Build SSG page with getStaticProps */ async buildSSGPage(pageMeta, params = {}) { const startTime = Date.now(); try { // Load component and execute getStaticProps const component = await this.vsv.loadComponent(pageMeta.file); let props = {}; if (component.getStaticProps) { const result = await component.getStaticProps({ params }); props = result.props || result; // Handle revalidate for ISR (Incremental Static Regeneration) if (result.revalidate) { pageMeta.revalidate = result.revalidate; } } const { html, assets } = await this.vsv.renderPage(pageMeta.file, props); // Store in SSG cache this.ssgCache.set(pageMeta.path, { html, props, assets, builtAt: Date.now(), hash: pageMeta.hash, revalidate: pageMeta.revalidate }); this.stats.totalBuildTime += Date.now() - startTime; return { success: true, html, props, buildTime: Date.now() - startTime }; } catch (error) { return { success: false, error: error.message }; } } /** * Render dynamic page on request */ async renderDynamicPage(pageMeta, req) { const startTime = Date.now(); try { const component = await this.vsv.loadComponent(pageMeta.file); let props = {}; if (component.getServerProps) { props = await component.getServerProps({ req, params: req.params || {}, query: req.query || {} }); } const { html } = await this.vsv.renderPage(pageMeta.file, props); return { success: true, html, renderTime: Date.now() - startTime, cached: false }; } catch (error) { return { success: false, error: error.message }; } } /** * Handle ultra-dynamic page with lazy building * Only builds components that are actually needed */ async handleUltraPage(pageMeta, req, ultraConfig) { const startTime = Date.now(); const cacheKey = this.getUltraCacheKey(pageMeta, req, ultraConfig); // Check if already built if (this.ultraBuildQueue.has(cacheKey)) { const cached = this.ultraBuildQueue.get(cacheKey); if (this.isUltraCacheValid(cached, ultraConfig)) { return { success: true, html: cached.html, cached: true }; } } try { const component = await this.vsv.loadComponent(pageMeta.file); // Get ultra config from component let config = ultraConfig || {}; if (component.getUltraConfig) { config = await component.getUltraConfig({ req }); } // Analyze which components are needed const neededComponents = await this.analyzeNeededComponents(pageMeta, config); // Build only needed components const { html, bundledComponents } = await this.vsv.renderUltraPage( pageMeta.file, config.props || {}, neededComponents ); // Cache the result this.ultraBuildQueue.set(cacheKey, { html, bundledComponents, builtAt: Date.now(), config }); return { success: true, html, bundledComponents: bundledComponents.length, buildTime: Date.now() - startTime, cached: false }; } catch (error) { return { success: false, error: error.message }; } } /** * Generate cache key for ultra pages */ getUltraCacheKey(pageMeta, req, config) { const baseKey = pageMeta.path; const configKey = config.cacheKey || ''; const themeKey = config.theme || ''; const moduleKey = (config.modules || []).sort().join(','); return `${baseKey}:${themeKey}:${moduleKey}:${configKey}`; } /** * Check if ultra cache is still valid */ isUltraCacheValid(cached, config) { if (!cached) return false; const maxAge = config.maxAge || 60000; // Default 1 minute return Date.now() - cached.builtAt < maxAge; } /** * Analyze which components are actually needed for ultra page * Based on config, theme, and modules */ async analyzeNeededComponents(pageMeta, config) { const needed = new Set(); // Add main component needed.add(pageMeta.component); // Add theme components if specified if (config.theme) { const themeComponents = await this.getThemeComponents(config.theme); themeComponents.forEach(c => needed.add(c)); } // Add module components if (config.modules) { for (const moduleName of config.modules) { const moduleComponents = await this.getModuleComponents(moduleName); moduleComponents.forEach(c => needed.add(c)); } } // Add explicitly included components if (config.include) { config.include.forEach(c => needed.add(c)); } // Remove explicitly excluded components if (config.exclude) { config.exclude.forEach(c => needed.delete(c)); } return Array.from(needed); } /** * Get theme components */ async getThemeComponents(themeName) { const themePath = path.join(process.cwd(), 'themes', themeName); if (!fs.existsSync(themePath)) return []; const components = []; const files = this.scanDirectory(themePath, ['.jsv', '.tsv']); for (const file of files) { components.push(this.getComponentName(file)); } return components; } /** * Get module components */ async getModuleComponents(moduleName) { const modulePath = path.join(process.cwd(), 'modules', moduleName); if (!fs.existsSync(modulePath)) return []; const components = []; const files = this.scanDirectory(modulePath, ['.jsv', '.tsv']); for (const file of files) { components.push(this.getComponentName(file)); } return components; } /** * Scan directory for files with given extensions */ scanDirectory(dir, extensions) { const results = []; if (!fs.existsSync(dir)) return results; const items = fs.readdirSync(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { results.push(...this.scanDirectory(fullPath, extensions)); } else if (extensions.some(ext => item.name.endsWith(ext))) { results.push(fullPath); } } return results; } /** * Get build summary */ getBuildSummary() { const summary = { pages: { total: this.pages.size, byType: { [PageTypes.STATIC]: this.stats.static, [PageTypes.SSG]: this.stats.ssg, [PageTypes.DYNAMIC]: this.stats.dynamic, [PageTypes.ULTRA]: this.stats.ultra } }, cache: { static: this.staticCache.size, ssg: this.ssgCache.size, ultra: this.ultraBuildQueue.size }, totalBuildTime: this.stats.totalBuildTime }; return summary; } /** * Print build summary with symbols */ printBuildSummary() { console.log('\n Build Summary:'); console.log(' ─────────────────────────────────'); for (const [routePath, meta] of this.pages) { const symbol = PageSymbols[meta.type]; const typeName = meta.type.toUpperCase().padEnd(7); console.log(` ${symbol} ${typeName} ${routePath}`); } console.log(' ─────────────────────────────────'); console.log(` ${PageSymbols.static} Static: ${this.stats.static}`); console.log(` ${PageSymbols.ssg} SSG: ${this.stats.ssg}`); console.log(` ${PageSymbols.dynamic} Dynamic: ${this.stats.dynamic}`); console.log(` ${PageSymbols.ultra} Ultra: ${this.stats.ultra}`); console.log(` Total: ${this.pages.size} pages`); console.log(` Build time: ${this.stats.totalBuildTime}ms\n`); } } module.exports = { PageTypes, PageSymbols, PageTypesManager };