veko
Version:
Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR
480 lines (403 loc) • 13.8 kB
JavaScript
/**
* 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
};