@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.
- package/AEROJS.md +12 -12
- package/AGENTS.md +709 -709
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +337 -344
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/new.js +51 -51
- package/dist/cli/templates/fullstack-templates.d.ts +3 -7
- package/dist/cli/templates/fullstack-templates.d.ts.map +1 -1
- package/dist/cli/templates/fullstack-templates.js +540 -13
- package/dist/cli/templates/fullstack-templates.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,17 +1,544 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file fullstack-templates.ts
|
|
3
|
-
* @description Canonical fullstack application templates
|
|
3
|
+
* @description Canonical fullstack application templates for AeroJS.
|
|
4
4
|
*/
|
|
5
|
-
export const engineTsContent =
|
|
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, '"');\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) • 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<div id=\"app\" data-page='{\"component\":\"ProductsReact\",\"props\":{\"products\":[...]}}'>...</div>\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, '"');\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) • 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<div id=\"app\" data-page='{\"component\":\"ProductsVue\",\"props\":{\"products\":[...]}}'>...</div>\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 • 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 • 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@each(product in products)\n <div class=\"product-card\">\n <h3>{{ product.name }}</h3>\n <span>${{ product.price }}</span>\n @if(product.stock > 10)\n <span class=\"stock-in\">In Stock</span>\n @else\n <span class=\"stock-low\">Low Stock</span>\n @endif\n </div>\n@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 • 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 • 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<% products.forEach(function(product) { %>\n <div class=\"product-card\">\n <h3><%= product.name %></h3>\n <span>$<%= product.price %></span>\n <% if (product.stock > 10) { %>\n <span class=\"stock-in\">In Stock</span>\n <% } else { %>\n <span class=\"stock-low\">Low Stock</span>\n <% } %>\n </div>\n<% }); %></code></pre>\n </div>\n </div>\n</body>\n</html>\n";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 →\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><% products.forEach(...) %></code> and <code><%= product.name %></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 →\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 →\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 →\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 • 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
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
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 →</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
|