@shohaghinfo/aerojs 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,544 @@
1
1
  /**
2
2
  * @file fullstack-templates.ts
3
- * @description Canonical fullstack application templates matching my-aero-app.
3
+ * @description Canonical fullstack application templates for AeroJS.
4
4
  */
5
- export const engineTsContent = "import { Edge } from 'edge.js';\nimport ejs from 'ejs';\nimport { join } from 'node:path';\n\n// Initialize Edge.js instance and mount views directory\nconst edge = Edge.create();\nedge.mount(join(process.cwd(), 'views'));\n\n/**\n * Render template with AdonisJS's Edge template engine\n */\nexport async function renderEdge(templateName: string, data: Record<string, unknown> = {}): Promise<string> {\n return edge.render(templateName, data);\n}\n\n/**\n * Render template with EJS template engine\n */\nexport async function renderEjs(templateRelPath: string, data: Record<string, unknown> = {}): Promise<string> {\n const fullPath = join(process.cwd(), 'views', templateRelPath);\n return ejs.renderFile(fullPath, data);\n}\n";
6
- export const shellsTsContent = "import type { InertiaPage } from 'aerojs';\n\nconst sharedStyles = `\n :root {\n --bg-base: #060913;\n --bg-card: #0d1322;\n --bg-card-hover: #131c31;\n --border-subtle: #1e293b;\n --text-main: #f8fafc;\n --text-muted: #94a3b8;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n html {\n overflow-y: scroll;\n scroll-behavior: smooth;\n }\n body {\n font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n background: var(--bg-base);\n color: var(--text-main);\n min-height: 100vh;\n line-height: 1.6;\n padding-bottom: 4rem;\n -webkit-font-smoothing: antialiased;\n }\n .ambient-glow {\n position: fixed;\n top: -220px;\n left: 50%;\n transform: translateX(-50%);\n width: 900px;\n height: 480px;\n pointer-events: none;\n z-index: 0;\n }\n .container {\n max-width: 1180px;\n margin: 0 auto;\n padding: 0 1.5rem;\n position: relative;\n z-index: 1;\n }\n nav {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem 0;\n border-bottom: 1px solid rgba(30, 41, 59, 0.6);\n margin-bottom: 2.5rem;\n }\n .nav-brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n text-decoration: none;\n font-size: 1.25rem;\n font-weight: 800;\n color: #fff;\n }\n .brand-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.75rem;\n border-radius: 9999px;\n font-size: 0.75rem;\n font-weight: 600;\n }\n .engine-switcher {\n display: flex;\n gap: 0.5rem;\n background: #0a0f1d;\n padding: 0.3rem;\n border-radius: 0.75rem;\n border: 1px solid var(--border-subtle);\n }\n .engine-tab {\n padding: 0.45rem 0.95rem;\n border-radius: 0.5rem;\n font-size: 0.85rem;\n font-weight: 600;\n text-decoration: none;\n color: #94a3b8;\n transition: all 0.2s ease;\n }\n .engine-tab:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.05);\n }\n .engine-tab.active {\n color: #fff;\n }\n .header-banner {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n flex-wrap: wrap;\n gap: 1.5rem;\n margin-bottom: 2.5rem;\n }\n .header-info h1 {\n font-size: 2.4rem;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1.2;\n margin-bottom: 0.5rem;\n }\n .header-info p {\n color: var(--text-muted);\n font-size: 1.05rem;\n }\n .db-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.6rem 1.2rem;\n border-radius: 0.75rem;\n background: rgba(16, 185, 129, 0.1);\n border: 1px solid rgba(16, 185, 129, 0.25);\n color: #34d399;\n font-size: 0.85rem;\n font-weight: 600;\n font-family: 'JetBrains Mono', monospace;\n }\n .dot-live {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #10b981;\n box-shadow: 0 0 10px #10b981;\n }\n /* Controls bar */\n .controls-bar {\n display: flex;\n gap: 1rem;\n align-items: center;\n flex-wrap: wrap;\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n padding: 1rem 1.25rem;\n border-radius: 0.85rem;\n margin-bottom: 2rem;\n }\n .search-input {\n flex: 1;\n min-width: 240px;\n background: #060913;\n border: 1px solid #1e293b;\n border-radius: 0.5rem;\n padding: 0.6rem 1rem;\n color: #fff;\n font-size: 0.9rem;\n outline: none;\n transition: border-color 0.2s;\n }\n .search-input:focus {\n border-color: #38bdf8;\n }\n .category-pills {\n display: flex;\n gap: 0.4rem;\n flex-wrap: wrap;\n }\n .pill-btn {\n background: #090e1c;\n border: 1px solid #1e293b;\n color: #94a3b8;\n padding: 0.4rem 0.85rem;\n border-radius: 0.45rem;\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .pill-btn:hover {\n color: #fff;\n border-color: #334155;\n }\n .pill-btn.active {\n color: #fff;\n border-color: transparent;\n }\n .sort-select {\n background: #060913;\n border: 1px solid #1e293b;\n border-radius: 0.5rem;\n padding: 0.6rem 1rem;\n color: #e2e8f0;\n font-size: 0.85rem;\n outline: none;\n }\n /* Cart Pill */\n .cart-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid var(--border-subtle);\n padding: 0.6rem 1.2rem;\n border-radius: 0.75rem;\n font-size: 0.9rem;\n color: #fff;\n }\n /* Products Grid - Stable height to prevent layout jumps */\n .products-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n gap: 1.5rem;\n min-height: 520px;\n align-content: start;\n }\n .product-card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.6rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;\n position: relative;\n overflow: hidden;\n min-height: 290px;\n }\n .product-card:hover {\n transform: translateY(-3px);\n background: var(--bg-card-hover);\n box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);\n }\n .card-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 1rem;\n }\n .cat-tag {\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 0.25rem 0.65rem;\n border-radius: 6px;\n }\n .stock-ok {\n font-size: 0.75rem;\n font-weight: 600;\n padding: 0.2rem 0.6rem;\n border-radius: 9999px;\n background: rgba(16, 185, 129, 0.12);\n color: #34d399;\n border: 1px solid rgba(16, 185, 129, 0.25);\n }\n .stock-low {\n font-size: 0.75rem;\n font-weight: 600;\n padding: 0.2rem 0.6rem;\n border-radius: 9999px;\n background: rgba(245, 158, 11, 0.12);\n color: #fbbf24;\n border: 1px solid rgba(245, 158, 11, 0.25);\n }\n .product-name {\n font-size: 1.2rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 0.5rem;\n line-height: 1.35;\n }\n .product-desc {\n color: var(--text-muted);\n font-size: 0.9rem;\n line-height: 1.6;\n margin-bottom: 1.5rem;\n flex-grow: 1;\n }\n .card-bottom {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-top: 1rem;\n border-top: 1px solid rgba(30, 41, 59, 0.6);\n }\n .price-tag {\n font-size: 1.5rem;\n font-weight: 800;\n color: #fff;\n font-family: 'JetBrains Mono', monospace;\n }\n .actions {\n display: flex;\n gap: 0.5rem;\n }\n .btn-details {\n background: #1e293b;\n color: #cbd5e1;\n border: 1px solid #334155;\n padding: 0.55rem 0.9rem;\n border-radius: 0.5rem;\n font-size: 0.85rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-details:hover {\n color: #fff;\n background: #334155;\n }\n .btn-add {\n color: #fff;\n border: none;\n padding: 0.55rem 1.1rem;\n border-radius: 0.5rem;\n font-size: 0.85rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-add:hover {\n transform: translateY(-1px);\n }\n /* Modal */\n .modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1.5rem;\n }\n .modal-content {\n background: #0d1322;\n border: 1px solid #334155;\n border-radius: 1rem;\n padding: 2rem;\n max-width: 520px;\n width: 100%;\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8);\n }\n .modal-content h2 {\n font-size: 1.5rem;\n font-weight: 700;\n margin-bottom: 0.5rem;\n }\n .modal-cat {\n color: #38bdf8;\n font-size: 0.85rem;\n font-weight: 600;\n margin-bottom: 1rem;\n }\n .modal-desc {\n color: #94a3b8;\n margin-bottom: 1.5rem;\n line-height: 1.7;\n }\n .modal-price {\n font-size: 2rem;\n font-weight: 800;\n font-family: 'JetBrains Mono', monospace;\n color: #fff;\n margin-bottom: 1.5rem;\n }\n .modal-actions {\n display: flex;\n justify-content: flex-end;\n gap: 0.75rem;\n }\n .btn-close {\n background: #1e293b;\n color: #cbd5e1;\n border: 1px solid #334155;\n padding: 0.6rem 1.2rem;\n border-radius: 0.5rem;\n font-size: 0.9rem;\n cursor: pointer;\n }\n .syntax-box {\n margin-top: 3.5rem;\n background: #080d1a;\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.75rem;\n }\n pre {\n background: #040711;\n border: 1px solid #1e293b;\n border-radius: 0.5rem;\n padding: 1rem;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.82rem;\n color: #e2e8f0;\n overflow-x: auto;\n }\n`;\n\n/**\n * Pre-renders the exact initial catalog HTML directly on the server.\n * This completely eliminates initial layout jumps and blank/loading flashes.\n */\nfunction renderInitialCatalogHtml(\n engineBadgeText: string,\n badgeClass: string,\n engineHeading: string,\n engineSubheading: string,\n products: any[]\n): string {\n const categories = ['All', ...new Set(products.map((p) => p.category))];\n const categoryPillsHtml = categories\n .map(\n (cat, idx) =>\n `<button class=\"pill-btn${idx === 0 ? ' active' : ''}\">${cat}</button>`\n )\n .join('');\n\n const productCardsHtml = products\n .map((p) => {\n const stockBadge =\n p.stock > 10\n ? `<span class=\"stock-ok\">In Stock (${p.stock})</span>`\n : `<span class=\"stock-low\">Low Stock (${p.stock})</span>`;\n return `\n <div class=\"product-card\">\n <div>\n <div class=\"card-top\">\n <span class=\"cat-tag\">${p.category}</span>\n ${stockBadge}\n </div>\n <div style=\"color: #fbbf24; font-size: 0.85rem; margin-bottom: 0.3rem;\">\n ★★★★★ (${p.rating}.0)\n </div>\n <h3 class=\"product-name\">${p.name}</h3>\n <p class=\"product-desc\">${p.description}</p>\n </div>\n <div class=\"card-bottom\">\n <div class=\"price-tag\">$${p.price}</div>\n <div class=\"actions\">\n <button class=\"btn-details\">Quick View</button>\n <button class=\"btn-add\">+ Add to Cart</button>\n </div>\n </div>\n </div>\n `;\n })\n .join('');\n\n return `\n <div>\n <div class=\"header-banner\">\n <div class=\"header-info\">\n <span class=\"${badgeClass}\">${engineBadgeText}</span>\n <h1>${engineHeading}</h1>\n <p>${engineSubheading}</p>\n </div>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div class=\"cart-pill\">\n 🛒 Cart: <strong>0 items</strong> ($0)\n </div>\n <div class=\"db-badge\">\n <span class=\"dot-live\"></span>\n MySQL 8 • aerojs_app\n </div>\n </div>\n </div>\n\n <div class=\"controls-bar\">\n <input\n type=\"text\"\n placeholder=\"Search products in MySQL in real-time...\"\n class=\"search-input\"\n />\n <div class=\"category-pills\">\n ${categoryPillsHtml}\n </div>\n <select class=\"sort-select\">\n <option value=\"name\">Sort by Name</option>\n <option value=\"price-asc\">Price: Low to High</option>\n <option value=\"price-desc\">Price: High to Low</option>\n </select>\n </div>\n\n <div class=\"products-grid\">\n ${productCardsHtml}\n </div>\n </div>\n `;\n}\n\n/**\n * Returns React Inertia HTML Shell with pre-rendered initial server markup\n * and smooth, zero-layout-shift React 18 client hydration.\n */\nexport function getReactShell(page: InertiaPage): string {\n const pageJson = JSON.stringify(page).replace(/\"/g, '&quot;');\n const products = (page.props?.products as any[]) || [];\n const initialHtml = renderInitialCatalogHtml(\n '⚛️ Inertia.js + React 18 SPA',\n 'badge-react',\n 'React <span>Interactive Catalog</span>',\n 'Live client-side reactivity powered by Inertia protocol & MySQL database records.',\n products\n );\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>React 18 (Inertia.js) &bull; AeroJS + MySQL</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <link rel=\"modulepreload\" href=\"https://esm.sh/react@18.3.1\">\n <link rel=\"modulepreload\" href=\"https://esm.sh/react-dom@18.3.1/client\">\n <style>\n ${sharedStyles}\n .ambient-glow {\n background: radial-gradient(circle, rgba(56, 189, 248, 0.16) 0%, rgba(99, 102, 241, 0.08) 50%, transparent 70%);\n }\n .brand-pill {\n background: rgba(56, 189, 248, 0.15);\n color: #38bdf8;\n border: 1px solid rgba(56, 189, 248, 0.3);\n }\n .engine-tab.active {\n background: linear-gradient(135deg, #0284c7, #2563eb);\n box-shadow: 0 2px 10px rgba(2, 132, 199, 0.4);\n }\n .header-info h1 span {\n background: linear-gradient(135deg, #38bdf8, #818cf8);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .pill-btn.active {\n background: #0284c7;\n }\n .cat-tag {\n color: #38bdf8;\n background: rgba(56, 189, 248, 0.12);\n border: 1px solid rgba(56, 189, 248, 0.25);\n }\n .btn-add {\n background: linear-gradient(135deg, #0284c7, #2563eb);\n }\n .btn-add:hover {\n box-shadow: 0 4px 15px rgba(2, 132, 199, 0.4);\n }\n .badge-react {\n font-size: 0.8rem;\n font-weight: 700;\n color: #38bdf8;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n }\n </style>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"react\": \"https://esm.sh/react@18.3.1\",\n \"react-dom/client\": \"https://esm.sh/react-dom@18.3.1/client\"\n }\n }\n </script>\n</head>\n<body>\n <div class=\"ambient-glow\"></div>\n <div class=\"container\">\n <!-- Navbar -->\n <nav>\n <a href=\"/\" class=\"nav-brand\">\n <span>🚀 AeroJS</span>\n <span class=\"brand-pill\">Engine: React (Inertia.js)</span>\n </a>\n <div class=\"engine-switcher\">\n <a href=\"/views/edge\" class=\"engine-tab\">Edge.js</a>\n <a href=\"/views/ejs\" class=\"engine-tab\">EJS</a>\n <a href=\"/views/react\" class=\"engine-tab active\">React</a>\n <a href=\"/views/vue\" class=\"engine-tab\">Vue 3</a>\n </div>\n </nav>\n\n <!-- Inertia Mount Root with Pre-rendered HTML (Zero Layout Shift) -->\n <div id=\"app\" data-page=\"${pageJson}\">${initialHtml}</div>\n\n <!-- Inertia Explainer -->\n <div class=\"syntax-box\">\n <h4 style=\"color: #38bdf8; margin-bottom: 0.75rem; font-weight: 700;\">⚛️ Inertia.js Protocol Lifecycle in AeroJS</h4>\n <pre><code>// 1. Controller renders React component via Inertia adapter:\nawait ctx.inertia.render('ProductsReact', { products });\n\n// 2. Initial visit returns pre-rendered HTML shell with data-page JSON payload:\n&lt;div id=\"app\" data-page='{\"component\":\"ProductsReact\",\"props\":{\"products\":[...]}}'&gt;...&lt;/div&gt;\n\n// 3. Subsequent client visits with 'X-Inertia: true' header receive blazing-fast JSON:\n// AeroJS automatically skips HTML rendering and sends pure JSON props!</code></pre>\n </div>\n </div>\n\n <!-- Client-side React 18 Mounting Script -->\n <script type=\"module\">\n import React, { useState, useMemo } from 'react';\n import { createRoot } from 'react-dom/client';\n\n const rootElement = document.getElementById('app');\n const pageData = JSON.parse(rootElement.getAttribute('data-page') || '{}');\n const initialProducts = pageData.props?.products || [];\n\n function ReactCatalog({ products }) {\n const [search, setSearch] = useState('');\n const [selectedCat, setSelectedCat] = useState('All');\n const [sortBy, setSortBy] = useState('name');\n const [cart, setCart] = useState([]);\n const [activeModal, setActiveModal] = useState(null);\n\n const categories = useMemo(() => {\n return ['All', ...new Set(products.map(p => p.category))];\n }, [products]);\n\n const filtered = useMemo(() => {\n return products\n .filter(p => {\n const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase()) ||\n p.description.toLowerCase().includes(search.toLowerCase());\n const matchesCat = selectedCat === 'All' || p.category === selectedCat;\n return matchesSearch && matchesCat;\n })\n .sort((a, b) => {\n if (sortBy === 'price-asc') return a.price - b.price;\n if (sortBy === 'price-desc') return b.price - a.price;\n return a.name.localeCompare(b.name);\n });\n }, [products, search, selectedCat, sortBy]);\n\n const cartTotal = useMemo(() => cart.reduce((s, i) => s + i.price, 0), [cart]);\n\n const addToCart = (product) => {\n setCart(prev => [...prev, product]);\n };\n\n return React.createElement('div', null,\n // Header Banner\n React.createElement('div', { className: 'header-banner' },\n React.createElement('div', { className: 'header-info' },\n React.createElement('span', { className: 'badge-react' }, '⚛️ Inertia.js + React 18 SPA'),\n React.createElement('h1', null, 'React ', React.createElement('span', null, 'Interactive Catalog')),\n React.createElement('p', null, 'Live client-side reactivity powered by Inertia protocol & MySQL database records.')\n ),\n React.createElement('div', { style: { display: 'flex', gap: '1rem', alignItems: 'center' } },\n React.createElement('div', { className: 'cart-pill' },\n '🛒 Cart: ', React.createElement('strong', null, cart.length, ' items'), ' ($' + cartTotal + ')'\n ),\n React.createElement('div', { className: 'db-badge' },\n React.createElement('span', { className: 'dot-live' }),\n 'MySQL 8 \\\\u2022 aerojs_app'\n )\n )\n ),\n\n // Controls bar\n React.createElement('div', { className: 'controls-bar' },\n React.createElement('input', {\n type: 'text',\n placeholder: 'Search products in MySQL in real-time...',\n value: search,\n onChange: (e) => setSearch(e.target.value),\n className: 'search-input'\n }),\n React.createElement('div', { className: 'category-pills' },\n categories.map(cat => React.createElement('button', {\n key: cat,\n onClick: () => setSelectedCat(cat),\n className: 'pill-btn' + (selectedCat === cat ? ' active' : '')\n }, cat))\n ),\n React.createElement('select', {\n value: sortBy,\n onChange: (e) => setSortBy(e.target.value),\n className: 'sort-select'\n },\n React.createElement('option', { value: 'name' }, 'Sort by Name'),\n React.createElement('option', { value: 'price-asc' }, 'Price: Low to High'),\n React.createElement('option', { value: 'price-desc' }, 'Price: High to Low')\n )\n ),\n\n // Products Grid\n React.createElement('div', { className: 'products-grid' },\n filtered.length > 0\n ? filtered.map(product => React.createElement('div', { key: product.id, className: 'product-card' },\n React.createElement('div', null,\n React.createElement('div', { className: 'card-top' },\n React.createElement('span', { className: 'cat-tag' }, product.category),\n React.createElement('span', { className: product.stock > 10 ? 'stock-ok' : 'stock-low' },\n product.stock > 10 ? 'In Stock (' + product.stock + ')' : 'Low Stock (' + product.stock + ')'\n )\n ),\n React.createElement('div', { style: { color: '#fbbf24', fontSize: '0.85rem', marginBottom: '0.3rem' } },\n '★★★★★ (' + product.rating + '.0)'\n ),\n React.createElement('h3', { className: 'product-name' }, product.name),\n React.createElement('p', { className: 'product-desc' }, product.description)\n ),\n React.createElement('div', { className: 'card-bottom' },\n React.createElement('div', { className: 'price-tag' }, '$' + product.price),\n React.createElement('div', { className: 'actions' },\n React.createElement('button', {\n className: 'btn-details',\n onClick: () => setActiveModal(product)\n }, 'Quick View'),\n React.createElement('button', {\n className: 'btn-add',\n onClick: () => addToCart(product)\n }, '+ Add to Cart')\n )\n )\n ))\n : React.createElement('div', {\n style: { gridColumn: '1 / -1', textAlign: 'center', padding: '4rem', color: '#94a3b8' }\n }, 'No products matching \"' + search + '\"')\n ),\n\n // Modal\n activeModal && React.createElement('div', { className: 'modal-backdrop', onClick: () => setActiveModal(null) },\n React.createElement('div', { className: 'modal-content', onClick: (e) => e.stopPropagation() },\n React.createElement('h2', null, activeModal.name),\n React.createElement('p', { className: 'modal-cat' }, 'Category: ' + activeModal.category + ' \\\\u2022 Rating: ' + activeModal.rating + '/5'),\n React.createElement('p', { className: 'modal-desc' }, activeModal.description),\n React.createElement('div', { className: 'modal-price' }, '$' + activeModal.price),\n React.createElement('div', { className: 'modal-actions' },\n React.createElement('button', {\n className: 'btn-add',\n onClick: () => { addToCart(activeModal); setActiveModal(null); }\n }, 'Add to Cart'),\n React.createElement('button', {\n className: 'btn-close',\n onClick: () => setActiveModal(null)\n }, 'Close')\n )\n )\n )\n );\n }\n\n const root = createRoot(rootElement);\n root.render(React.createElement(ReactCatalog, { products: initialProducts }));\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Returns Vue 3 Inertia HTML Shell with pre-rendered initial server markup\n * and smooth, zero-layout-shift Vue 3 Composition API hydration.\n */\nexport function getVueShell(page: InertiaPage): string {\n const pageJson = JSON.stringify(page).replace(/\"/g, '&quot;');\n const products = (page.props?.products as any[]) || [];\n const initialHtml = renderInitialCatalogHtml(\n '💚 Inertia.js + Vue 3 (Composition API)',\n 'badge-vue',\n 'Vue 3 <span>Reactive Catalog</span>',\n 'Deep reactivity with ref() & computed() powered by Inertia protocol & MySQL database.',\n products\n );\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Vue 3 (Inertia.js) &bull; AeroJS + MySQL</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <link rel=\"modulepreload\" href=\"https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js\">\n <style>\n ${sharedStyles}\n .ambient-glow {\n background: radial-gradient(circle, rgba(16, 185, 129, 0.16) 0%, rgba(6, 182, 212, 0.08) 50%, transparent 70%);\n }\n .brand-pill {\n background: rgba(16, 185, 129, 0.15);\n color: #34d399;\n border: 1px solid rgba(16, 185, 129, 0.3);\n }\n .engine-tab.active {\n background: linear-gradient(135deg, #059669, #10b981);\n box-shadow: 0 2px 10px rgba(16, 185, 129, 0.4);\n }\n .header-info h1 span {\n background: linear-gradient(135deg, #34d399, #10b981);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .pill-btn.active {\n background: #059669;\n }\n .cat-tag {\n color: #34d399;\n background: rgba(16, 185, 129, 0.12);\n border: 1px solid rgba(16, 185, 129, 0.25);\n }\n .btn-add {\n background: linear-gradient(135deg, #059669, #10b981);\n }\n .btn-add:hover {\n box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);\n }\n .badge-vue {\n font-size: 0.8rem;\n font-weight: 700;\n color: #34d399;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n }\n </style>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"vue\": \"https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js\"\n }\n }\n </script>\n</head>\n<body>\n <div class=\"ambient-glow\"></div>\n <div class=\"container\">\n <!-- Navbar -->\n <nav>\n <a href=\"/\" class=\"nav-brand\">\n <span>🚀 AeroJS</span>\n <span class=\"brand-pill\">Engine: Vue 3 (Inertia.js)</span>\n </a>\n <div class=\"engine-switcher\">\n <a href=\"/views/edge\" class=\"engine-tab\">Edge.js</a>\n <a href=\"/views/ejs\" class=\"engine-tab\">EJS</a>\n <a href=\"/views/react\" class=\"engine-tab\">React</a>\n <a href=\"/views/vue\" class=\"engine-tab active\">Vue 3</a>\n </div>\n </nav>\n\n <!-- Inertia Mount Root with Pre-rendered HTML (Zero Layout Shift) -->\n <div id=\"app\" data-page=\"${pageJson}\">${initialHtml}</div>\n\n <!-- Inertia Explainer -->\n <div class=\"syntax-box\">\n <h4 style=\"color: #34d399; margin-bottom: 0.75rem; font-weight: 700;\">💚 Inertia.js Protocol Lifecycle in AeroJS</h4>\n <pre><code>// 1. Controller renders Vue 3 component via Inertia adapter:\nawait ctx.inertia.render('ProductsVue', { products });\n\n// 2. Initial visit returns pre-rendered HTML shell with data-page JSON payload:\n&lt;div id=\"app\" data-page='{\"component\":\"ProductsVue\",\"props\":{\"products\":[...]}}'&gt;...&lt;/div&gt;\n\n// 3. Subsequent client visits with 'X-Inertia: true' header receive blazing-fast JSON:\n// AeroJS automatically skips HTML rendering and sends pure JSON props!</code></pre>\n </div>\n </div>\n\n <!-- Client-side Vue 3 Mounting Script -->\n <script type=\"module\">\n import { createApp, ref, computed, h } from 'vue';\n\n const rootElement = document.getElementById('app');\n const pageData = JSON.parse(rootElement.getAttribute('data-page') || '{}');\n const initialProducts = pageData.props?.products || [];\n\n const App = {\n setup() {\n const products = ref(initialProducts);\n const search = ref('');\n const selectedCat = ref('All');\n const sortBy = ref('name');\n const cart = ref([]);\n const activeModal = ref(null);\n\n const categories = computed(() => {\n return ['All', ...new Set(products.value.map(p => p.category))];\n });\n\n const filtered = computed(() => {\n return products.value\n .filter(p => {\n const matchesSearch = p.name.toLowerCase().includes(search.value.toLowerCase()) ||\n p.description.toLowerCase().includes(search.value.toLowerCase());\n const matchesCat = selectedCat.value === 'All' || p.category === selectedCat.value;\n return matchesSearch && matchesCat;\n })\n .sort((a, b) => {\n if (sortBy.value === 'price-asc') return a.price - b.price;\n if (sortBy.value === 'price-desc') return b.price - a.price;\n return a.name.localeCompare(b.name);\n });\n });\n\n const cartTotal = computed(() => cart.value.reduce((s, i) => s + i.price, 0));\n\n const addToCart = (product) => {\n cart.value.push(product);\n };\n\n return () => h('div', [\n // Header Banner\n h('div', { class: 'header-banner' }, [\n h('div', { class: 'header-info' }, [\n h('span', { class: 'badge-vue' }, '💚 Inertia.js + Vue 3 (Composition API)'),\n h('h1', ['Vue 3 ', h('span', 'Reactive Catalog')]),\n h('p', 'Deep reactivity with ref() & computed() powered by Inertia protocol & MySQL database.')\n ]),\n h('div', { style: { display: 'flex', gap: '1rem', alignItems: 'center' } }, [\n h('div', { class: 'cart-pill' }, [\n '🛒 Cart: ', h('strong', cart.value.length + ' items'), ' ($' + cartTotal.value + ')'\n ]),\n h('div', { class: 'db-badge' }, [\n h('span', { class: 'dot-live' }),\n 'MySQL 8 • aerojs_app'\n ])\n ])\n ]),\n\n // Controls Bar\n h('div', { class: 'controls-bar' }, [\n h('input', {\n type: 'text',\n placeholder: 'Filter MySQL products in real time...',\n value: search.value,\n onInput: (e) => search.value = e.target.value,\n class: 'search-input'\n }),\n h('div', { class: 'category-pills' },\n categories.value.map(cat => h('button', {\n key: cat,\n onClick: () => selectedCat.value = cat,\n class: 'pill-btn' + (selectedCat.value === cat ? ' active' : '')\n }, cat))\n ),\n h('select', {\n value: sortBy.value,\n onChange: (e) => sortBy.value = e.target.value,\n class: 'sort-select'\n }, [\n h('option', { value: 'name' }, 'Sort by Name'),\n h('option', { value: 'price-asc' }, 'Price: Low to High'),\n h('option', { value: 'price-desc' }, 'Price: High to Low')\n ])\n ]),\n\n // Products Grid\n h('div', { class: 'products-grid' },\n filtered.value.length > 0\n ? filtered.value.map(product => h('div', { key: product.id, class: 'product-card' }, [\n h('div', [\n h('div', { class: 'card-top' }, [\n h('span', { class: 'cat-tag' }, product.category),\n h('span', { class: product.stock > 10 ? 'stock-ok' : 'stock-low' },\n product.stock > 10 ? ('In Stock (' + product.stock + ')') : ('Low Stock (' + product.stock + ')')\n )\n ]),\n h('div', { style: { color: '#fbbf24', fontSize: '0.85rem', marginBottom: '0.3rem' } },\n '★★★★★ (' + product.rating + '.0)'\n ),\n h('h3', { class: 'product-name' }, product.name),\n h('p', { class: 'product-desc' }, product.description)\n ]),\n h('div', { class: 'card-bottom' }, [\n h('div', { class: 'price-tag' }, '$' + product.price),\n h('div', { class: 'actions' }, [\n h('button', {\n class: 'btn-details',\n onClick: () => activeModal.value = product\n }, 'Quick View'),\n h('button', {\n class: 'btn-add',\n onClick: () => addToCart(product)\n }, '+ Add to Cart')\n ])\n ])\n ]))\n : [\n h('div', {\n style: { gridColumn: '1 / -1', textAlign: 'center', padding: '4rem', color: '#94a3b8' }\n }, 'No products matching \"' + search.value + '\"')\n ]\n ),\n\n // Modal\n activeModal.value ? h('div', {\n class: 'modal-backdrop',\n onClick: () => activeModal.value = null\n }, [\n h('div', {\n class: 'modal-content',\n onClick: (e) => e.stopPropagation()\n }, [\n h('h2', activeModal.value.name),\n h('p', { class: 'modal-cat' }, 'Category: ' + activeModal.value.category + ' • Rating: ' + activeModal.value.rating + '/5'),\n h('p', { class: 'modal-desc' }, activeModal.value.description),\n h('div', { class: 'modal-price' }, '$' + activeModal.value.price),\n h('div', { class: 'modal-actions' }, [\n h('button', {\n class: 'btn-add',\n onClick: () => { addToCart(activeModal.value); activeModal.value = null; }\n }, 'Add to Cart'),\n h('button', {\n class: 'btn-close',\n onClick: () => activeModal.value = null\n }, 'Close')\n ])\n ])\n ]) : null\n ]);\n }\n };\n\n createApp(App).mount('#app');\n </script>\n</body>\n</html>`;\n}\n";
7
- export const productsEdgeContent = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Edge.js View Engine &bull; AeroJS + MySQL</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>\n :root {\n --bg-base: #060913;\n --bg-card: #0d1322;\n --bg-card-hover: #131c31;\n --border-subtle: #1e293b;\n --border-highlight: #a855f7;\n --text-main: #f8fafc;\n --text-muted: #94a3b8;\n --brand-edge: #a855f7;\n --brand-edge-light: #c084fc;\n --brand-cyan: #38bdf8;\n --brand-emerald: #10b981;\n --brand-amber: #f59e0b;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n background: var(--bg-base);\n color: var(--text-main);\n min-height: 100vh;\n line-height: 1.6;\n padding-bottom: 4rem;\n }\n .ambient-glow {\n position: fixed;\n top: -220px;\n left: 50%;\n transform: translateX(-50%);\n width: 900px;\n height: 480px;\n background: radial-gradient(circle, rgba(168, 85, 247, 0.16) 0%, rgba(56, 189, 248, 0.08) 50%, transparent 70%);\n pointer-events: none;\n z-index: 0;\n }\n .container {\n max-width: 1180px;\n margin: 0 auto;\n padding: 0 1.5rem;\n position: relative;\n z-index: 1;\n }\n /* Top Navbar */\n nav {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem 0;\n border-bottom: 1px solid rgba(30, 41, 59, 0.6);\n margin-bottom: 2.5rem;\n }\n .nav-brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n text-decoration: none;\n font-size: 1.25rem;\n font-weight: 800;\n color: #fff;\n }\n .brand-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.75rem;\n border-radius: 9999px;\n font-size: 0.75rem;\n font-weight: 600;\n background: rgba(168, 85, 247, 0.15);\n color: var(--brand-edge-light);\n border: 1px solid rgba(168, 85, 247, 0.3);\n }\n .engine-switcher {\n display: flex;\n gap: 0.5rem;\n background: #0a0f1d;\n padding: 0.3rem;\n border-radius: 0.75rem;\n border: 1px solid var(--border-subtle);\n }\n .engine-tab {\n padding: 0.45rem 0.95rem;\n border-radius: 0.5rem;\n font-size: 0.85rem;\n font-weight: 600;\n text-decoration: none;\n color: #94a3b8;\n transition: all 0.2s;\n }\n .engine-tab:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.05);\n }\n .engine-tab.active {\n color: #fff;\n background: linear-gradient(135deg, #9333ea, #7e22ce);\n box-shadow: 0 2px 10px rgba(147, 51, 234, 0.4);\n }\n\n /* Page Header */\n .header-banner {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n flex-wrap: wrap;\n gap: 1.5rem;\n margin-bottom: 2.5rem;\n }\n .header-info h1 {\n font-size: 2.4rem;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1.2;\n margin-bottom: 0.5rem;\n }\n .header-info h1 span {\n background: linear-gradient(135deg, #c084fc, #e879f9);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .header-info p {\n color: var(--text-muted);\n font-size: 1.05rem;\n }\n .db-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.6rem 1.2rem;\n border-radius: 0.75rem;\n background: rgba(16, 185, 129, 0.1);\n border: 1px solid rgba(16, 185, 129, 0.25);\n color: #34d399;\n font-size: 0.85rem;\n font-weight: 600;\n font-family: 'JetBrains Mono', monospace;\n }\n .dot-live {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #10b981;\n box-shadow: 0 0 10px #10b981;\n }\n\n /* Metric stats */\n .stats-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 1rem;\n margin-bottom: 2.5rem;\n }\n .stat-card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 0.85rem;\n padding: 1.25rem;\n }\n .stat-label {\n color: var(--text-muted);\n font-size: 0.82rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 0.35rem;\n }\n .stat-val {\n font-size: 1.8rem;\n font-weight: 800;\n color: #fff;\n }\n\n /* Products Grid */\n .catalog-title {\n font-size: 1.35rem;\n font-weight: 700;\n letter-spacing: -0.02em;\n margin-bottom: 1.25rem;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n }\n .products-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n gap: 1.5rem;\n }\n .product-card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.6rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: all 0.25s ease;\n position: relative;\n overflow: hidden;\n }\n .product-card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: linear-gradient(90deg, #9333ea, #38bdf8);\n opacity: 0;\n transition: opacity 0.25s ease;\n }\n .product-card:hover {\n transform: translateY(-3px);\n border-color: rgba(168, 85, 247, 0.4);\n background: var(--bg-card-hover);\n box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);\n }\n .product-card:hover::before {\n opacity: 1;\n }\n .card-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 1rem;\n }\n .category-tag {\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--brand-edge-light);\n background: rgba(168, 85, 247, 0.12);\n border: 1px solid rgba(168, 85, 247, 0.25);\n padding: 0.25rem 0.65rem;\n border-radius: 6px;\n }\n .stock-badge {\n font-size: 0.75rem;\n font-weight: 600;\n padding: 0.2rem 0.6rem;\n border-radius: 9999px;\n }\n .stock-in {\n background: rgba(16, 185, 129, 0.12);\n color: #34d399;\n border: 1px solid rgba(16, 185, 129, 0.25);\n }\n .stock-low {\n background: rgba(245, 158, 11, 0.12);\n color: #fbbf24;\n border: 1px solid rgba(245, 158, 11, 0.25);\n }\n .product-name {\n font-size: 1.2rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 0.5rem;\n line-height: 1.35;\n }\n .product-desc {\n color: var(--text-muted);\n font-size: 0.9rem;\n line-height: 1.6;\n margin-bottom: 1.5rem;\n flex-grow: 1;\n }\n .card-bottom {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-top: 1rem;\n border-top: 1px solid rgba(30, 41, 59, 0.6);\n }\n .price-box {\n display: flex;\n flex-direction: column;\n }\n .price-label {\n font-size: 0.72rem;\n color: #64748b;\n text-transform: uppercase;\n font-weight: 600;\n }\n .price-val {\n font-size: 1.5rem;\n font-weight: 800;\n color: #fff;\n font-family: 'JetBrains Mono', monospace;\n }\n .rating-stars {\n color: #fbbf24;\n font-size: 0.85rem;\n margin-bottom: 0.3rem;\n }\n .btn-buy {\n background: linear-gradient(135deg, #9333ea, #7e22ce);\n color: #fff;\n border: none;\n padding: 0.6rem 1.2rem;\n border-radius: 0.6rem;\n font-size: 0.88rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-buy:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 15px rgba(147, 51, 234, 0.4);\n }\n\n /* Template Snippet Box */\n .syntax-box {\n margin-top: 3.5rem;\n background: #080d1a;\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.75rem;\n }\n .syntax-box h4 {\n font-size: 1rem;\n font-weight: 700;\n margin-bottom: 0.75rem;\n color: var(--brand-edge-light);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n pre {\n background: #040711;\n border: 1px solid #1e293b;\n border-radius: 0.5rem;\n padding: 1rem;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.82rem;\n color: #e2e8f0;\n overflow-x: auto;\n }\n </style>\n</head>\n<body>\n <div class=\"ambient-glow\"></div>\n <div class=\"container\">\n <!-- Navbar -->\n <nav>\n <a href=\"/\" class=\"nav-brand\">\n <span>🚀 AeroJS</span>\n <span class=\"brand-pill\">Engine: Edge.js</span>\n </a>\n <div class=\"engine-switcher\">\n <a href=\"/views/edge\" class=\"engine-tab active\">Edge.js</a>\n <a href=\"/views/ejs\" class=\"engine-tab\">EJS</a>\n <a href=\"/views/react\" class=\"engine-tab\">React</a>\n <a href=\"/views/vue\" class=\"engine-tab\">Vue 3</a>\n </div>\n </nav>\n\n <!-- Header Banner -->\n <div class=\"header-banner\">\n <div class=\"header-info\">\n <h1><span>Edge.js</span> Template Engine</h1>\n <p>Rendered on the server with official AdonisJS Edge templating syntax, fetching live from MySQL.</p>\n </div>\n <div class=\"db-badge\">\n <span class=\"dot-live\"></span>\n MySQL 8 &bull; aerojs_app.products\n </div>\n </div>\n\n <!-- Quick Stats -->\n <div class=\"stats-row\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Catalog Items</div>\n <div class=\"stat-val\">{{ products.length }}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Template Driver</div>\n <div class=\"stat-val\" style=\"font-size: 1.4rem; color: var(--brand-edge-light);\">@edge-js/edge</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Database Engine</div>\n <div class=\"stat-val\" style=\"font-size: 1.4rem; color: #38bdf8;\">MySQL2 + Knex</div>\n </div>\n </div>\n\n <!-- Products Catalog Section -->\n <div class=\"catalog-title\">\n <span>📦</span> Live Products from MySQL Database\n </div>\n\n <div class=\"products-grid\">\n @each(product in products)\n <div class=\"product-card\">\n <div>\n <div class=\"card-top\">\n <span class=\"category-tag\">{{ product.category }}</span>\n @if(product.stock > 10)\n <span class=\"stock-badge stock-in\">In Stock ({{ product.stock }})</span>\n @else\n <span class=\"stock-badge stock-low\">Low Stock ({{ product.stock }})</span>\n @endif\n </div>\n\n <div class=\"rating-stars\">\n ★★★★★ ({{ product.rating }}.0)\n </div>\n\n <h3 class=\"product-name\">{{ product.name }}</h3>\n <p class=\"product-desc\">{{ product.description }}</p>\n </div>\n\n <div class=\"card-bottom\">\n <div class=\"price-box\">\n <span class=\"price-label\">Price</span>\n <span class=\"price-val\">${{ product.price }}</span>\n </div>\n <button class=\"btn-buy\" onclick=\"alert('Selected: {{ product.name }} - ${{ product.price }}')\">\n Select\n </button>\n </div>\n </div>\n @endeach\n </div>\n\n <!-- Edge Syntax Explainer -->\n <div class=\"syntax-box\">\n <h4><span>💡</span> How Edge.js Renders This Page</h4>\n <pre><code>// Edge.js loop syntax used directly above:\n&#64;each(product in products)\n &lt;div class=\"product-card\"&gt;\n &lt;h3&gt;&#123;&#123; product.name &#125;&#125;&lt;/h3&gt;\n &lt;span&gt;$&#123;&#123; product.price &#125;&#125;&lt;/span&gt;\n &#64;if(product.stock &gt; 10)\n &lt;span class=\"stock-in\"&gt;In Stock&lt;/span&gt;\n &#64;else\n &lt;span class=\"stock-low\"&gt;Low Stock&lt;/span&gt;\n &#64;endif\n &lt;/div&gt;\n&#64;endeach</code></pre>\n </div>\n </div>\n</body>\n</html>\n";
8
- export const productsEjsContent = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>EJS View Engine &bull; AeroJS + MySQL</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>\n :root {\n --bg-base: #060913;\n --bg-card: #0d1322;\n --bg-card-hover: #131c31;\n --border-subtle: #1e293b;\n --border-highlight: #06b6d4;\n --text-main: #f8fafc;\n --text-muted: #94a3b8;\n --brand-ejs: #06b6d4;\n --brand-ejs-light: #22d3ee;\n --brand-emerald: #10b981;\n --brand-amber: #f59e0b;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n background: var(--bg-base);\n color: var(--text-main);\n min-height: 100vh;\n line-height: 1.6;\n padding-bottom: 4rem;\n }\n .ambient-glow {\n position: fixed;\n top: -220px;\n left: 50%;\n transform: translateX(-50%);\n width: 900px;\n height: 480px;\n background: radial-gradient(circle, rgba(6, 182, 212, 0.16) 0%, rgba(14, 165, 233, 0.08) 50%, transparent 70%);\n pointer-events: none;\n z-index: 0;\n }\n .container {\n max-width: 1180px;\n margin: 0 auto;\n padding: 0 1.5rem;\n position: relative;\n z-index: 1;\n }\n /* Top Navbar */\n nav {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem 0;\n border-bottom: 1px solid rgba(30, 41, 59, 0.6);\n margin-bottom: 2.5rem;\n }\n .nav-brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n text-decoration: none;\n font-size: 1.25rem;\n font-weight: 800;\n color: #fff;\n }\n .brand-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.75rem;\n border-radius: 9999px;\n font-size: 0.75rem;\n font-weight: 600;\n background: rgba(6, 182, 212, 0.15);\n color: var(--brand-ejs-light);\n border: 1px solid rgba(6, 182, 212, 0.3);\n }\n .engine-switcher {\n display: flex;\n gap: 0.5rem;\n background: #0a0f1d;\n padding: 0.3rem;\n border-radius: 0.75rem;\n border: 1px solid var(--border-subtle);\n }\n .engine-tab {\n padding: 0.45rem 0.95rem;\n border-radius: 0.5rem;\n font-size: 0.85rem;\n font-weight: 600;\n text-decoration: none;\n color: #94a3b8;\n transition: all 0.2s;\n }\n .engine-tab:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.05);\n }\n .engine-tab.active {\n color: #fff;\n background: linear-gradient(135deg, #0891b2, #0284c7);\n box-shadow: 0 2px 10px rgba(8, 145, 178, 0.4);\n }\n\n /* Page Header */\n .header-banner {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n flex-wrap: wrap;\n gap: 1.5rem;\n margin-bottom: 2.5rem;\n }\n .header-info h1 {\n font-size: 2.4rem;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1.2;\n margin-bottom: 0.5rem;\n }\n .header-info h1 span {\n background: linear-gradient(135deg, #22d3ee, #38bdf8);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .header-info p {\n color: var(--text-muted);\n font-size: 1.05rem;\n }\n .db-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.6rem 1.2rem;\n border-radius: 0.75rem;\n background: rgba(16, 185, 129, 0.1);\n border: 1px solid rgba(16, 185, 129, 0.25);\n color: #34d399;\n font-size: 0.85rem;\n font-weight: 600;\n font-family: 'JetBrains Mono', monospace;\n }\n .dot-live {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #10b981;\n box-shadow: 0 0 10px #10b981;\n }\n\n /* Metric stats */\n .stats-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 1rem;\n margin-bottom: 2.5rem;\n }\n .stat-card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 0.85rem;\n padding: 1.25rem;\n }\n .stat-label {\n color: var(--text-muted);\n font-size: 0.82rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 0.35rem;\n }\n .stat-val {\n font-size: 1.8rem;\n font-weight: 800;\n color: #fff;\n }\n\n /* Products Grid */\n .catalog-title {\n font-size: 1.35rem;\n font-weight: 700;\n letter-spacing: -0.02em;\n margin-bottom: 1.25rem;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n }\n .products-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n gap: 1.5rem;\n }\n .product-card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.6rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: all 0.25s ease;\n position: relative;\n overflow: hidden;\n }\n .product-card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: linear-gradient(90deg, #06b6d4, #10b981);\n opacity: 0;\n transition: opacity 0.25s ease;\n }\n .product-card:hover {\n transform: translateY(-3px);\n border-color: rgba(6, 182, 212, 0.4);\n background: var(--bg-card-hover);\n box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);\n }\n .product-card:hover::before {\n opacity: 1;\n }\n .card-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 1rem;\n }\n .category-tag {\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--brand-ejs-light);\n background: rgba(6, 182, 212, 0.12);\n border: 1px solid rgba(6, 182, 212, 0.25);\n padding: 0.25rem 0.65rem;\n border-radius: 6px;\n }\n .stock-badge {\n font-size: 0.75rem;\n font-weight: 600;\n padding: 0.2rem 0.6rem;\n border-radius: 9999px;\n }\n .stock-in {\n background: rgba(16, 185, 129, 0.12);\n color: #34d399;\n border: 1px solid rgba(16, 185, 129, 0.25);\n }\n .stock-low {\n background: rgba(245, 158, 11, 0.12);\n color: #fbbf24;\n border: 1px solid rgba(245, 158, 11, 0.25);\n }\n .product-name {\n font-size: 1.2rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 0.5rem;\n line-height: 1.35;\n }\n .product-desc {\n color: var(--text-muted);\n font-size: 0.9rem;\n line-height: 1.6;\n margin-bottom: 1.5rem;\n flex-grow: 1;\n }\n .card-bottom {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-top: 1rem;\n border-top: 1px solid rgba(30, 41, 59, 0.6);\n }\n .price-box {\n display: flex;\n flex-direction: column;\n }\n .price-label {\n font-size: 0.72rem;\n color: #64748b;\n text-transform: uppercase;\n font-weight: 600;\n }\n .price-val {\n font-size: 1.5rem;\n font-weight: 800;\n color: #fff;\n font-family: 'JetBrains Mono', monospace;\n }\n .rating-stars {\n color: #fbbf24;\n font-size: 0.85rem;\n margin-bottom: 0.3rem;\n }\n .btn-buy {\n background: linear-gradient(135deg, #0891b2, #0284c7);\n color: #fff;\n border: none;\n padding: 0.6rem 1.2rem;\n border-radius: 0.6rem;\n font-size: 0.88rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-buy:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 15px rgba(8, 145, 178, 0.4);\n }\n\n /* Template Snippet Box */\n .syntax-box {\n margin-top: 3.5rem;\n background: #080d1a;\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.75rem;\n }\n .syntax-box h4 {\n font-size: 1rem;\n font-weight: 700;\n margin-bottom: 0.75rem;\n color: var(--brand-ejs-light);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n pre {\n background: #040711;\n border: 1px solid #1e293b;\n border-radius: 0.5rem;\n padding: 1rem;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.82rem;\n color: #e2e8f0;\n overflow-x: auto;\n }\n </style>\n</head>\n<body>\n <div class=\"ambient-glow\"></div>\n <div class=\"container\">\n <!-- Navbar -->\n <nav>\n <a href=\"/\" class=\"nav-brand\">\n <span>🚀 AeroJS</span>\n <span class=\"brand-pill\">Engine: EJS</span>\n </a>\n <div class=\"engine-switcher\">\n <a href=\"/views/edge\" class=\"engine-tab\">Edge.js</a>\n <a href=\"/views/ejs\" class=\"engine-tab active\">EJS</a>\n <a href=\"/views/react\" class=\"engine-tab\">React</a>\n <a href=\"/views/vue\" class=\"engine-tab\">Vue 3</a>\n </div>\n </nav>\n\n <!-- Header Banner -->\n <div class=\"header-banner\">\n <div class=\"header-info\">\n <h1><span>EJS</span> Embedded JavaScript</h1>\n <p>Classic Node.js templating engine rendered on the server with real MySQL database rows.</p>\n </div>\n <div class=\"db-badge\">\n <span class=\"dot-live\"></span>\n MySQL 8 &bull; aerojs_app.products\n </div>\n </div>\n\n <!-- Quick Stats -->\n <div class=\"stats-row\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Catalog Items</div>\n <div class=\"stat-val\"><%= products.length %></div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Template Driver</div>\n <div class=\"stat-val\" style=\"font-size: 1.4rem; color: var(--brand-ejs-light);\">ejs (v3.1.10)</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Database Engine</div>\n <div class=\"stat-val\" style=\"font-size: 1.4rem; color: #38bdf8;\">MySQL2 + Knex</div>\n </div>\n </div>\n\n <!-- Products Catalog Section -->\n <div class=\"catalog-title\">\n <span>📦</span> Live Products from MySQL Database\n </div>\n\n <div class=\"products-grid\">\n <% products.forEach(function(product) { %>\n <div class=\"product-card\">\n <div>\n <div class=\"card-top\">\n <span class=\"category-tag\"><%= product.category %></span>\n <% if (product.stock > 10) { %>\n <span class=\"stock-badge stock-in\">In Stock (<%= product.stock %>)</span>\n <% } else { %>\n <span class=\"stock-badge stock-low\">Low Stock (<%= product.stock %>)</span>\n <% } %>\n </div>\n\n <div class=\"rating-stars\">\n ★★★★★ (<%= product.rating %>.0)\n </div>\n\n <h3 class=\"product-name\"><%= product.name %></h3>\n <p class=\"product-desc\"><%= product.description %></p>\n </div>\n\n <div class=\"card-bottom\">\n <div class=\"price-box\">\n <span class=\"price-label\">Price</span>\n <span class=\"price-val\">$<%= product.price %></span>\n </div>\n <button class=\"btn-buy\" onclick=\"alert('Selected: <%= product.name %> - $<%= product.price %>')\">\n Select\n </button>\n </div>\n </div>\n <% }); %>\n </div>\n\n <!-- EJS Syntax Explainer -->\n <div class=\"syntax-box\">\n <h4><span>💡</span> How EJS Renders This Page</h4>\n <pre><code>// EJS scriptlet and interpolation syntax used directly above:\n&lt;% products.forEach(function(product) { %&gt;\n &lt;div class=\"product-card\"&gt;\n &lt;h3&gt;&lt;%= product.name %&gt;&lt;/h3&gt;\n &lt;span&gt;$&lt;%= product.price %&gt;&lt;/span&gt;\n &lt;% if (product.stock &gt; 10) { %&gt;\n &lt;span class=\"stock-in\"&gt;In Stock&lt;/span&gt;\n &lt;% } else { %&gt;\n &lt;span class=\"stock-low\"&gt;Low Stock&lt;/span&gt;\n &lt;% } %&gt;\n &lt;/div&gt;\n&lt;% }); %&gt;</code></pre>\n </div>\n </div>\n</body>\n</html>\n";
9
- export const productsReactContent = "import React, { useState, useMemo } from 'react';\n\nexport interface Product {\n id: number;\n name: string;\n description: string;\n price: number;\n category: string;\n stock: number;\n rating: number;\n created_at: string;\n}\n\nexport interface ProductsReactProps {\n products: Product[];\n}\n\nexport default function ProductsReact({ products = [] }: ProductsReactProps) {\n const [search, setSearch] = useState('');\n const [selectedCategory, setSelectedCategory] = useState('All');\n const [sortBy, setSortBy] = useState<'name' | 'price-asc' | 'price-desc'>('name');\n const [cart, setCart] = useState<{ id: number; name: string; price: number }[]>([]);\n const [activeModalProduct, setActiveModalProduct] = useState<Product | null>(null);\n\n // Categories\n const categories = useMemo(() => {\n const cats = ['All', ...new Set(products.map((p) => p.category))];\n return cats;\n }, [products]);\n\n // Filtered & Sorted products\n const filteredProducts = useMemo(() => {\n return products\n .filter((p) => {\n const matchesSearch =\n p.name.toLowerCase().includes(search.toLowerCase()) ||\n p.description.toLowerCase().includes(search.toLowerCase());\n const matchesCategory =\n selectedCategory === 'All' || p.category === selectedCategory;\n return matchesSearch && matchesCategory;\n })\n .sort((a, b) => {\n if (sortBy === 'price-asc') return a.price - b.price;\n if (sortBy === 'price-desc') return b.price - a.price;\n return a.name.localeCompare(b.name);\n });\n }, [products, search, selectedCategory, sortBy]);\n\n const addToCart = (product: Product) => {\n setCart((prev) => [...prev, { id: product.id, name: product.name, price: product.price }]);\n };\n\n const cartTotal = useMemo(() => cart.reduce((sum, item) => sum + item.price, 0), [cart]);\n\n return (\n <div className=\"react-container\">\n <header className=\"page-header\">\n <div>\n <span className=\"badge-react\">⚛️ Inertia.js + React 18</span>\n <h1>React Interactive Catalog</h1>\n <p>Powered by MySQL2 connection pool and real-time state management.</p>\n </div>\n <div className=\"cart-pill\">\n 🛒 Cart: <strong>{cart.length} items</strong> (${cartTotal})\n </div>\n </header>\n\n {/* Filter & Search Bar */}\n <div className=\"controls-bar\">\n <input\n type=\"text\"\n placeholder=\"Search products in MySQL...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"search-input\"\n />\n\n <div className=\"category-pills\">\n {categories.map((cat) => (\n <button\n key={cat}\n onClick={() => setSelectedCategory(cat)}\n className={`pill-btn ${selectedCategory === cat ? 'active' : ''}`}\n >\n {cat}\n </button>\n ))}\n </div>\n\n <select\n value={sortBy}\n onChange={(e) => setSortBy(e.target.value as any)}\n className=\"sort-select\"\n >\n <option value=\"name\">Sort by Name</option>\n <option value=\"price-asc\">Price: Low to High</option>\n <option value=\"price-desc\">Price: High to Low</option>\n </select>\n </div>\n\n {/* Grid */}\n <div className=\"products-grid\">\n {filteredProducts.map((product) => (\n <div key={product.id} className=\"product-card\">\n <div className=\"card-top\">\n <span className=\"cat-tag\">{product.category}</span>\n <span className={product.stock > 10 ? 'stock-ok' : 'stock-low'}>\n {product.stock > 10 ? `In Stock (${product.stock})` : `Low Stock (${product.stock})`}\n </span>\n </div>\n <h3>{product.name}</h3>\n <p>{product.description}</p>\n <div className=\"card-bottom\">\n <div className=\"price-tag\">${product.price}</div>\n <div className=\"actions\">\n <button\n className=\"btn-details\"\n onClick={() => setActiveModalProduct(product)}\n >\n Quick View\n </button>\n <button className=\"btn-add\" onClick={() => addToCart(product)}>\n + Add to Cart\n </button>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n {/* Quick View Modal */}\n {activeModalProduct && (\n <div className=\"modal-backdrop\" onClick={() => setActiveModalProduct(null)}>\n <div className=\"modal-content\" onClick={(e) => e.stopPropagation()}>\n <h2>{activeModalProduct.name}</h2>\n <p className=\"modal-cat\">Category: {activeModalProduct.category} &bull; Rating: {activeModalProduct.rating}/5</p>\n <p className=\"modal-desc\">{activeModalProduct.description}</p>\n <div className=\"modal-price\">${activeModalProduct.price}</div>\n <div className=\"modal-actions\">\n <button\n className=\"btn-add\"\n onClick={() => {\n addToCart(activeModalProduct);\n setActiveModalProduct(null);\n }}\n >\n Add to Cart\n </button>\n <button\n className=\"btn-close\"\n onClick={() => setActiveModalProduct(null)}\n >\n Close\n </button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n";
10
- export const productsVueContent = "<script setup lang=\"ts\">\nimport { ref, computed } from 'vue';\n\nexport interface Product {\n id: number;\n name: string;\n description: string;\n price: number;\n category: string;\n stock: number;\n rating: number;\n created_at: string;\n}\n\nconst props = defineProps<{\n products: Product[];\n}>();\n\nconst search = ref('');\nconst selectedCategory = ref('All');\nconst sortBy = ref<'name' | 'price-asc' | 'price-desc'>('name');\nconst cart = ref<{ id: number; name: string; price: number }[]>([]);\nconst activeModalProduct = ref<Product | null>(null);\n\nconst categories = computed(() => {\n const cats = ['All', ...new Set((props.products || []).map((p) => p.category))];\n return cats;\n});\n\nconst filteredProducts = computed(() => {\n return (props.products || [])\n .filter((p) => {\n const matchesSearch =\n p.name.toLowerCase().includes(search.value.toLowerCase()) ||\n p.description.toLowerCase().includes(search.value.toLowerCase());\n const matchesCategory =\n selectedCategory.value === 'All' || p.category === selectedCategory.value;\n return matchesSearch && matchesCategory;\n })\n .sort((a, b) => {\n if (sortBy.value === 'price-asc') return a.price - b.price;\n if (sortBy.value === 'price-desc') return b.price - a.price;\n return a.name.localeCompare(b.name);\n });\n});\n\nconst cartTotal = computed(() =>\n cart.value.reduce((sum, item) => sum + item.price, 0)\n);\n\nfunction addToCart(product: Product) {\n cart.value.push({ id: product.id, name: product.name, price: product.price });\n}\n\nfunction openModal(product: Product) {\n activeModalProduct.value = product;\n}\n\nfunction closeModal() {\n activeModalProduct.value = null;\n}\n</script>\n\n<template>\n <div class=\"vue-container\">\n <header class=\"page-header\">\n <div>\n <span class=\"badge-vue\">💚 Inertia.js + Vue 3 (Composition API)</span>\n <h1>Vue 3 Reactive Catalog</h1>\n <p>Ultra-responsive reactive frontend wired directly to MySQL database records.</p>\n </div>\n <div class=\"cart-pill\">\n 🛒 Cart: <strong>{{ cart.length }} items</strong> (${{ cartTotal }})\n </div>\n </header>\n\n <!-- Controls Bar -->\n <div class=\"controls-bar\">\n <input\n v-model=\"search\"\n type=\"text\"\n placeholder=\"Filter MySQL products in real time...\"\n className=\"search-input\"\n />\n\n <div class=\"category-pills\">\n <button\n v-for=\"cat in categories\"\n :key=\"cat\"\n @click=\"selectedCategory = cat\"\n :class=\"['pill-btn', { active: selectedCategory === cat }]\"\n >\n {{ cat }}\n </button>\n </div>\n\n <select v-model=\"sortBy\" className=\"sort-select\">\n <option value=\"name\">Sort by Name</option>\n <option value=\"price-asc\">Price: Low to High</option>\n <option value=\"price-desc\">Price: High to Low</option>\n </select>\n </div>\n\n <!-- Products Grid -->\n <div class=\"products-grid\">\n <div\n v-for=\"product in filteredProducts\"\n :key=\"product.id\"\n class=\"product-card\"\n >\n <div class=\"card-top\">\n <span class=\"cat-tag\">{{ product.category }}</span>\n <span :class=\"product.stock > 10 ? 'stock-ok' : 'stock-low'\">\n {{ product.stock > 10 ? `In Stock (${product.stock})` : `Low Stock (${product.stock})` }}\n </span>\n </div>\n <h3>{{ product.name }}</h3>\n <p>{{ product.description }}</p>\n <div class=\"card-bottom\">\n <div class=\"price-tag\">${{ product.price }}</div>\n <div class=\"actions\">\n <button class=\"btn-details\" @click=\"openModal(product)\">\n Quick View\n </button>\n <button class=\"btn-add\" @click=\"addToCart(product)\">\n + Add to Cart\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Quick View Modal -->\n <div\n v-if=\"activeModalProduct\"\n class=\"modal-backdrop\"\n @click=\"closeModal\"\n >\n <div class=\"modal-content\" @click.stop>\n <h2>{{ activeModalProduct.name }}</h2>\n <p class=\"modal-cat\">\n Category: {{ activeModalProduct.category }} &bull; Rating: {{ activeModalProduct.rating }}/5\n </p>\n <p class=\"modal-desc\">{{ activeModalProduct.description }}</p>\n <div class=\"modal-price\">${{ activeModalProduct.price }}</div>\n <div class=\"modal-actions\">\n <button\n class=\"btn-add\"\n @click=\"addToCart(activeModalProduct); closeModal()\"\n >\n Add to Cart\n </button>\n <button class=\"btn-close\" @click=\"closeModal\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n";
11
- export const productModelContent = "import { Model } from 'aerojs';\n\nexport interface ProductAttributes {\n id?: number;\n name: string;\n description: string;\n price: number;\n category: string;\n stock: number;\n rating: number;\n created_at?: string;\n updated_at?: string;\n}\n\nexport class Product extends Model {\n public static override table = 'products';\n public static override primaryKey = 'id';\n public static override fillable = [\n 'name',\n 'description',\n 'price',\n 'category',\n 'stock',\n 'rating',\n ];\n}\n";
12
- export const initDbContent = "import knex from 'knex';\nimport { useKnex, DB } from 'aerojs';\nimport { databaseConfig } from '../config/database.js';\n\nexport async function initDatabase(): Promise<void> {\n // Initialize Knex with MySQL connection pool\n const db = knex({\n client: databaseConfig.connections.mysql.client,\n connection: databaseConfig.connections.mysql.connection,\n pool: databaseConfig.connections.mysql.pool,\n });\n\n useKnex(db);\n\n // Ensure products table exists\n const hasTable = await db.schema.hasTable('products');\n if (!hasTable) {\n await db.schema.createTable('products', (table) => {\n table.increments('id').primary();\n table.string('name', 255).notNullable();\n table.text('description');\n table.integer('price').notNullable().defaultTo(0);\n table.string('category', 100).notNullable().defaultTo('General');\n table.integer('stock').notNullable().defaultTo(0);\n table.integer('rating').notNullable().defaultTo(5);\n table.timestamps(true, true);\n });\n console.log('✅ Created MySQL table: products');\n }\n\n // Seed initial products if table has fewer than 5 products\n const countResult = await DB.table('products').count();\n if (countResult < 5) {\n await DB.table('products').delete();\n await DB.table('products').insert([\n {\n name: 'MacBook Pro M3 Max',\n description: 'Apple M3 Max chip with 16-core CPU and 40-core GPU, 36GB Unified Memory.',\n price: 3499,\n category: 'Laptops',\n stock: 14,\n rating: 5,\n },\n {\n name: 'Sony WH-1000XM5 Wireless Headphones',\n description: 'Industry-leading noise cancellation with two processors and 8 microphones.',\n price: 399,\n category: 'Audio',\n stock: 35,\n rating: 5,\n },\n {\n name: 'Dell UltraSharp 32\" 4K QD-OLED Monitor',\n description: 'Stunning 4K resolution with Quantum Dot OLED panel and 99% DCI-P3 color.',\n price: 1199,\n category: 'Monitors',\n stock: 8,\n rating: 5,\n },\n {\n name: 'Keychron Q1 Pro Custom Mechanical Keyboard',\n description: 'Wireless custom mechanical keyboard with CNC aluminum body and hot-swappable switches.',\n price: 199,\n category: 'Keyboards',\n stock: 50,\n rating: 4,\n },\n {\n name: 'Logitech MX Master 3S Wireless Mouse',\n description: 'Quiet clicks and 8K DPI track-on-glass sensor for ultimate precision.',\n price: 99,\n category: 'Accessories',\n stock: 80,\n rating: 5,\n },\n ]);\n console.log('🌱 Seeded 5 initial products into MySQL!');\n }\n}\n\n// Run immediately if executed directly\nif (process.argv[1]?.endsWith('init-db.ts') || process.argv[1]?.endsWith('init-db.js')) {\n initDatabase()\n .then(() => {\n console.log('🎉 Database initialization complete!');\n process.exit(0);\n })\n .catch((err) => {\n console.error('❌ Database initialization error:', err);\n process.exit(1);\n });\n}\n";
13
- export const webRoutesContent = "import type { Router, AeroContext } from 'aerojs';\nimport { DB } from 'aerojs';\nimport { renderEdge, renderEjs } from '../app/views/engine.js';\n\nexport function registerWebRoutes(router: Router): void {\n router.get('/', async (ctx: AeroContext) => {\n ctx.html(`\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Welcome to AeroJS — High Performance Full-Stack Framework</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>\n :root {\n --bg-base: #060913;\n --bg-card: #0d1322;\n --bg-card-hover: #131c31;\n --border-subtle: #1e293b;\n --border-highlight: #38bdf8;\n --text-main: #f8fafc;\n --text-muted: #94a3b8;\n --brand-primary: #38bdf8;\n --brand-cyan: #06b6d4;\n --brand-indigo: #6366f1;\n --brand-emerald: #10b981;\n --brand-amber: #f59e0b;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n background: var(--bg-base);\n color: var(--text-main);\n min-height: 100vh;\n line-height: 1.6;\n overflow-x: hidden;\n padding-bottom: 4rem;\n }\n /* Subtle Ambient Glow */\n .glow-bg {\n position: fixed;\n top: -200px;\n left: 50%;\n transform: translateX(-50%);\n width: 800px;\n height: 450px;\n background: radial-gradient(circle, rgba(56, 189, 248, 0.15) 0%, rgba(99, 102, 241, 0.08) 50%, transparent 70%);\n pointer-events: none;\n z-index: 0;\n }\n .container {\n max-width: 1140px;\n margin: 0 auto;\n padding: 0 1.5rem;\n position: relative;\n z-index: 1;\n }\n /* Navbar */\n nav {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem 0;\n border-bottom: 1px solid rgba(30, 41, 59, 0.6);\n margin-bottom: 3.5rem;\n }\n .logo {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n font-size: 1.35rem;\n font-weight: 800;\n letter-spacing: -0.03em;\n color: #fff;\n text-decoration: none;\n }\n .logo span {\n background: linear-gradient(135deg, var(--brand-primary), var(--brand-indigo));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .badge-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.75rem;\n border-radius: 9999px;\n font-size: 0.75rem;\n font-weight: 600;\n background: rgba(56, 189, 248, 0.1);\n color: var(--brand-primary);\n border: 1px solid rgba(56, 189, 248, 0.25);\n }\n .status-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--brand-emerald);\n box-shadow: 0 0 8px var(--brand-emerald);\n }\n /* Hero */\n .hero {\n text-align: center;\n max-width: 820px;\n margin: 0 auto 4rem;\n }\n .hero h1 {\n font-size: 3rem;\n font-weight: 800;\n letter-spacing: -0.04em;\n line-height: 1.15;\n margin: 1rem 0;\n }\n .hero h1 .gradient-text {\n background: linear-gradient(135deg, #ffffff 30%, #94a3b8 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .hero h1 .accent-text {\n background: linear-gradient(135deg, var(--brand-primary) 0%, #818cf8 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n .hero p {\n color: var(--text-muted);\n font-size: 1.15rem;\n margin-bottom: 2rem;\n line-height: 1.7;\n }\n .feature-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.6rem;\n justify-content: center;\n margin-bottom: 2rem;\n }\n .tag {\n font-size: 0.8rem;\n font-weight: 500;\n padding: 0.35rem 0.85rem;\n border-radius: 6px;\n background: rgba(30, 41, 59, 0.7);\n color: #cbd5e1;\n border: 1px solid rgba(51, 65, 85, 0.6);\n }\n /* Action Buttons */\n .hero-cta {\n display: flex;\n gap: 1rem;\n justify-content: center;\n align-items: center;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1.6rem;\n border-radius: 0.6rem;\n font-size: 0.95rem;\n font-weight: 600;\n text-decoration: none;\n transition: all 0.2s ease;\n cursor: pointer;\n border: 1px solid transparent;\n }\n .btn-primary {\n background: linear-gradient(135deg, #0284c7, #2563eb);\n color: #fff;\n box-shadow: 0 4px 20px rgba(2, 132, 199, 0.35);\n }\n .btn-primary:hover {\n background: linear-gradient(135deg, #0369a1, #1d4ed8);\n transform: translateY(-1px);\n box-shadow: 0 6px 24px rgba(2, 132, 199, 0.5);\n }\n .btn-secondary {\n background: #131c2e;\n color: #e2e8f0;\n border-color: #1e293b;\n }\n .btn-secondary:hover {\n background: #1e293b;\n color: #fff;\n transform: translateY(-1px);\n }\n\n /* Sections Grid */\n .section-title {\n font-size: 1.5rem;\n font-weight: 700;\n letter-spacing: -0.02em;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n }\n .grid-2 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 1.5rem;\n margin-bottom: 3.5rem;\n }\n .grid-3 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n gap: 1.25rem;\n margin-bottom: 3.5rem;\n }\n .card {\n background: var(--bg-card);\n border: 1px solid var(--border-subtle);\n border-radius: 1rem;\n padding: 1.75rem;\n transition: border-color 0.2s, background 0.2s;\n }\n .card:hover {\n border-color: rgba(56, 189, 248, 0.3);\n background: var(--bg-card-hover);\n }\n .card-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n }\n .card-icon {\n width: 38px;\n height: 38px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n background: rgba(56, 189, 248, 0.1);\n color: var(--brand-primary);\n border: 1px solid rgba(56, 189, 248, 0.2);\n }\n .card h3 {\n font-size: 1.15rem;\n font-weight: 700;\n color: #f1f5f9;\n }\n .card p {\n color: var(--text-muted);\n font-size: 0.92rem;\n line-height: 1.6;\n margin-bottom: 1.25rem;\n }\n\n /* Code Snippets */\n pre {\n background: #060913;\n border: 1px solid #1e293b;\n border-radius: 0.65rem;\n padding: 1rem;\n overflow-x: auto;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.82rem;\n color: #e2e8f0;\n line-height: 1.6;\n }\n .code-keyword { color: #f472b6; }\n .code-func { color: #38bdf8; }\n .code-str { color: #34d399; }\n .code-comment { color: #64748b; font-style: italic; }\n\n /* Interactive API Sandbox */\n .sandbox-card {\n background: linear-gradient(180deg, #0e172a 0%, #060913 100%);\n border: 1px solid #1e293b;\n border-radius: 1rem;\n padding: 2rem;\n margin-bottom: 3.5rem;\n }\n .sandbox-controls {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n margin-bottom: 1.25rem;\n }\n .sandbox-btn {\n background: #1e293b;\n color: #e2e8f0;\n border: 1px solid #334155;\n padding: 0.6rem 1.2rem;\n border-radius: 0.5rem;\n font-size: 0.88rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n .sandbox-btn:hover {\n background: #0284c7;\n border-color: #0284c7;\n color: #fff;\n }\n .terminal-window {\n background: #030712;\n border: 1px solid #1f2937;\n border-radius: 0.75rem;\n padding: 1.25rem;\n font-family: 'JetBrains Mono', monospace;\n font-size: 0.85rem;\n color: #10b981;\n min-height: 120px;\n white-space: pre-wrap;\n overflow-x: auto;\n }\n\n /* AI Assistant Banner */\n .ai-banner {\n background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(56, 189, 248, 0.15) 100%);\n border: 1px solid rgba(99, 102, 241, 0.3);\n border-radius: 1rem;\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n text-align: center;\n align-items: center;\n }\n .ai-banner h3 {\n font-size: 1.3rem;\n font-weight: 700;\n color: #fff;\n }\n .ai-banner p {\n color: #cbd5e1;\n max-width: 680px;\n font-size: 0.95rem;\n }\n footer {\n text-align: center;\n color: #64748b;\n font-size: 0.85rem;\n margin-top: 3.5rem;\n border-top: 1px solid #1e293b;\n padding-top: 2rem;\n }\n </style>\n </head>\n <body>\n <div class=\"glow-bg\"></div>\n <div class=\"container\">\n <!-- Navbar -->\n <nav>\n <a href=\"/\" class=\"logo\">\n <span>🚀 AeroJS</span>\n </a>\n <div style=\"display: flex; align-items: center; gap: 1rem;\">\n <span class=\"badge-pill\">\n <span class=\"status-dot\"></span>\n v0.1.0 Ready\n </span>\n <a href=\"/api/health\" class=\"tag\" style=\"text-decoration: none;\">API Status</a>\n </div>\n </nav>\n\n <!-- Hero Section -->\n <section class=\"hero\">\n <div class=\"badge-pill\" style=\"margin-bottom: 1.25rem;\">\n ⚡ High-Performance Full-Stack TypeScript Framework\n </div>\n <h1>\n <span class=\"gradient-text\">Welcome to </span>\n <span class=\"accent-text\">AeroJS</span>\n </h1>\n <p>\n Your enterprise-grade application is scaffolded and running with full MVC architecture,\n Active Record ORM, multi-dialect database connection pooling, and multi-frontend view drivers.\n </p>\n\n <div class=\"feature-tags\">\n <span class=\"tag\">Zero Core Dependencies</span>\n <span class=\"tag\">Active Record ORM</span>\n <span class=\"tag\">MySQL & PostgreSQL Pool</span>\n <span class=\"tag\">Inertia.js (Vue & React)</span>\n <span class=\"tag\">Edge.js & EJS</span>\n <span class=\"tag\">Background Queue & Mail</span>\n <span class=\"tag\">AI-Native AGENTS.md</span>\n </div>\n\n <div class=\"hero-cta\">\n <a href=\"#view-engines\" class=\"btn btn-primary\" style=\"background: linear-gradient(135deg, #a855f7, #0284c7);\">⚡ Test 4 Frontend Engines</a>\n <a href=\"#sandbox\" class=\"btn btn-secondary\">Test Live APIs</a>\n </div>\n </section>\n\n <!-- 4 Frontend & View Engines Showcase -->\n <section id=\"view-engines\" style=\"margin-bottom: 3.5rem;\">\n <div class=\"section-title\">\n <span>🎨</span> 4 Frontend & View Engines (MySQL Powered)\n </div>\n <p style=\"color: var(--text-muted); margin-bottom: 1.5rem;\">\n Each engine below fetches real products directly from your <strong>MySQL (aerojs_app)</strong> database and tests rendering with its unique syntax and client interactivity:\n </p>\n\n <div class=\"grid-2\">\n <!-- Engine 1: Edge.js -->\n <div class=\"card\" style=\"border-color: rgba(168, 85, 247, 0.3);\">\n <div class=\"card-header\">\n <div class=\"card-icon\" style=\"background: rgba(168, 85, 247, 0.15); color: #c084fc; border-color: rgba(168, 85, 247, 0.3);\">🚀</div>\n <div>\n <h3>1. Edge.js (AdonisJS Engine)</h3>\n <small style=\"color: #c084fc;\">Server-Side Template Engine</small>\n </div>\n </div>\n <p>Features <code>@each(product in products)</code>, <code>@if/@endif</code>, layouts, and components with blazing-fast Node.js compilation.</p>\n <div style=\"margin-top: 1rem;\">\n <a href=\"/views/edge\" target=\"_blank\" class=\"btn btn-primary\" style=\"background: linear-gradient(135deg, #9333ea, #7e22ce); font-size: 0.85rem; padding: 0.55rem 1.1rem;\">\n Launch Edge.js View &rarr;\n </a>\n </div>\n </div>\n\n <!-- Engine 2: EJS -->\n <div class=\"card\" style=\"border-color: rgba(6, 182, 212, 0.3);\">\n <div class=\"card-header\">\n <div class=\"card-icon\" style=\"background: rgba(6, 182, 212, 0.15); color: #22d3ee; border-color: rgba(6, 182, 212, 0.3);\">⚡</div>\n <div>\n <h3>2. EJS (Embedded JavaScript)</h3>\n <small style=\"color: #22d3ee;\">Classic Express / Node Template Engine</small>\n </div>\n </div>\n <p>Standard JavaScript scriptlets with <code>&lt;% products.forEach(...) %&gt;</code> and <code>&lt;%= product.name %&gt;</code> variable outputs.</p>\n <div style=\"margin-top: 1rem;\">\n <a href=\"/views/ejs\" target=\"_blank\" class=\"btn btn-primary\" style=\"background: linear-gradient(135deg, #0891b2, #0284c7); font-size: 0.85rem; padding: 0.55rem 1.1rem;\">\n Launch EJS View &rarr;\n </a>\n </div>\n </div>\n\n <!-- Engine 3: React 18 -->\n <div class=\"card\" style=\"border-color: rgba(56, 189, 248, 0.3);\">\n <div class=\"card-header\">\n <div class=\"card-icon\" style=\"background: rgba(56, 189, 248, 0.15); color: #38bdf8; border-color: rgba(56, 189, 248, 0.3);\">⚛️</div>\n <div>\n <h3>3. React 18 (Inertia.js SPA)</h3>\n <small style=\"color: #38bdf8;\">Client React Component + Server Hydration</small>\n </div>\n </div>\n <p>Interactive single-page catalog with live client-side search, category filter pills, price sorting, cart counter, and modal.</p>\n <div style=\"margin-top: 1rem;\">\n <a href=\"/views/react\" target=\"_blank\" class=\"btn btn-primary\" style=\"background: linear-gradient(135deg, #0284c7, #2563eb); font-size: 0.85rem; padding: 0.55rem 1.1rem;\">\n Launch React View &rarr;\n </a>\n </div>\n </div>\n\n <!-- Engine 4: Vue 3 -->\n <div class=\"card\" style=\"border-color: rgba(16, 185, 129, 0.3);\">\n <div class=\"card-header\">\n <div class=\"card-icon\" style=\"background: rgba(16, 185, 129, 0.15); color: #34d399; border-color: rgba(16, 185, 129, 0.3);\">💚</div>\n <div>\n <h3>4. Vue 3 (Composition API)</h3>\n <small style=\"color: #34d399;\">Inertia Protocol + Vue 3 Reactivity</small>\n </div>\n </div>\n <p>Composition API with <code>ref()</code> and <code>computed()</code>, reactive filtering, interactive modal details, and toast notifications.</p>\n <div style=\"margin-top: 1rem;\">\n <a href=\"/views/vue\" target=\"_blank\" class=\"btn btn-primary\" style=\"background: linear-gradient(135deg, #059669, #10b981); font-size: 0.85rem; padding: 0.55rem 1.1rem;\">\n Launch Vue 3 View &rarr;\n </a>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Core Feature Cards -->\n <section id=\"features\">\n <div class=\"section-title\">\n <span>💎</span> Framework Capabilities & Usage Guide\n </div>\n\n <div class=\"grid-2\">\n <!-- Feature 1: Layered Architecture -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"card-icon\">🏛️</div>\n <div>\n <h3>Layered MVC + Service Architecture</h3>\n <small style=\"color: #64748b;\">Clean Separation of Concerns</small>\n </div>\n </div>\n <p>Clean request pipelines with Controller Tuples and decoupled business logic inside services:</p>\n <pre><code><span class=\"code-comment\">// routes/api.ts</span>\nrouter.<span class=\"code-func\">group</span>(<span class=\"code-str\">'/api'</span>, (api) => {\n api.<span class=\"code-func\">get</span>(<span class=\"code-str\">'/users'</span>, [UserController, <span class=\"code-str\">'index'</span>]);\n api.<span class=\"code-func\">post</span>(<span class=\"code-str\">'/users'</span>, [UserController, <span class=\"code-str\">'store'</span>])\n .<span class=\"code-func\">schema</span>(createUserSchema);\n});</code></pre>\n </div>\n\n <!-- Feature 2: Database & Connection Pooling -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"card-icon\">🗄️</div>\n <div>\n <h3>Multi-Dialect Database & Pooling</h3>\n <small style=\"color: #64748b;\">MySQL • PostgreSQL • SQLite • Memory</small>\n </div>\n </div>\n <p>Configured with Knex pooling (<span style=\"color:#38bdf8;\">DB_POOL_MIN</span> & <span style=\"color:#38bdf8;\">DB_POOL_MAX</span>) for high-concurrency production workloads:</p>\n <pre><code><span class=\"code-comment\">// config/database.ts</span>\n<span class=\"code-keyword\">const</span> poolMin = parseInt(process.env.DB_POOL_MIN || <span class=\"code-str\">'2'</span>, 10);\n<span class=\"code-keyword\">const</span> poolMax = parseInt(process.env.DB_POOL_MAX || <span class=\"code-str\">'20'</span>, 10);\n\n<span class=\"code-keyword\">export const</span> databaseConfig = {\n <span class=\"code-func\">default</span>: process.env.DB_CONNECTION || <span class=\"code-str\">'mysql'</span>,\n pool: { min: poolMin, max: poolMax }\n};</code></pre>\n </div>\n\n <!-- Feature 3: Active Record ORM & QueryBuilder -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"card-icon\">⚡</div>\n <div>\n <h3>Active Record ORM & Migrations</h3>\n <small style=\"color: #64748b;\">Fluent Models & Schema Blueprint</small>\n </div>\n </div>\n <p>Laravel/Adonis style models, relationships, soft deletes, and timestamped schema migrations:</p>\n <pre><code><span class=\"code-comment\">// Find or mutate active record</span>\n<span class=\"code-keyword\">const</span> user = <span class=\"code-keyword\">await</span> User.<span class=\"code-func\">where</span>(<span class=\"code-str\">'role'</span>, <span class=\"code-str\">'admin'</span>).<span class=\"code-func\">first</span>();\nuser.name = <span class=\"code-str\">'Lead Architect'</span>;\n<span class=\"code-keyword\">await</span> user.<span class=\"code-func\">save</span>();\n\n<span class=\"code-comment\">// Fluent QueryBuilder</span>\n<span class=\"code-keyword\">const</span> paged = <span class=\"code-keyword\">await</span> DB.<span class=\"code-func\">table</span>(<span class=\"code-str\">'users'</span>).<span class=\"code-func\">paginate</span>(1, 15);</code></pre>\n </div>\n\n <!-- Feature 4: Frontend View Adapters -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"card-icon\">🎨</div>\n <div>\n <h3>Frontend Views: Inertia, Edge, EJS</h3>\n <small style=\"color: #64748b;\">React, Vue 3, or Template SSR</small>\n </div>\n </div>\n <p>Support for Modern SPAs with Inertia.js or server-rendered HTML using Edge.js or EJS:</p>\n <pre><code><span class=\"code-comment\">// Inertia.js (React / Vue 3)</span>\n<span class=\"code-keyword\">await</span> ctx.inertia.<span class=\"code-func\">render</span>(<span class=\"code-str\">'Users/Index'</span>, { users });\n\n<span class=\"code-comment\">// Edge.js or EJS Template</span>\n<span class=\"code-keyword\">await</span> ctx.<span class=\"code-func\">view</span>(<span class=\"code-str\">'welcome'</span>, { title: <span class=\"code-str\">'Dashboard'</span> });</code></pre>\n </div>\n </div>\n </section>\n\n <!-- Interactive Live API Sandbox -->\n <section id=\"sandbox\">\n <div class=\"section-title\">\n <span>🧪</span> Interactive API Sandbox\n </div>\n <div class=\"sandbox-card\">\n <p style=\"color: #94a3b8; margin-bottom: 1.25rem;\">\n Click below to send live requests to your AeroJS application and inspect the server response:\n </p>\n <div class=\"sandbox-controls\">\n <button class=\"sandbox-btn\" id=\"btn-health\">GET /api/health</button>\n <button class=\"sandbox-btn\" id=\"btn-users\">GET /api/users</button>\n <a href=\"/demo-error\" target=\"_blank\" class=\"sandbox-btn\" style=\"background: rgba(239, 68, 68, 0.2); color: #f87171; border-color: rgba(239, 68, 68, 0.4); text-decoration: none;\">💥 Test Error Dashboard</a>\n <button class=\"sandbox-btn\" id=\"btn-clear\" style=\"background: transparent; border-color: #334155;\">Clear Console</button>\n </div>\n <div class=\"terminal-window\" id=\"terminal-output\">// Click a button above to run live API test...</div>\n </div>\n </section>\n\n <!-- AI Assistant Guidelines Banner -->\n <section class=\"ai-banner\">\n <h3>🤖 AI-Native Engineering with AGENTS.md</h3>\n <p>\n Your project comes with an exhaustive <strong>AGENTS.md</strong> and <strong>AEROJS.md</strong> in the project root.\n Any AI agent (Gemini, Claude, Cursor, Windsurf, Copilot) reading this file will instantly know how to write 100% correct AeroJS code.\n </p>\n <div style=\"font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: #38bdf8; background: rgba(0,0,0,0.3); padding: 0.4rem 1rem; border-radius: 6px;\">\n d:\\\\nodejs\\\\my-aero-app\\\\AGENTS.md\n </div>\n </section>\n\n <!-- Footer -->\n <footer>\n <p>AeroJS Framework &bull; Designed for maximum developer speed, zero core bloat, and enterprise scalability.</p>\n </footer>\n </div>\n\n <script>\n const terminal = document.getElementById('terminal-output');\n\n async function callApi(url) {\n terminal.innerText = 'Connecting to ' + url + '...';\n try {\n const start = performance.now();\n const res = await fetch(url);\n const data = await res.json();\n const elapsed = Math.round(performance.now() - start);\n terminal.innerText = 'HTTP ' + res.status + ' OK (' + elapsed + 'ms)\\\\n' + JSON.stringify(data, null, 2);\n } catch (err) {\n terminal.innerText = 'Request Error: ' + err.message;\n }\n }\n\n document.getElementById('btn-health').addEventListener('click', () => callApi('/api/health'));\n document.getElementById('btn-users').addEventListener('click', () => callApi('/api/users'));\n document.getElementById('btn-clear').addEventListener('click', () => {\n terminal.innerText = '// Console cleared.';\n });\n </script>\n </body>\n </html>\n `);\n });\n\n // 1. Edge.js View Engine Route (MySQL Powered)\n router.get('/views/edge', async (ctx: AeroContext) => {\n const products = await DB.table('products').get();\n const html = await renderEdge('edge/products', { products });\n ctx.html(html);\n });\n\n // 2. EJS View Engine Route (MySQL Powered)\n router.get('/views/ejs', async (ctx: AeroContext) => {\n const products = await DB.table('products').get();\n const html = await renderEjs('ejs/products.ejs', { products });\n ctx.html(html);\n });\n\n // 3. React 18 (Inertia.js SPA) Route (MySQL Powered)\n router.get('/views/react', async (ctx: AeroContext) => {\n const products = await DB.table('products').get();\n await ctx.inertia.render('ProductsReact', { products });\n });\n\n // 4. Vue 3 (Inertia.js SPA) Route (MySQL Powered)\n router.get('/views/vue', async (ctx: AeroContext) => {\n const products = await DB.table('products').get();\n await ctx.inertia.render('ProductsVue', { products });\n });\n\n // Demo route intentionally triggering an unhandled exception to demonstrate the Next.js style Error Dashboard\n router.get('/demo-error', () => {\n const user: any = undefined;\n // This will trigger: TypeError: Cannot read properties of undefined (reading 'profile')\n return user.profile.getSettings();\n });\n}\n";
14
- export const apiRoutesContent = "import type { Router, AeroContext } from 'aerojs';\nimport { HealthController } from '../app/controllers/HealthController.js';\nimport { UserController } from '../app/controllers/UserController.js';\nimport { createUserSchema } from '../app/validators/UserValidator.js';\nimport { Product } from '../app/models/Product.js';\n\nexport function registerApiRoutes(router: Router): void {\n router.group('/api', (api) => {\n api.get('/health', [HealthController, 'check']);\n\n api.get('/products', async (ctx: AeroContext) => {\n const products = await Product.all();\n ctx.json({ success: true, count: products.length, data: products });\n });\n\n api.group('/users', (users) => {\n users.get('/', [UserController, 'index']);\n users.get('/:id', [UserController, 'show']);\n users.post('/', [UserController, 'store']).schema(createUserSchema);\n });\n });\n}\n";
15
- export const serverTsContent = "import { AeroJS } from 'aerojs';\nimport { appConfig } from './config/app.js';\nimport { registerApiRoutes } from './routes/api.js';\nimport { registerWebRoutes } from './routes/web.js';\nimport { join } from 'node:path';\nimport { initDatabase } from './database/init-db.js';\nimport { getReactShell, getVueShell } from './app/views/shells.js';\n\n// Initialize Database connection and ensure tables exist\nawait initDatabase();\n\nconst app = new AeroJS({\n debug: appConfig.debug,\n trustProxy: true,\n});\n\n// Middlewares\napp.useCors();\napp.useSecurityHeaders();\napp.serveStatic('/public', join(process.cwd(), 'public'));\n\n// Configure Inertia for React & Vue 3 full-stack SPAs\napp.useInertia({\n rootView: (page) => {\n if (page.component === 'ProductsVue') {\n return getVueShell(page);\n }\n return getReactShell(page);\n },\n});\n\n// Register Routes\nregisterWebRoutes(app.router);\nregisterApiRoutes(app.router);\n\n// Start server (only when not in test runner)\nif (process.env.NODE_ENV !== 'test') {\n app.listen(appConfig.port, () => {\n console.log(`\\n🚀 AeroJS Server running at http://localhost:${appConfig.port}`);\n console.log(`👉 Health Check: http://localhost:${appConfig.port}/api/health`);\n console.log(`👉 Users API: http://localhost:${appConfig.port}/api/users\\n`);\n });\n}\n\nexport default app;\n";
16
- export const databaseConfigContent = "/**\n * Database Configuration for AeroJS\n * Supports MySQL, PostgreSQL, SQLite, and In-Memory out-of-the-box.\n *\n * Connection pooling is configured via DB_POOL_MIN and DB_POOL_MAX:\n */\nconst poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);\nconst poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);\n\nexport const databaseConfig = {\n // Active connection dialect: 'mysql' | 'postgres' | 'sqlite' | 'memory'\n default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),\n\n // Global connection pool configuration\n pool: {\n min: poolMin,\n max: poolMax,\n },\n\n connections: {\n // MySQL / MariaDB (Driver: mysql2)\n mysql: {\n client: 'mysql2',\n connection: {\n host: process.env.DB_HOST || '127.0.0.1',\n port: parseInt(process.env.DB_PORT || '3306', 10),\n user: process.env.DB_USER || 'root',\n password: process.env.DB_PASSWORD !== undefined ? process.env.DB_PASSWORD : (process.env.DB_PASS || ''),\n database: process.env.DB_DATABASE || process.env.DB_NAME || 'aerojs_app',\n },\n pool: { min: poolMin, max: poolMax },\n },\n\n // PostgreSQL (Driver: pg)\n postgres: {\n client: 'pg',\n connection: {\n host: process.env.DB_HOST || '127.0.0.1',\n port: parseInt(process.env.DB_PORT || '5432', 10),\n user: process.env.DB_USER || 'postgres',\n password: process.env.DB_PASSWORD !== undefined ? process.env.DB_PASSWORD : (process.env.DB_PASS || ''),\n database: process.env.DB_DATABASE || process.env.DB_NAME || 'aerojs_app',\n ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,\n },\n pool: { min: poolMin, max: poolMax },\n },\n\n // SQLite (Driver: better-sqlite3 or sqlite3)\n sqlite: {\n client: 'better-sqlite3',\n connection: {\n filename: process.env.DB_FILENAME || process.env.DB_DATABASE || 'storage/database.sqlite',\n },\n useNullAsDefault: true,\n },\n\n // In-Memory Database (Zero-dependency, ideal for fast unit tests)\n memory: {\n client: 'memory',\n },\n },\n};\n\n";
5
+ export const engineTsContent = `import { Edge } from 'edge.js';
6
+ import { join } from 'node:path';
7
+
8
+ // Initialize Edge.js instance and mount views directory
9
+ const edge = Edge.create();
10
+ edge.mount(join(process.cwd(), 'views'));
11
+
12
+ /**
13
+ * Render template with AdonisJS's Edge template engine
14
+ */
15
+ export async function renderEdge(templateName: string, data: Record<string, unknown> = {}): Promise<string> {
16
+ return edge.render(templateName, data);
17
+ }
18
+ `;
19
+ export const appTsContent = `/**
20
+ * AeroJS Client Entrypoint
21
+ * Standard entry for Vite, Inertia.js (React / Vue 3), and client-side scripts.
22
+ */
23
+
24
+ console.log('🚀 AeroJS client-side application loaded');
25
+ `;
26
+ export const indexEdgeContent = `<!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>Welcome to AeroJS — High Performance Full-Stack Framework</title>
32
+ <link rel="preconnect" href="https://fonts.googleapis.com">
33
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
34
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
35
+ <style>
36
+ :root {
37
+ --bg-base: #060913;
38
+ --bg-card: #0d1322;
39
+ --bg-card-hover: #131c31;
40
+ --border-subtle: #1e293b;
41
+ --border-highlight: #38bdf8;
42
+ --text-main: #f8fafc;
43
+ --text-muted: #94a3b8;
44
+ --brand-primary: #38bdf8;
45
+ --brand-cyan: #06b6d4;
46
+ --brand-indigo: #6366f1;
47
+ --brand-emerald: #10b981;
48
+ --brand-amber: #f59e0b;
49
+ }
50
+ * { box-sizing: border-box; margin: 0; padding: 0; }
51
+ body {
52
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
53
+ background: var(--bg-base);
54
+ color: var(--text-main);
55
+ min-height: 100vh;
56
+ line-height: 1.6;
57
+ padding-bottom: 4rem;
58
+ }
59
+ .ambient-glow {
60
+ position: fixed;
61
+ top: -200px;
62
+ left: 50%;
63
+ transform: translateX(-50%);
64
+ width: 900px;
65
+ height: 500px;
66
+ background: radial-gradient(circle, rgba(56, 189, 248, 0.15) 0%, rgba(99, 102, 241, 0.08) 50%, transparent 70%);
67
+ pointer-events: none;
68
+ z-index: 0;
69
+ }
70
+ .container {
71
+ max-width: 1140px;
72
+ margin: 0 auto;
73
+ padding: 0 1.5rem;
74
+ position: relative;
75
+ z-index: 1;
76
+ }
77
+ nav {
78
+ display: flex;
79
+ justify-content: space-between;
80
+ align-items: center;
81
+ padding: 1.5rem 0;
82
+ border-bottom: 1px solid rgba(30, 41, 59, 0.6);
83
+ margin-bottom: 3.5rem;
84
+ }
85
+ .brand {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.75rem;
89
+ font-size: 1.35rem;
90
+ font-weight: 800;
91
+ color: #fff;
92
+ }
93
+ .brand span {
94
+ background: linear-gradient(135deg, var(--brand-primary), var(--brand-indigo));
95
+ -webkit-background-clip: text;
96
+ -webkit-text-fill-color: transparent;
97
+ }
98
+ .badge-pill {
99
+ font-family: 'JetBrains Mono', monospace;
100
+ font-size: 0.75rem;
101
+ background: rgba(56, 189, 248, 0.12);
102
+ color: var(--brand-primary);
103
+ border: 1px solid rgba(56, 189, 248, 0.25);
104
+ padding: 0.25rem 0.65rem;
105
+ border-radius: 9999px;
106
+ }
107
+ .nav-links {
108
+ display: flex;
109
+ gap: 0.75rem;
110
+ }
111
+ .nav-btn {
112
+ background: var(--bg-card);
113
+ border: 1px solid var(--border-subtle);
114
+ color: var(--text-muted);
115
+ padding: 0.5rem 1rem;
116
+ border-radius: 8px;
117
+ cursor: pointer;
118
+ font-size: 0.88rem;
119
+ text-decoration: none;
120
+ transition: all 0.2s;
121
+ }
122
+ .nav-btn:hover {
123
+ color: #fff;
124
+ border-color: var(--brand-primary);
125
+ }
126
+ .nav-btn.primary {
127
+ background: linear-gradient(135deg, #0284c7, #2563eb);
128
+ color: #fff;
129
+ border-color: transparent;
130
+ font-weight: 600;
131
+ }
132
+ .hero {
133
+ text-align: center;
134
+ max-width: 820px;
135
+ margin: 0 auto 4rem;
136
+ }
137
+ .hero h1 {
138
+ font-size: 3rem;
139
+ font-weight: 800;
140
+ line-height: 1.15;
141
+ margin-bottom: 1.25rem;
142
+ letter-spacing: -0.03em;
143
+ }
144
+ .hero h1 span {
145
+ background: linear-gradient(135deg, #38bdf8, #818cf8);
146
+ -webkit-background-clip: text;
147
+ -webkit-text-fill-color: transparent;
148
+ }
149
+ .hero p {
150
+ font-size: 1.15rem;
151
+ color: var(--text-muted);
152
+ margin-bottom: 2rem;
153
+ line-height: 1.6;
154
+ }
155
+ .hero-actions {
156
+ display: flex;
157
+ justify-content: center;
158
+ gap: 1rem;
159
+ margin-bottom: 2.5rem;
160
+ }
161
+ .btn {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ gap: 0.5rem;
165
+ padding: 0.75rem 1.6rem;
166
+ border-radius: 10px;
167
+ font-weight: 700;
168
+ font-size: 0.95rem;
169
+ text-decoration: none;
170
+ transition: transform 0.2s, box-shadow 0.2s;
171
+ }
172
+ .btn-main {
173
+ background: linear-gradient(135deg, #0284c7 0%, #2563eb 100%);
174
+ color: #fff;
175
+ box-shadow: 0 4px 15px rgba(2, 132, 199, 0.35);
176
+ }
177
+ .btn-main:hover {
178
+ transform: translateY(-2px);
179
+ box-shadow: 0 6px 20px rgba(2, 132, 199, 0.45);
180
+ }
181
+ .btn-outline {
182
+ background: var(--bg-card);
183
+ border: 1px solid var(--border-subtle);
184
+ color: var(--text-main);
185
+ }
186
+ .btn-outline:hover {
187
+ border-color: var(--brand-primary);
188
+ transform: translateY(-2px);
189
+ }
190
+ .grid-3 {
191
+ display: grid;
192
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
193
+ gap: 1.5rem;
194
+ margin-bottom: 3.5rem;
195
+ }
196
+ .feature-card {
197
+ background: var(--bg-card);
198
+ border: 1px solid var(--border-subtle);
199
+ border-radius: 14px;
200
+ padding: 1.75rem;
201
+ transition: all 0.2s ease;
202
+ }
203
+ .feature-card:hover {
204
+ transform: translateY(-3px);
205
+ border-color: rgba(56, 189, 248, 0.4);
206
+ background: var(--bg-card-hover);
207
+ }
208
+ .card-icon {
209
+ font-size: 1.8rem;
210
+ margin-bottom: 1rem;
211
+ }
212
+ .feature-card h3 {
213
+ font-size: 1.2rem;
214
+ font-weight: 700;
215
+ color: #fff;
216
+ margin-bottom: 0.5rem;
217
+ }
218
+ .feature-card p {
219
+ font-size: 0.92rem;
220
+ color: var(--text-muted);
221
+ line-height: 1.6;
222
+ }
223
+ .architecture-box {
224
+ background: #090e1a;
225
+ border: 1px solid var(--border-subtle);
226
+ border-radius: 14px;
227
+ padding: 2rem;
228
+ margin-bottom: 3.5rem;
229
+ }
230
+ .architecture-box h3 {
231
+ font-size: 1.3rem;
232
+ font-weight: 700;
233
+ color: #fff;
234
+ margin-bottom: 1rem;
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 0.5rem;
238
+ }
239
+ pre {
240
+ background: #040711;
241
+ border: 1px solid #1e293b;
242
+ border-radius: 10px;
243
+ padding: 1.25rem;
244
+ font-family: 'JetBrains Mono', monospace;
245
+ font-size: 0.85rem;
246
+ color: #38bdf8;
247
+ overflow-x: auto;
248
+ line-height: 1.5;
249
+ }
250
+ footer {
251
+ text-align: center;
252
+ padding-top: 2rem;
253
+ border-top: 1px solid var(--border-subtle);
254
+ color: var(--text-muted);
255
+ font-size: 0.88rem;
256
+ }
257
+ </style>
258
+ </head>
259
+ <body>
260
+ <div class="ambient-glow"></div>
261
+
262
+ <div class="container">
263
+ <nav>
264
+ <div class="brand">
265
+ <span>🚀 AeroJS</span>
266
+ <span class="badge-pill">v0.1.1</span>
267
+ </div>
268
+ <div class="nav-links">
269
+ <a href="/docs" target="_blank" class="nav-btn primary">📚 Swagger API Docs</a>
270
+ <a href="/openapi.json" target="_blank" class="nav-btn">OpenAPI Spec</a>
271
+ <a href="/api/health" target="_blank" class="nav-btn">API Health</a>
272
+ </div>
273
+ </nav>
274
+
275
+ <section class="hero">
276
+ <h1>The Next-Gen <span>Full-Stack Web Framework</span> for Node.js</h1>
277
+ <p>
278
+ Zero external runtime dependencies in core engine. High-throughput HTTP routing, built-in Active Record ORM, DI container, automated OpenAPI documentation, and flexible frontend drivers.
279
+ </p>
280
+ <div class="hero-actions">
281
+ <a href="/docs" class="btn btn-main">Explore Swagger API Docs &rarr;</a>
282
+ <a href="/api/health" class="btn btn-outline">Check Health Status</a>
283
+ </div>
284
+ </section>
285
+
286
+ <div class="grid-3">
287
+ <div class="feature-card">
288
+ <div class="card-icon">⚡</div>
289
+ <h3>Zero-Dependency Engine</h3>
290
+ <p>Built purely on top of Node.js native standard primitives. No bloated runtime baggage, delivering maximum throughput and ultra-low memory overhead.</p>
291
+ </div>
292
+
293
+ <div class="feature-card">
294
+ <div class="card-icon">🏛️</div>
295
+ <h3>Layered Architecture</h3>
296
+ <p>Strict MVC & Service-Oriented pattern. Clean separation between Routers, Middlewares, Input Validators, Thin Controllers, and Reusable Services.</p>
297
+ </div>
298
+
299
+ <div class="feature-card">
300
+ <div class="card-icon">🗄️</div>
301
+ <h3>Active Record ORM</h3>
302
+ <p>Fluent QueryBuilder, relationships (hasMany, belongsTo), lifecycle hooks, migration blueprints, and Knex connection pooling for MySQL, PostgreSQL, and SQLite.</p>
303
+ </div>
304
+
305
+ <div class="feature-card">
306
+ <div class="card-icon">📚</div>
307
+ <h3>Interactive OpenAPI Docs</h3>
308
+ <p>Integrated Swagger UI and OpenAPI 3.0 spec auto-generated from your route definitions and validation schemas with live request execution.</p>
309
+ </div>
310
+
311
+ <div class="feature-card">
312
+ <div class="card-icon">🎨</div>
313
+ <h3>Flexible Presentation</h3>
314
+ <p>Support for Inertia.js (React 18 & Vue 3 SPAs) as well as classic Server-Side Rendering with Edge.js and EJS template drivers.</p>
315
+ </div>
316
+
317
+ <div class="feature-card">
318
+ <div class="card-icon">🛡️</div>
319
+ <h3>Enterprise Security</h3>
320
+ <p>Built-in CORS, Security Headers, CSRF protection, Rate Limiting, Session & JWT Guards, and Audit Trail logging out of the box.</p>
321
+ </div>
322
+ </div>
323
+
324
+ <div class="architecture-box">
325
+ <h3><span>🏗️</span> Request Lifecycle Flow</h3>
326
+ <pre>Incoming HTTP Request
327
+ │
328
+ [Middleware] (CORS, Security Headers, CSRF, Rate Limiting, Auth)
329
+ │
330
+ [Router] (Matches Path & HTTP Verb)
331
+ │
332
+ [Validator] (Validates request schema / payload before handler)
333
+ │
334
+ [Controller] (Extracts request inputs, calls Service layer)
335
+ │
336
+ [Service Layer] (Business logic, transactions, external APIs)
337
+ │
338
+ [Active Record Model / QueryBuilder / Database]
339
+ │
340
+ [Response / View Engine / Inertia.js]</pre>
341
+ </div>
342
+
343
+ <footer>
344
+ <p>Powered by <strong>@shohaghinfo/aerojs</strong> • Built for high-performance mission-critical applications.</p>
345
+ </footer>
346
+ </div>
347
+ </body>
348
+ </html>
349
+ `;
350
+ export const initDbContent = `import knex from 'knex';
351
+ import { useKnex } from 'aerojs';
352
+ import { databaseConfig } from '../config/database.js';
353
+
354
+ export async function initDatabase(): Promise<void> {
355
+ const activeDialect = databaseConfig.default;
356
+ const dialectConfig = (databaseConfig.connections as any)[activeDialect];
357
+
358
+ if (!dialectConfig) {
359
+ return;
360
+ }
361
+
362
+ // Initialize Knex with active connection pool
363
+ const db = knex({
364
+ client: dialectConfig.client,
365
+ connection: dialectConfig.connection,
366
+ pool: databaseConfig.pool,
367
+ useNullAsDefault: activeDialect === 'sqlite',
368
+ });
369
+
370
+ useKnex(db);
371
+ }
372
+
373
+ // Run immediately if executed directly
374
+ if (process.argv[1]?.endsWith('init-db.ts') || process.argv[1]?.endsWith('init-db.js')) {
375
+ initDatabase()
376
+ .then(() => {
377
+ console.log('🎉 Database connection initialized successfully!');
378
+ process.exit(0);
379
+ })
380
+ .catch((err) => {
381
+ console.error('❌ Database initialization error:', err);
382
+ process.exit(1);
383
+ });
384
+ }
385
+ `;
386
+ export const webRoutesContent = `import type { Router, AeroContext } from 'aerojs';
387
+ import { SwaggerGenerator, renderSwaggerUI } from 'aerojs';
388
+ import { renderEdge } from '../app/views/engine.js';
389
+
390
+ export function registerWebRoutes(router: Router): void {
391
+ // 1. Interactive Swagger UI & OpenAPI Specification
392
+ router.get('/openapi.json', async (ctx: AeroContext) => {
393
+ const spec = SwaggerGenerator.generate([...router.routes], {
394
+ title: '🚀 AeroJS Application API Documentation',
395
+ version: '1.0.0',
396
+ description: 'High-throughput OpenAPI 3.0 specification auto-generated from route definitions and schemas.',
397
+ servers: [
398
+ { url: 'http://localhost:3000', description: 'Local Development Server' },
399
+ ],
400
+ });
401
+ ctx.status(200).json(spec);
402
+ });
403
+
404
+ router.get('/docs', async (ctx: AeroContext) => {
405
+ const html = renderSwaggerUI({
406
+ title: '🚀 AeroJS API Documentation',
407
+ specUrl: '/openapi.json',
408
+ });
409
+ ctx.status(200).html(html);
410
+ });
411
+
412
+ router.get('/api/docs', async (ctx: AeroContext) => {
413
+ ctx.redirect('/docs', 302);
414
+ });
415
+
416
+ // 2. Default Welcome Home Page (rendered with Edge.js)
417
+ router.get('/', async (ctx: AeroContext) => {
418
+ const html = await renderEdge('index', {});
419
+ ctx.html(html);
420
+ });
421
+
422
+ // 3. Demo route demonstrating interactive Error Dashboard
423
+ router.get('/demo-error', () => {
424
+ const user: any = undefined;
425
+ return user.profile.getSettings();
426
+ });
427
+ }
428
+ `;
429
+ export const apiRoutesContent = `import type { Router } from 'aerojs';
430
+ import { HealthController } from '../app/controllers/HealthController.js';
431
+ import { UserController } from '../app/controllers/UserController.js';
432
+ import { createUserSchema } from '../app/validators/UserValidator.js';
433
+
434
+ export function registerApiRoutes(router: Router): void {
435
+ router.group('/api', (api) => {
436
+ api.get('/health', [HealthController, 'check']);
437
+
438
+ api.group('/users', (users) => {
439
+ users.get('/', [UserController, 'index']);
440
+ users.get('/:id', [UserController, 'show']);
441
+ users.post('/', [UserController, 'store']).schema(createUserSchema);
442
+ });
443
+ });
444
+ }
445
+ `;
446
+ export const serverTsContent = `import { AeroJS } from 'aerojs';
447
+ import { appConfig } from './config/app.js';
448
+ import { registerApiRoutes } from './routes/api.js';
449
+ import { registerWebRoutes } from './routes/web.js';
450
+ import { join } from 'node:path';
451
+ import { initDatabase } from './database/init-db.js';
452
+
453
+ // Initialize Database connection
454
+ await initDatabase();
455
+
456
+ const app = new AeroJS({
457
+ debug: appConfig.debug,
458
+ trustProxy: true,
459
+ });
460
+
461
+ // Built-in Middlewares
462
+ app.useCors();
463
+ app.useSecurityHeaders();
464
+ app.serveStatic('/public', join(process.cwd(), 'public'));
465
+
466
+ // Register Application Routes
467
+ registerWebRoutes(app.router);
468
+ registerApiRoutes(app.router);
469
+
470
+ // Start server (only when not in test runner)
471
+ if (process.env.NODE_ENV !== 'test') {
472
+ app.listen(appConfig.port, () => {
473
+ console.log(\`\\n🚀 AeroJS Server running at http://localhost:\${appConfig.port}\`);
474
+ console.log(\`👉 API Documentation: http://localhost:\${appConfig.port}/docs\`);
475
+ console.log(\`👉 Health Check: http://localhost:\${appConfig.port}/api/health\\n\`);
476
+ });
477
+ }
478
+
479
+ export default app;
480
+ `;
481
+ export const databaseConfigContent = `/**
482
+ * Database Configuration for AeroJS
483
+ * Supports MySQL, PostgreSQL, SQLite, and In-Memory out-of-the-box.
484
+ *
485
+ * Connection pooling is configured via DB_POOL_MIN and DB_POOL_MAX:
486
+ */
487
+ const poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);
488
+ const poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);
489
+
490
+ export const databaseConfig = {
491
+ // Active connection dialect: 'mysql' | 'postgres' | 'sqlite' | 'memory'
492
+ default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),
493
+
494
+ // Global connection pool configuration
495
+ pool: {
496
+ min: poolMin,
497
+ max: poolMax,
498
+ },
499
+
500
+ connections: {
501
+ // MySQL / MariaDB (Driver: mysql2)
502
+ mysql: {
503
+ client: 'mysql2',
504
+ connection: {
505
+ host: process.env.DB_HOST || '127.0.0.1',
506
+ port: parseInt(process.env.DB_PORT || '3306', 10),
507
+ user: process.env.DB_USER || 'root',
508
+ password: process.env.DB_PASSWORD !== undefined ? process.env.DB_PASSWORD : (process.env.DB_PASS || ''),
509
+ database: process.env.DB_DATABASE || process.env.DB_NAME || 'aerojs_app',
510
+ },
511
+ pool: { min: poolMin, max: poolMax },
512
+ },
513
+
514
+ // PostgreSQL (Driver: pg)
515
+ postgres: {
516
+ client: 'pg',
517
+ connection: {
518
+ host: process.env.DB_HOST || '127.0.0.1',
519
+ port: parseInt(process.env.DB_PORT || '5432', 10),
520
+ user: process.env.DB_USER || 'postgres',
521
+ password: process.env.DB_PASSWORD !== undefined ? process.env.DB_PASSWORD : (process.env.DB_PASS || ''),
522
+ database: process.env.DB_DATABASE || process.env.DB_NAME || 'aerojs_app',
523
+ ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
524
+ },
525
+ pool: { min: poolMin, max: poolMax },
526
+ },
527
+
528
+ // SQLite (Driver: better-sqlite3 or sqlite3)
529
+ sqlite: {
530
+ client: 'better-sqlite3',
531
+ connection: {
532
+ filename: process.env.DB_FILENAME || process.env.DB_DATABASE || 'storage/database.sqlite',
533
+ },
534
+ useNullAsDefault: true,
535
+ },
536
+
537
+ // In-Memory Database (Zero-dependency, ideal for fast unit tests)
538
+ memory: {
539
+ client: 'memory',
540
+ },
541
+ },
542
+ };
543
+ `;
17
544
  //# sourceMappingURL=fullstack-templates.js.map