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