UNPKG

@razen-core/zenweb

Version:

A minimalist TypeScript framework for building reactive web applications with no virtual DOM

396 lines (394 loc) 15.4 kB
/** * ZenWeb Builder - Extended * Handles compilation and bundling with TypeScript support * Supports both simple and advanced project structures */ import * as esbuild from 'esbuild'; import * as fs from 'fs'; import * as path from 'path'; import { parseZenWebFile, transformImports } from './parser.js'; import { logger } from './logger.js'; import { scanRoutes, generateRouteManifest, generateRouterConfig } from './route-scanner.js'; /** * Copy files from public directory to dist */ function copyPublicFiles(sourceDir, destDir) { const files = fs.readdirSync(sourceDir); for (const file of files) { const sourcePath = path.join(sourceDir, file); const destPath = path.join(destDir, file); const stat = fs.statSync(sourcePath); if (stat.isDirectory()) { // Recursively copy directories if (!fs.existsSync(destPath)) { fs.mkdirSync(destPath, { recursive: true }); } copyPublicFiles(sourcePath, destPath); } else { // Copy files (skip styles.css as it's handled separately) if (file !== 'styles.css') { fs.copyFileSync(sourcePath, destPath); logger.debug(`Copied: ${file}`); } } } } /** * Build components in src/components directory */ async function buildComponents(projectRoot, distDir, minify, sourceMaps) { const componentsDir = path.join(projectRoot, 'src', 'components'); if (!fs.existsSync(componentsDir)) { logger.debug('No components directory found'); return; } const componentFiles = fs.readdirSync(componentsDir).filter(f => (f.endsWith('.ts') || f.endsWith('.tsx')) && !f.endsWith('.d.ts')); if (componentFiles.length === 0) { logger.debug('No component files to build'); return; } logger.info(`Building ${componentFiles.length} components`); const distComponentsDir = path.join(distDir, 'components'); if (!fs.existsSync(distComponentsDir)) { fs.mkdirSync(distComponentsDir, { recursive: true }); } for (const file of componentFiles) { const componentPath = path.join(componentsDir, file); const componentName = path.basename(file, path.extname(file)); const outputPath = path.join(distComponentsDir, `${componentName}.bundel.js`); logger.debug(`Building component: ${componentName}`); try { await esbuild.build({ entryPoints: [componentPath], bundle: true, outfile: outputPath, format: 'esm', platform: 'browser', target: 'es2020', minify, sourcemap: sourceMaps, external: [], write: true, absWorkingDir: projectRoot }); logger.success(`Built component: ${componentName}.bundel.js`); } catch (error) { logger.error(`Failed to build component ${componentName}`, error); } } } /** * Generate HTML file for a page */ function generatePageHTML(pageName, distPageDir) { const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${pageName.charAt(0).toUpperCase() + pageName.slice(1)} - ZenWeb</title> <link rel="stylesheet" href="styles.css"> </head> <body> <div id="root"></div> <script type="module" src="bundel.js"></script> </body> </html> `; const htmlPath = path.join(distPageDir, 'page.html'); fs.writeFileSync(htmlPath, html, 'utf8'); logger.debug(`Generated HTML for page: ${pageName}`); } /** * Generate CSS file for a page */ function generatePageCSS(distPageDir) { const css = `/* Page Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } `; const cssPath = path.join(distPageDir, 'styles.css'); fs.writeFileSync(cssPath, css, 'utf8'); logger.debug(`Generated CSS for page`); } /** * Build pages in src/pages directory */ async function buildPages(projectRoot, distDir, minify, sourceMaps) { const pagesDir = path.join(projectRoot, 'src', 'pages'); if (!fs.existsSync(pagesDir)) { logger.debug('No pages directory found'); return; } const pageDirectories = fs.readdirSync(pagesDir).filter(f => { const fullPath = path.join(pagesDir, f); return fs.statSync(fullPath).isDirectory(); }); if (pageDirectories.length === 0) { logger.debug('No page directories to build'); return; } logger.info(`Building ${pageDirectories.length} pages`); for (const pageDir of pageDirectories) { const pagePath = path.join(pagesDir, pageDir); const pageFile = path.join(pagePath, 'page.ts'); if (!fs.existsSync(pageFile)) { logger.warning(`No page.ts found in ${pageDir}`); continue; } const distPageDir = path.join(distDir, 'pages', pageDir); if (!fs.existsSync(distPageDir)) { fs.mkdirSync(distPageDir, { recursive: true }); } // Build page TypeScript to bundel.js const outputPath = path.join(distPageDir, 'bundel.js'); logger.debug(`Building page: ${pageDir}`); try { await esbuild.build({ entryPoints: [pageFile], bundle: true, outfile: outputPath, format: 'esm', platform: 'browser', target: 'es2020', minify, sourcemap: sourceMaps, external: [], write: true, absWorkingDir: projectRoot }); // Generate page.html generatePageHTML(pageDir, distPageDir); // Generate styles.css generatePageCSS(distPageDir); logger.success(`Built page: ${pageDir}`); } catch (error) { logger.error(`Failed to build page ${pageDir}`, error); } } } /** * Build main entry point (for simple projects) */ async function buildMainEntry(projectRoot, options, allStyles) { const isDebug = process.argv.includes('--debug'); const distDir = path.join(projectRoot, path.dirname(options.output)); // Create esbuild plugin for ZenWeb transformation const zenwebPlugin = { name: 'zenweb-transform', setup(build) { build.onLoad({ filter: /\.(ts|js|tsx|jsx)$/ }, async (args) => { logger.debug(`Processing file: ${args.path}`); const source = await fs.promises.readFile(args.path, 'utf8'); // Check if file contains view or style keywords if (source.includes('view {') || source.includes('style {')) { logger.debug(`Found view/style keywords in: ${args.path}`); const parsed = parseZenWebFile(source); if (parsed.styles) { logger.debug(`Extracted ${parsed.styles.length} chars of styles from: ${args.path}`); allStyles += parsed.styles; } let transformedCode = parsed.code; transformedCode = transformImports(transformedCode); logger.debug(`Transformed file: ${args.path}`); return { contents: transformedCode, loader: args.path.endsWith('.ts') || args.path.endsWith('.tsx') ? 'ts' : 'js' }; } // Transform imports even if no view/style logger.debug(`No view/style keywords in: ${args.path}`); const transformedCode = transformImports(source); return { contents: transformedCode, loader: args.path.endsWith('.ts') || args.path.endsWith('.tsx') ? 'ts' : 'js' }; }); } }; logger.debug(`Building main entry: ${options.entry}`); // Build with esbuild const result = await esbuild.build({ entryPoints: [path.join(projectRoot, options.entry)], bundle: true, outfile: path.join(projectRoot, options.output), format: 'esm', platform: 'browser', target: 'es2020', minify: options.minify, sourcemap: options.sourceMaps, plugins: [zenwebPlugin], external: [], write: true, logLevel: isDebug ? 'debug' : 'warning', absWorkingDir: projectRoot }); logger.debug(`esbuild completed successfully`); logger.debug(`Output files: ${result.outputFiles?.length || 'written to disk'}`); // Handle styles.css const publicStylesPath = path.join(projectRoot, 'public', 'styles.css'); const distStylesPath = path.join(distDir, 'styles.css'); if (allStyles) { // If we extracted styles from components, write them logger.debug(`Writing ${allStyles.length} chars of extracted CSS to ${distStylesPath}`); await fs.promises.writeFile(distStylesPath, allStyles, 'utf8'); logger.success('Component styles written to dist/styles.css'); } else if (!fs.existsSync(distStylesPath)) { // If no extracted styles and no styles.css exists, create empty one logger.debug('No styles found, creating default styles.css'); const defaultStyles = `/* ZenWeb Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } `; await fs.promises.writeFile(distStylesPath, defaultStyles, 'utf8'); logger.success('Created default styles.css'); } logger.success(`Build complete: ${options.output}`); if (result.warnings.length > 0) { logger.warning(`Build warnings: ${result.warnings.length} warning(s)`); result.warnings.forEach(w => { logger.debug(`Warning: ${w.text}`); if (w.location) { logger.debug(` at ${w.location.file}:${w.location.line}:${w.location.column}`); } }); } if (result.errors.length > 0) { logger.error(`Build errors: ${result.errors.length} error(s)`); result.errors.forEach(e => { logger.error(`Error: ${e.text}`); if (e.location) { logger.error(` at ${e.location.file}:${e.location.line}:${e.location.column}`); } }); } } /** * Build a ZenWeb project * Supports both simple and advanced project structures */ export async function build(options) { const isDebug = process.argv.includes('--debug'); if (isDebug) { logger.setDebug(true); } logger.info('Building ZenWeb project'); logger.debug(`Build options: ${JSON.stringify(options)}`); const projectRoot = process.cwd(); const srcDir = path.join(projectRoot, 'src'); const distDir = path.join(projectRoot, path.dirname(options.output)); logger.debug(`Project root: ${projectRoot}`); logger.debug(`Source directory: ${srcDir}`); logger.debug(`Output directory: ${distDir}`); // Ensure dist directory exists if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir, { recursive: true }); } // Collect all styles let allStyles = ''; try { // Scan and generate routes if file-based routing is enabled if (options.config?.routing?.fileBasedRouting) { const pagesDir = path.join(projectRoot, options.config.routing.pagesDir || 'src/pages'); if (fs.existsSync(pagesDir)) { logger.info('Scanning file-based routes...'); const routeManifest = scanRoutes(pagesDir); // Generate route manifest const manifestPath = path.join(distDir, 'route-manifest.js'); generateRouteManifest(routeManifest, manifestPath); // Generate router config const routerConfigPath = path.join(distDir, 'router-config.js'); generateRouterConfig(routeManifest, routerConfigPath); logger.success(`Generated routes: ${routeManifest.routes.length} routes found`); } } // Check if this is an advanced project structure (has components or pages) const hasComponents = fs.existsSync(path.join(srcDir, 'components')); const hasPages = fs.existsSync(path.join(srcDir, 'pages')); if (hasComponents || hasPages) { logger.info('Detected advanced project structure'); // Build components if (hasComponents) { await buildComponents(projectRoot, distDir, options.minify, options.sourceMaps); } // Build pages if (hasPages) { await buildPages(projectRoot, distDir, options.minify, options.sourceMaps); } } // Always build main entry point await buildMainEntry(projectRoot, options, allStyles); // Copy public files to dist const publicDir = path.join(projectRoot, 'public'); if (fs.existsSync(publicDir)) { logger.debug(`Copying public files from ${publicDir} to ${distDir}`); copyPublicFiles(publicDir, distDir); logger.success('Public files copied to dist'); } } catch (error) { logger.error('Build failed', error); logger.debug(`Error details: ${JSON.stringify(error, null, 2)}`); throw error; } } /** * Watch mode for development */ export async function watch(options) { logger.info('Watching for changes'); // Use esbuild's watch mode const projectRoot = process.cwd(); const zenwebPlugin = { name: 'zenweb-transform', setup(build) { build.onLoad({ filter: /\.(ts|js|tsx|jsx)$/ }, async (args) => { const source = await fs.promises.readFile(args.path, 'utf8'); if (source.includes('view {') || source.includes('style {')) { const parsed = parseZenWebFile(source); let transformedCode = parsed.code; transformedCode = transformImports(transformedCode); return { contents: transformedCode, loader: args.path.endsWith('.ts') || args.path.endsWith('.tsx') ? 'ts' : 'js' }; } const transformedCode = transformImports(source); return { contents: transformedCode, loader: args.path.endsWith('.ts') || args.path.endsWith('.tsx') ? 'ts' : 'js' }; }); } }; const ctx = await esbuild.context({ entryPoints: [path.join(projectRoot, options.entry)], bundle: true, outfile: path.join(projectRoot, options.output), format: 'esm', platform: 'browser', target: 'es2020', minify: false, sourcemap: true, plugins: [zenwebPlugin] }); await ctx.watch(); logger.success('Watch mode enabled'); } //# sourceMappingURL=builder.js.map