create-ereo 0.1.22 → 0.1.24
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/dist/index.js +571 -101
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
import { join, resolve } from "path";
|
|
5
|
+
import { join, resolve, dirname } from "path";
|
|
6
6
|
import { mkdir } from "fs/promises";
|
|
7
|
+
var pkgJsonPath = join(dirname(import.meta.dir), "package.json");
|
|
8
|
+
var EREO_VERSION = `^${(await Bun.file(pkgJsonPath).json()).version}`;
|
|
7
9
|
var defaultOptions = {
|
|
8
10
|
template: "tailwind",
|
|
9
11
|
typescript: true,
|
|
@@ -44,7 +46,18 @@ function parseArgs(args) {
|
|
|
44
46
|
process.exit(0);
|
|
45
47
|
}
|
|
46
48
|
if (arg === "-t" || arg === "--template") {
|
|
47
|
-
|
|
49
|
+
if (i + 1 >= args.length) {
|
|
50
|
+
console.error(` \x1B[31m\u2717\x1B[0m --template requires a value (minimal, default, tailwind)
|
|
51
|
+
`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const tmpl = args[++i];
|
|
55
|
+
if (tmpl !== "minimal" && tmpl !== "default" && tmpl !== "tailwind") {
|
|
56
|
+
console.error(` \x1B[31m\u2717\x1B[0m Unknown template "${tmpl}". Valid options: minimal, default, tailwind
|
|
57
|
+
`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
options.template = tmpl;
|
|
48
61
|
} else if (arg === "--no-typescript") {
|
|
49
62
|
options.typescript = false;
|
|
50
63
|
} else if (arg === "--no-git") {
|
|
@@ -57,6 +70,74 @@ function parseArgs(args) {
|
|
|
57
70
|
}
|
|
58
71
|
return { projectName, options };
|
|
59
72
|
}
|
|
73
|
+
function generateDockerfile(typescript) {
|
|
74
|
+
const tsconfigCopy = typescript ? `COPY --from=builder --chown=ereo:ereo /app/tsconfig.json ./
|
|
75
|
+
` : "";
|
|
76
|
+
return `# syntax=docker/dockerfile:1
|
|
77
|
+
|
|
78
|
+
# ---- Stage 1: Install production dependencies ----
|
|
79
|
+
FROM oven/bun:1-slim AS deps
|
|
80
|
+
WORKDIR /app
|
|
81
|
+
COPY package.json bun.lockb* ./
|
|
82
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \\
|
|
83
|
+
bun install --frozen-lockfile --production --ignore-scripts
|
|
84
|
+
|
|
85
|
+
# ---- Stage 2: Install all deps + build ----
|
|
86
|
+
FROM oven/bun:1-slim AS builder
|
|
87
|
+
WORKDIR /app
|
|
88
|
+
COPY package.json bun.lockb* ./
|
|
89
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \\
|
|
90
|
+
bun install --frozen-lockfile --ignore-scripts
|
|
91
|
+
COPY . .
|
|
92
|
+
RUN bun run build
|
|
93
|
+
|
|
94
|
+
# ---- Stage 3: Production image ----
|
|
95
|
+
FROM oven/bun:1-slim AS runner
|
|
96
|
+
WORKDIR /app
|
|
97
|
+
|
|
98
|
+
ENV NODE_ENV=production
|
|
99
|
+
|
|
100
|
+
# Non-root user for security
|
|
101
|
+
RUN groupadd --system --gid 1001 ereo && \\
|
|
102
|
+
useradd --system --uid 1001 --gid ereo --no-create-home ereo
|
|
103
|
+
|
|
104
|
+
# Copy only what's needed to run
|
|
105
|
+
COPY --from=deps --chown=ereo:ereo /app/node_modules ./node_modules
|
|
106
|
+
COPY --from=builder --chown=ereo:ereo /app/.ereo ./.ereo
|
|
107
|
+
COPY --from=builder --chown=ereo:ereo /app/package.json ./
|
|
108
|
+
COPY --from=builder --chown=ereo:ereo /app/ereo.config.* ./
|
|
109
|
+
${tsconfigCopy}COPY --from=builder --chown=ereo:ereo /app/app ./app
|
|
110
|
+
COPY --from=builder --chown=ereo:ereo /app/public ./public
|
|
111
|
+
|
|
112
|
+
USER ereo
|
|
113
|
+
|
|
114
|
+
EXPOSE 3000
|
|
115
|
+
|
|
116
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
117
|
+
CMD bun -e "fetch('http://localhost:3000').then(r=>{if(!r.ok)throw 1}).catch(()=>process.exit(1))"
|
|
118
|
+
|
|
119
|
+
CMD ["bun", "run", "start"]
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function generateDockerignore() {
|
|
123
|
+
return `node_modules
|
|
124
|
+
dist
|
|
125
|
+
.ereo
|
|
126
|
+
.git
|
|
127
|
+
.gitignore
|
|
128
|
+
.env
|
|
129
|
+
.env.*
|
|
130
|
+
!.env.example
|
|
131
|
+
Dockerfile
|
|
132
|
+
*.log
|
|
133
|
+
*.md
|
|
134
|
+
.DS_Store
|
|
135
|
+
.vscode
|
|
136
|
+
.idea
|
|
137
|
+
coverage
|
|
138
|
+
*.tgz
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
60
141
|
async function generateMinimalProject(projectDir, projectName, typescript) {
|
|
61
142
|
const ext = typescript ? "tsx" : "jsx";
|
|
62
143
|
await mkdir(projectDir, { recursive: true });
|
|
@@ -72,12 +153,12 @@ async function generateMinimalProject(projectDir, projectName, typescript) {
|
|
|
72
153
|
start: "ereo start"
|
|
73
154
|
},
|
|
74
155
|
dependencies: {
|
|
75
|
-
"@ereo/core":
|
|
76
|
-
"@ereo/router":
|
|
77
|
-
"@ereo/server":
|
|
78
|
-
"@ereo/client":
|
|
79
|
-
"@ereo/data":
|
|
80
|
-
"@ereo/cli":
|
|
156
|
+
"@ereo/core": EREO_VERSION,
|
|
157
|
+
"@ereo/router": EREO_VERSION,
|
|
158
|
+
"@ereo/server": EREO_VERSION,
|
|
159
|
+
"@ereo/client": EREO_VERSION,
|
|
160
|
+
"@ereo/data": EREO_VERSION,
|
|
161
|
+
"@ereo/cli": EREO_VERSION,
|
|
81
162
|
react: "^18.2.0",
|
|
82
163
|
"react-dom": "^18.2.0"
|
|
83
164
|
},
|
|
@@ -107,6 +188,88 @@ export default function RootLayout({ children }${typescript ? ": { children: Rea
|
|
|
107
188
|
<meta charSet="utf-8" />
|
|
108
189
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
109
190
|
<title>${projectName}</title>
|
|
191
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
192
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
|
193
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
194
|
+
<style dangerouslySetInnerHTML={{ __html: \`
|
|
195
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
196
|
+
:root {
|
|
197
|
+
--brand-500: #6366f1;
|
|
198
|
+
--brand-600: #4f46e5;
|
|
199
|
+
--brand-400: #818cf8;
|
|
200
|
+
--purple-500: #8b5cf6;
|
|
201
|
+
--violet-500: #a855f7;
|
|
202
|
+
--bg: #ffffff;
|
|
203
|
+
--bg-soft: #f8fafc;
|
|
204
|
+
--bg-card: #ffffff;
|
|
205
|
+
--text: #0f172a;
|
|
206
|
+
--text-soft: #64748b;
|
|
207
|
+
--border: #e2e8f0;
|
|
208
|
+
--code-bg: #1e1e2e;
|
|
209
|
+
}
|
|
210
|
+
@media (prefers-color-scheme: dark) {
|
|
211
|
+
:root {
|
|
212
|
+
--bg: #0f172a;
|
|
213
|
+
--bg-soft: #1e293b;
|
|
214
|
+
--bg-card: #1e293b;
|
|
215
|
+
--text: #f1f5f9;
|
|
216
|
+
--text-soft: #94a3b8;
|
|
217
|
+
--border: #334155;
|
|
218
|
+
--code-bg: #0f172a;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
body {
|
|
222
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
223
|
+
background: var(--bg);
|
|
224
|
+
color: var(--text);
|
|
225
|
+
line-height: 1.6;
|
|
226
|
+
-webkit-font-smoothing: antialiased;
|
|
227
|
+
}
|
|
228
|
+
@keyframes fadeInUp {
|
|
229
|
+
from { opacity: 0; transform: translateY(24px); }
|
|
230
|
+
to { opacity: 1; transform: translateY(0); }
|
|
231
|
+
}
|
|
232
|
+
@keyframes float {
|
|
233
|
+
0%, 100% { transform: translateY(0); }
|
|
234
|
+
50% { transform: translateY(-10px); }
|
|
235
|
+
}
|
|
236
|
+
@keyframes gradientShift {
|
|
237
|
+
0%, 100% { background-position: 0% 50%; }
|
|
238
|
+
50% { background-position: 100% 50%; }
|
|
239
|
+
}
|
|
240
|
+
.hero { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; position: relative; overflow: hidden; }
|
|
241
|
+
.hero::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.15) 0%, transparent 70%); pointer-events: none; }
|
|
242
|
+
.hero-logo { animation: float 4s ease-in-out infinite; margin-bottom: 2rem; }
|
|
243
|
+
.gradient-text { background: linear-gradient(135deg, var(--brand-500), var(--purple-500), var(--violet-500)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; background-size: 200% 200%; animation: gradientShift 6s ease infinite; }
|
|
244
|
+
.hero h1 { font-size: clamp(2.5rem, 6vw, 4.5rem); font-weight: 800; letter-spacing: -0.03em; margin-bottom: 1rem; animation: fadeInUp 0.6s ease both; }
|
|
245
|
+
.hero p { font-size: 1.25rem; color: var(--text-soft); max-width: 540px; margin-bottom: 2rem; animation: fadeInUp 0.6s ease 0.15s both; }
|
|
246
|
+
.btn-group { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; animation: fadeInUp 0.6s ease 0.3s both; }
|
|
247
|
+
.cta-btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.75rem; border-radius: 0.75rem; font-weight: 600; font-size: 0.95rem; text-decoration: none; transition: all 0.2s; }
|
|
248
|
+
.cta-btn-primary { background: var(--brand-500); color: white; }
|
|
249
|
+
.cta-btn-primary:hover { background: var(--brand-600); transform: translateY(-1px); box-shadow: 0 8px 24px rgba(99,102,241,0.3); }
|
|
250
|
+
.cta-btn-secondary { border: 2px solid var(--border); color: var(--text); background: transparent; }
|
|
251
|
+
.cta-btn-secondary:hover { border-color: var(--brand-500); color: var(--brand-500); }
|
|
252
|
+
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; max-width: 72rem; margin: 0 auto; padding: 5rem 2rem; }
|
|
253
|
+
.feature-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 1rem; padding: 2rem; transition: all 0.25s; }
|
|
254
|
+
.feature-card:hover { border-color: var(--brand-400); transform: translateY(-4px); box-shadow: 0 12px 32px rgba(99,102,241,0.1); }
|
|
255
|
+
.feature-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 1rem; background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.1)); }
|
|
256
|
+
.feature-card h3 { font-size: 1.15rem; font-weight: 700; margin-bottom: 0.5rem; }
|
|
257
|
+
.feature-card p { color: var(--text-soft); font-size: 0.925rem; }
|
|
258
|
+
.code-window { background: var(--code-bg); border-radius: 1rem; overflow: hidden; max-width: 540px; margin: 0 auto; text-align: left; animation: fadeInUp 0.6s ease 0.45s both; }
|
|
259
|
+
.code-header { display: flex; align-items: center; gap: 6px; padding: 0.875rem 1.25rem; background: rgba(255,255,255,0.05); }
|
|
260
|
+
.code-dot { width: 12px; height: 12px; border-radius: 50%; }
|
|
261
|
+
.code-dot-r { background: #ff5f57; }
|
|
262
|
+
.code-dot-y { background: #febc2e; }
|
|
263
|
+
.code-dot-g { background: #28c840; }
|
|
264
|
+
.code-body { padding: 1.25rem; font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; color: #a5b4fc; line-height: 1.8; }
|
|
265
|
+
.code-body .prompt { color: #6ee7b7; }
|
|
266
|
+
.quickstart-section { background: var(--bg-soft); padding: 5rem 2rem; text-align: center; }
|
|
267
|
+
.quickstart-section h2 { font-size: 2rem; font-weight: 700; margin-bottom: 1rem; }
|
|
268
|
+
.quickstart-section .subtitle { color: var(--text-soft); margin-bottom: 2rem; }
|
|
269
|
+
.site-footer { text-align: center; padding: 3rem 2rem; color: var(--text-soft); font-size: 0.875rem; border-top: 1px solid var(--border); }
|
|
270
|
+
.site-footer a { color: var(--brand-500); text-decoration: none; }
|
|
271
|
+
.site-footer a:hover { text-decoration: underline; }
|
|
272
|
+
\` }} />
|
|
110
273
|
</head>
|
|
111
274
|
<body>
|
|
112
275
|
{children}
|
|
@@ -119,10 +282,102 @@ export default function RootLayout({ children }${typescript ? ": { children: Rea
|
|
|
119
282
|
const indexPage = `
|
|
120
283
|
export default function HomePage() {
|
|
121
284
|
return (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
285
|
+
<>
|
|
286
|
+
{/* Hero */}
|
|
287
|
+
<section className="hero">
|
|
288
|
+
<div className="hero-logo">
|
|
289
|
+
<svg width="72" height="72" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
290
|
+
<path d="M40 8L72 24V56L40 72L8 56V24L40 8Z" stroke="url(#logo-grad)" strokeWidth="3" fill="none" />
|
|
291
|
+
<path d="M40 20L60 30V50L40 60L20 50V30L40 20Z" fill="url(#logo-grad)" opacity="0.15" />
|
|
292
|
+
<path d="M40 28L52 34V46L40 52L28 46V34L40 28Z" fill="url(#logo-grad)" />
|
|
293
|
+
<defs>
|
|
294
|
+
<linearGradient id="logo-grad" x1="8" y1="8" x2="72" y2="72">
|
|
295
|
+
<stop stopColor="#6366f1" />
|
|
296
|
+
<stop offset="1" stopColor="#a855f7" />
|
|
297
|
+
</linearGradient>
|
|
298
|
+
</defs>
|
|
299
|
+
</svg>
|
|
300
|
+
</div>
|
|
301
|
+
<h1><span className="gradient-text">EreoJS</span></h1>
|
|
302
|
+
<p>A React fullstack framework built on Bun. Fast server-side rendering, file-based routing, and islands architecture.</p>
|
|
303
|
+
<div className="btn-group">
|
|
304
|
+
<a href="https://ereo.dev/docs" className="cta-btn cta-btn-primary">Get Started</a>
|
|
305
|
+
<a href="https://github.com/nicholasgriffintn/ereo" className="cta-btn cta-btn-secondary">
|
|
306
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
307
|
+
GitHub
|
|
308
|
+
</a>
|
|
309
|
+
</div>
|
|
310
|
+
</section>
|
|
311
|
+
|
|
312
|
+
{/* Features */}
|
|
313
|
+
<div className="feature-grid">
|
|
314
|
+
<div className="feature-card">
|
|
315
|
+
<div className="feature-icon">
|
|
316
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
|
317
|
+
</div>
|
|
318
|
+
<h3>Bun-Powered</h3>
|
|
319
|
+
<p>Built on Bun for blazing-fast startup, builds, and runtime performance.</p>
|
|
320
|
+
</div>
|
|
321
|
+
<div className="feature-card">
|
|
322
|
+
<div className="feature-icon">
|
|
323
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
|
324
|
+
</div>
|
|
325
|
+
<h3>File Routing</h3>
|
|
326
|
+
<p>Intuitive file-based routing with nested layouts and dynamic segments.</p>
|
|
327
|
+
</div>
|
|
328
|
+
<div className="feature-card">
|
|
329
|
+
<div className="feature-icon">
|
|
330
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
331
|
+
</div>
|
|
332
|
+
<h3>Server-Side Rendering</h3>
|
|
333
|
+
<p>Stream HTML from the server for fast, SEO-friendly initial page loads.</p>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="feature-card">
|
|
336
|
+
<div className="feature-icon">
|
|
337
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></svg>
|
|
338
|
+
</div>
|
|
339
|
+
<h3>Islands Architecture</h3>
|
|
340
|
+
<p>Selective hydration \u2014 only interactive parts ship JavaScript to the client.</p>
|
|
341
|
+
</div>
|
|
342
|
+
<div className="feature-card">
|
|
343
|
+
<div className="feature-icon">
|
|
344
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
345
|
+
</div>
|
|
346
|
+
<h3>Loaders & Actions</h3>
|
|
347
|
+
<p>Server-side data loading and form handling with simple async functions.</p>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="feature-card">
|
|
350
|
+
<div className="feature-icon">
|
|
351
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
352
|
+
</div>
|
|
353
|
+
<h3>TypeScript First</h3>
|
|
354
|
+
<p>Full type safety out of the box with zero-config TypeScript support.</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Quick Start */}
|
|
359
|
+
<section className="quickstart-section">
|
|
360
|
+
<h2>Get Started in Seconds</h2>
|
|
361
|
+
<p className="subtitle">One command to scaffold your project.</p>
|
|
362
|
+
<div className="code-window">
|
|
363
|
+
<div className="code-header">
|
|
364
|
+
<span className="code-dot code-dot-r" />
|
|
365
|
+
<span className="code-dot code-dot-y" />
|
|
366
|
+
<span className="code-dot code-dot-g" />
|
|
367
|
+
</div>
|
|
368
|
+
<div className="code-body">
|
|
369
|
+
<div><span className="prompt">$</span> bunx create-ereo my-app</div>
|
|
370
|
+
<div><span className="prompt">$</span> cd my-app</div>
|
|
371
|
+
<div><span className="prompt">$</span> bun run dev</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</section>
|
|
375
|
+
|
|
376
|
+
{/* Footer */}
|
|
377
|
+
<footer className="site-footer">
|
|
378
|
+
<p>Built with EreoJS — <a href="https://ereo.dev/docs">Docs</a> · <a href="https://github.com/nicholasgriffintn/ereo">GitHub</a></p>
|
|
379
|
+
</footer>
|
|
380
|
+
</>
|
|
126
381
|
);
|
|
127
382
|
}
|
|
128
383
|
`.trim();
|
|
@@ -151,6 +406,8 @@ dist
|
|
|
151
406
|
.DS_Store
|
|
152
407
|
.env
|
|
153
408
|
.env.local`);
|
|
409
|
+
await Bun.write(join(projectDir, "Dockerfile"), generateDockerfile(typescript));
|
|
410
|
+
await Bun.write(join(projectDir, ".dockerignore"), generateDockerignore());
|
|
154
411
|
}
|
|
155
412
|
async function generateTailwindProject(projectDir, projectName, typescript) {
|
|
156
413
|
const ext = typescript ? "tsx" : "jsx";
|
|
@@ -172,20 +429,20 @@ async function generateTailwindProject(projectDir, projectName, typescript) {
|
|
|
172
429
|
typecheck: "tsc --noEmit"
|
|
173
430
|
},
|
|
174
431
|
dependencies: {
|
|
175
|
-
"@ereo/core":
|
|
176
|
-
"@ereo/router":
|
|
177
|
-
"@ereo/server":
|
|
178
|
-
"@ereo/client":
|
|
179
|
-
"@ereo/data":
|
|
180
|
-
"@ereo/cli":
|
|
181
|
-
"@ereo/runtime-bun":
|
|
182
|
-
"@ereo/plugin-tailwind":
|
|
432
|
+
"@ereo/core": EREO_VERSION,
|
|
433
|
+
"@ereo/router": EREO_VERSION,
|
|
434
|
+
"@ereo/server": EREO_VERSION,
|
|
435
|
+
"@ereo/client": EREO_VERSION,
|
|
436
|
+
"@ereo/data": EREO_VERSION,
|
|
437
|
+
"@ereo/cli": EREO_VERSION,
|
|
438
|
+
"@ereo/runtime-bun": EREO_VERSION,
|
|
439
|
+
"@ereo/plugin-tailwind": EREO_VERSION,
|
|
183
440
|
react: "^18.2.0",
|
|
184
441
|
"react-dom": "^18.2.0"
|
|
185
442
|
},
|
|
186
443
|
devDependencies: {
|
|
187
|
-
"@ereo/testing":
|
|
188
|
-
"@ereo/dev-inspector":
|
|
444
|
+
"@ereo/testing": EREO_VERSION,
|
|
445
|
+
"@ereo/dev-inspector": EREO_VERSION,
|
|
189
446
|
...ts ? {
|
|
190
447
|
"@types/bun": "^1.1.0",
|
|
191
448
|
"@types/react": "^18.2.0",
|
|
@@ -249,18 +506,48 @@ export default {
|
|
|
249
506
|
darkMode: 'class',
|
|
250
507
|
theme: {
|
|
251
508
|
extend: {
|
|
509
|
+
fontFamily: {
|
|
510
|
+
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
|
511
|
+
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
|
512
|
+
},
|
|
252
513
|
colors: {
|
|
253
514
|
primary: {
|
|
254
|
-
50: '#
|
|
255
|
-
100: '#
|
|
256
|
-
200: '#
|
|
257
|
-
300: '#
|
|
258
|
-
400: '#
|
|
259
|
-
500: '#
|
|
260
|
-
600: '#
|
|
261
|
-
700: '#
|
|
262
|
-
800: '#
|
|
263
|
-
900: '#
|
|
515
|
+
50: '#eef2ff',
|
|
516
|
+
100: '#e0e7ff',
|
|
517
|
+
200: '#c7d2fe',
|
|
518
|
+
300: '#a5b4fc',
|
|
519
|
+
400: '#818cf8',
|
|
520
|
+
500: '#6366f1',
|
|
521
|
+
600: '#4f46e5',
|
|
522
|
+
700: '#4338ca',
|
|
523
|
+
800: '#3730a3',
|
|
524
|
+
900: '#312e81',
|
|
525
|
+
950: '#1e1b4b',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
animation: {
|
|
529
|
+
'fade-in': 'fadeIn 0.5s ease forwards',
|
|
530
|
+
'slide-up': 'slideUp 0.6s ease forwards',
|
|
531
|
+
'float': 'float 4s ease-in-out infinite',
|
|
532
|
+
'gradient': 'gradientShift 6s ease infinite',
|
|
533
|
+
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
|
534
|
+
},
|
|
535
|
+
keyframes: {
|
|
536
|
+
fadeIn: {
|
|
537
|
+
'0%': { opacity: '0' },
|
|
538
|
+
'100%': { opacity: '1' },
|
|
539
|
+
},
|
|
540
|
+
slideUp: {
|
|
541
|
+
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
|
542
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
543
|
+
},
|
|
544
|
+
float: {
|
|
545
|
+
'0%, 100%': { transform: 'translateY(0)' },
|
|
546
|
+
'50%': { transform: 'translateY(-10px)' },
|
|
547
|
+
},
|
|
548
|
+
gradientShift: {
|
|
549
|
+
'0%, 100%': { backgroundPosition: '0% 50%' },
|
|
550
|
+
'50%': { backgroundPosition: '100% 50%' },
|
|
264
551
|
},
|
|
265
552
|
},
|
|
266
553
|
},
|
|
@@ -270,22 +557,24 @@ export default {
|
|
|
270
557
|
`.trim();
|
|
271
558
|
await Bun.write(join(projectDir, "tailwind.config.js"), tailwindConfig);
|
|
272
559
|
const styles = `
|
|
560
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
561
|
+
|
|
273
562
|
@tailwind base;
|
|
274
563
|
@tailwind components;
|
|
275
564
|
@tailwind utilities;
|
|
276
565
|
|
|
277
566
|
@layer base {
|
|
278
567
|
body {
|
|
279
|
-
@apply antialiased;
|
|
568
|
+
@apply antialiased font-sans;
|
|
280
569
|
}
|
|
281
570
|
}
|
|
282
571
|
|
|
283
572
|
@layer components {
|
|
284
573
|
.btn {
|
|
285
|
-
@apply px-4 py-2 rounded-lg font-medium transition-
|
|
574
|
+
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
|
|
286
575
|
}
|
|
287
576
|
.btn-primary {
|
|
288
|
-
@apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
|
|
577
|
+
@apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 hover:-translate-y-0.5 hover:shadow-lg;
|
|
289
578
|
}
|
|
290
579
|
.btn-secondary {
|
|
291
580
|
@apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600;
|
|
@@ -296,6 +585,33 @@ export default {
|
|
|
296
585
|
.card {
|
|
297
586
|
@apply bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6;
|
|
298
587
|
}
|
|
588
|
+
.gradient-text {
|
|
589
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary-500 via-purple-500 to-violet-500;
|
|
590
|
+
background-size: 200% 200%;
|
|
591
|
+
animation: gradientShift 6s ease infinite;
|
|
592
|
+
}
|
|
593
|
+
.code-window {
|
|
594
|
+
@apply rounded-xl overflow-hidden;
|
|
595
|
+
background: #1e1e2e;
|
|
596
|
+
}
|
|
597
|
+
.code-window-header {
|
|
598
|
+
@apply flex items-center gap-1.5 px-4 py-3;
|
|
599
|
+
background: rgba(255, 255, 255, 0.05);
|
|
600
|
+
}
|
|
601
|
+
.code-window-dot {
|
|
602
|
+
@apply w-3 h-3 rounded-full;
|
|
603
|
+
}
|
|
604
|
+
.glow {
|
|
605
|
+
box-shadow: 0 0 40px rgba(99, 102, 241, 0.15), 0 0 80px rgba(99, 102, 241, 0.05);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
@layer utilities {
|
|
610
|
+
.delay-100 { animation-delay: 100ms; }
|
|
611
|
+
.delay-200 { animation-delay: 200ms; }
|
|
612
|
+
.delay-300 { animation-delay: 300ms; }
|
|
613
|
+
.delay-400 { animation-delay: 400ms; }
|
|
614
|
+
.delay-500 { animation-delay: 500ms; }
|
|
299
615
|
}
|
|
300
616
|
`.trim();
|
|
301
617
|
await Bun.write(join(projectDir, "app/styles.css"), styles);
|
|
@@ -495,8 +811,17 @@ export function Navigation() {
|
|
|
495
811
|
<div className="max-w-6xl mx-auto px-4">
|
|
496
812
|
<div className="flex items-center justify-between h-16">
|
|
497
813
|
{/* Logo */}
|
|
498
|
-
<a href="/" className="flex items-center space-x-2">
|
|
499
|
-
<
|
|
814
|
+
<a href="/" className="flex items-center space-x-2 group">
|
|
815
|
+
<svg width="28" height="28" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg" className="transition-transform group-hover:scale-110">
|
|
816
|
+
<path d="M40 8L72 24V56L40 72L8 56V24L40 8Z" stroke="url(#nav-grad)" strokeWidth="3" fill="none" />
|
|
817
|
+
<path d="M40 28L52 34V46L40 52L28 46V34L40 28Z" fill="url(#nav-grad)" />
|
|
818
|
+
<defs>
|
|
819
|
+
<linearGradient id="nav-grad" x1="8" y1="8" x2="72" y2="72">
|
|
820
|
+
<stop stopColor="#6366f1" />
|
|
821
|
+
<stop offset="1" stopColor="#a855f7" />
|
|
822
|
+
</linearGradient>
|
|
823
|
+
</defs>
|
|
824
|
+
</svg>
|
|
500
825
|
<span className="font-bold text-xl">EreoJS</span>
|
|
501
826
|
</a>
|
|
502
827
|
|
|
@@ -671,6 +996,9 @@ export default function RootLayout({ children }${ts ? ": RootLayoutProps" : ""})
|
|
|
671
996
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
672
997
|
<meta name="description" content="A modern web application built with EreoJS" />
|
|
673
998
|
<title>${projectName}</title>
|
|
999
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
1000
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
|
1001
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
674
1002
|
<link rel="stylesheet" href="/app/styles.css" />
|
|
675
1003
|
</head>
|
|
676
1004
|
<body className="min-h-screen flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
@@ -726,100 +1054,210 @@ export default function HomePage({ loaderData }${ts ? ": HomePageProps" : ""}) {
|
|
|
726
1054
|
const { featuredPost, stats } = loaderData;
|
|
727
1055
|
|
|
728
1056
|
return (
|
|
729
|
-
<div
|
|
730
|
-
{/* Hero
|
|
731
|
-
<section className="
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1057
|
+
<div>
|
|
1058
|
+
{/* Hero */}
|
|
1059
|
+
<section className="relative min-h-[90vh] flex flex-col items-center justify-center text-center px-4 overflow-hidden">
|
|
1060
|
+
{/* Background */}
|
|
1061
|
+
<div className="absolute inset-0 bg-gradient-to-b from-primary-50/50 via-transparent to-transparent dark:from-primary-950/30 dark:via-transparent dark:to-transparent" />
|
|
1062
|
+
<div className="absolute inset-0" style={{ background: 'radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.12) 0%, transparent 60%)' }} />
|
|
1063
|
+
|
|
1064
|
+
<div className="relative z-10 max-w-4xl mx-auto">
|
|
1065
|
+
{/* Logo */}
|
|
1066
|
+
<div className="animate-float mb-8 opacity-0 animate-fade-in">
|
|
1067
|
+
<svg width="72" height="72" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1068
|
+
<path d="M40 8L72 24V56L40 72L8 56V24L40 8Z" stroke="url(#hero-grad)" strokeWidth="3" fill="none" />
|
|
1069
|
+
<path d="M40 20L60 30V50L40 60L20 50V30L40 20Z" fill="url(#hero-grad)" opacity="0.15" />
|
|
1070
|
+
<path d="M40 28L52 34V46L40 52L28 46V34L40 28Z" fill="url(#hero-grad)" />
|
|
1071
|
+
<defs>
|
|
1072
|
+
<linearGradient id="hero-grad" x1="8" y1="8" x2="72" y2="72">
|
|
1073
|
+
<stop stopColor="#6366f1" />
|
|
1074
|
+
<stop offset="1" stopColor="#a855f7" />
|
|
1075
|
+
</linearGradient>
|
|
1076
|
+
</defs>
|
|
1077
|
+
</svg>
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
{/* Version Badge */}
|
|
1081
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 text-sm font-medium mb-6 opacity-0 animate-slide-up">
|
|
1082
|
+
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse-slow" />
|
|
1083
|
+
v${EREO_VERSION.slice(1)}
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
<h1 className="text-5xl sm:text-6xl md:text-7xl font-extrabold tracking-tight mb-6 opacity-0 animate-slide-up delay-100">
|
|
1087
|
+
Build Faster with <span className="gradient-text">EreoJS</span>
|
|
735
1088
|
</h1>
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
Fast, simple, and powerful.
|
|
1089
|
+
|
|
1090
|
+
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10 opacity-0 animate-slide-up delay-200">
|
|
1091
|
+
A React fullstack framework built on Bun. Server-side rendering, file-based routing, and islands architecture out of the box.
|
|
740
1092
|
</p>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1093
|
+
|
|
1094
|
+
{/* Terminal Preview */}
|
|
1095
|
+
<div className="code-window glow max-w-md mx-auto text-left opacity-0 animate-slide-up delay-300">
|
|
1096
|
+
<div className="code-window-header">
|
|
1097
|
+
<span className="code-window-dot bg-red-500" />
|
|
1098
|
+
<span className="code-window-dot bg-yellow-500" />
|
|
1099
|
+
<span className="code-window-dot bg-green-500" />
|
|
1100
|
+
</div>
|
|
1101
|
+
<div className="px-5 py-4 font-mono text-sm text-primary-300 leading-relaxed">
|
|
1102
|
+
<div><span className="text-emerald-400">$</span> bunx create-ereo my-app</div>
|
|
1103
|
+
<div><span className="text-emerald-400">$</span> cd my-app</div>
|
|
1104
|
+
<div><span className="text-emerald-400">$</span> bun run dev</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
|
|
1108
|
+
{/* CTA Buttons */}
|
|
1109
|
+
<div className="flex flex-wrap gap-4 justify-center mt-10 opacity-0 animate-slide-up delay-400">
|
|
1110
|
+
<a href="https://ereo.dev/docs" className="btn btn-primary text-base px-6 py-3">
|
|
1111
|
+
Get Started
|
|
744
1112
|
</a>
|
|
745
|
-
<a
|
|
746
|
-
|
|
747
|
-
target="_blank"
|
|
748
|
-
rel="noopener"
|
|
749
|
-
className="btn border-2 border-white text-white hover:bg-white/10"
|
|
750
|
-
>
|
|
751
|
-
View on GitHub
|
|
1113
|
+
<a href="/blog" className="btn btn-secondary text-base px-6 py-3">
|
|
1114
|
+
Read the Blog
|
|
752
1115
|
</a>
|
|
753
1116
|
</div>
|
|
754
1117
|
</div>
|
|
755
1118
|
</section>
|
|
756
1119
|
|
|
757
|
-
{/* Features
|
|
758
|
-
<section className="py-
|
|
1120
|
+
{/* Features */}
|
|
1121
|
+
<section className="py-20 px-4">
|
|
759
1122
|
<div className="max-w-6xl mx-auto">
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
<
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1123
|
+
<div className="text-center mb-16">
|
|
1124
|
+
<h2 className="text-3xl sm:text-4xl font-bold mb-4">Everything You Need</h2>
|
|
1125
|
+
<p className="text-gray-600 dark:text-gray-400 text-lg max-w-2xl mx-auto">
|
|
1126
|
+
EreoJS combines the best of React with Bun's performance to deliver a complete fullstack framework.
|
|
1127
|
+
</p>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
1130
|
+
{[
|
|
1131
|
+
{ icon: 'bolt', title: 'Bun-Powered', desc: 'Lightning-fast startup, builds, and runtime. 10x faster than Node.js for common operations.' },
|
|
1132
|
+
{ icon: 'folder', title: 'File Routing', desc: 'Intuitive file-based routing with nested layouts, dynamic segments, and automatic code splitting.' },
|
|
1133
|
+
{ icon: 'server', title: 'Server-Side Rendering', desc: 'Stream HTML from the server for fast initial loads with full SEO support.' },
|
|
1134
|
+
{ icon: 'island', title: 'Islands Architecture', desc: 'Selective hydration means only interactive parts ship JavaScript to the client.' },
|
|
1135
|
+
{ icon: 'data', title: 'Loaders & Actions', desc: 'Simple async functions for server-side data loading and form handling.' },
|
|
1136
|
+
{ icon: 'ts', title: 'TypeScript First', desc: 'Full type safety with zero-config TypeScript. Types flow from server to client.' },
|
|
1137
|
+
].map((feature) => (
|
|
1138
|
+
<div key={feature.title} className="group card hover:shadow-xl hover:-translate-y-1 transition-all duration-300 border border-transparent hover:border-primary-200 dark:hover:border-primary-800">
|
|
1139
|
+
<div className="w-12 h-12 rounded-xl flex items-center justify-center mb-4 bg-gradient-to-br from-primary-100 to-purple-100 dark:from-primary-900/40 dark:to-purple-900/40 group-hover:from-primary-200 group-hover:to-purple-200 dark:group-hover:from-primary-900/60 dark:group-hover:to-purple-900/60 transition-colors">
|
|
1140
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1141
|
+
{feature.icon === 'bolt' && <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />}
|
|
1142
|
+
{feature.icon === 'folder' && <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />}
|
|
1143
|
+
{feature.icon === 'server' && <><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></>}
|
|
1144
|
+
{feature.icon === 'island' && <><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></>}
|
|
1145
|
+
{feature.icon === 'data' && <><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>}
|
|
1146
|
+
{feature.icon === 'ts' && <><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></>}
|
|
1147
|
+
</svg>
|
|
1148
|
+
</div>
|
|
1149
|
+
<h3 className="text-lg font-bold mb-2">{feature.title}</h3>
|
|
1150
|
+
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">{feature.desc}</p>
|
|
1151
|
+
</div>
|
|
1152
|
+
))}
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
</section>
|
|
1156
|
+
|
|
1157
|
+
{/* Code Showcase */}
|
|
1158
|
+
<section className="py-20 px-4 bg-gray-50 dark:bg-gray-800/50">
|
|
1159
|
+
<div className="max-w-6xl mx-auto">
|
|
1160
|
+
<div className="text-center mb-16">
|
|
1161
|
+
<h2 className="text-3xl sm:text-4xl font-bold mb-4">Simple, Powerful APIs</h2>
|
|
1162
|
+
<p className="text-gray-600 dark:text-gray-400 text-lg">Loaders fetch data. Actions handle mutations. It's that simple.</p>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div className="grid md:grid-cols-2 gap-6">
|
|
1165
|
+
<div className="code-window">
|
|
1166
|
+
<div className="code-window-header">
|
|
1167
|
+
<span className="code-window-dot bg-red-500" />
|
|
1168
|
+
<span className="code-window-dot bg-yellow-500" />
|
|
1169
|
+
<span className="code-window-dot bg-green-500" />
|
|
1170
|
+
<span className="ml-3 text-xs text-gray-500 font-mono">routes/users.tsx</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
<pre className="px-5 py-4 font-mono text-sm text-primary-300 leading-relaxed overflow-x-auto">
|
|
1173
|
+
{\`// Data runs on the server
|
|
1174
|
+
export async function loader() {
|
|
1175
|
+
const users = await db.user.findMany();
|
|
1176
|
+
return { users };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Component renders on server + client
|
|
1180
|
+
export default function Users({ loaderData }) {
|
|
1181
|
+
return (
|
|
1182
|
+
<ul>
|
|
1183
|
+
{loaderData.users.map(user => (
|
|
1184
|
+
<li key={user.id}>{user.name}</li>
|
|
1185
|
+
))}
|
|
1186
|
+
</ul>
|
|
1187
|
+
);
|
|
1188
|
+
}\`}
|
|
1189
|
+
</pre>
|
|
775
1190
|
</div>
|
|
776
|
-
<div className="
|
|
777
|
-
<div className="
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1191
|
+
<div className="code-window">
|
|
1192
|
+
<div className="code-window-header">
|
|
1193
|
+
<span className="code-window-dot bg-red-500" />
|
|
1194
|
+
<span className="code-window-dot bg-yellow-500" />
|
|
1195
|
+
<span className="code-window-dot bg-green-500" />
|
|
1196
|
+
<span className="ml-3 text-xs text-gray-500 font-mono">routes/contact.tsx</span>
|
|
1197
|
+
</div>
|
|
1198
|
+
<pre className="px-5 py-4 font-mono text-sm text-primary-300 leading-relaxed overflow-x-auto">
|
|
1199
|
+
{\`// Actions handle form submissions
|
|
1200
|
+
export async function action({ request }) {
|
|
1201
|
+
const form = await request.formData();
|
|
1202
|
+
await db.message.create({
|
|
1203
|
+
data: {
|
|
1204
|
+
name: form.get('name'),
|
|
1205
|
+
email: form.get('email'),
|
|
1206
|
+
body: form.get('message'),
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
return { success: true };
|
|
1210
|
+
}\`}
|
|
1211
|
+
</pre>
|
|
782
1212
|
</div>
|
|
783
1213
|
</div>
|
|
784
1214
|
</div>
|
|
785
1215
|
</section>
|
|
786
1216
|
|
|
787
|
-
{/* Interactive Demo
|
|
788
|
-
<section className="py-
|
|
789
|
-
<div className="max-w-4xl mx-auto
|
|
790
|
-
<
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
<
|
|
1217
|
+
{/* Interactive Demo */}
|
|
1218
|
+
<section className="py-20 px-4">
|
|
1219
|
+
<div className="max-w-4xl mx-auto">
|
|
1220
|
+
<div className="card text-center border border-gray-200 dark:border-gray-700">
|
|
1221
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 text-xs font-medium mb-6">
|
|
1222
|
+
Interactive Island
|
|
1223
|
+
</div>
|
|
1224
|
+
<h2 className="text-3xl font-bold mb-3">Islands Architecture</h2>
|
|
1225
|
+
<p className="text-gray-600 dark:text-gray-400 mb-8 max-w-lg mx-auto">
|
|
1226
|
+
This counter is an island \u2014 it's the only part of this page that ships JavaScript. The rest is pure HTML from the server.
|
|
1227
|
+
</p>
|
|
1228
|
+
<div className="flex justify-center">
|
|
1229
|
+
<Counter initialCount={0} />
|
|
1230
|
+
</div>
|
|
796
1231
|
</div>
|
|
797
1232
|
</div>
|
|
798
1233
|
</section>
|
|
799
1234
|
|
|
800
|
-
{/* Server Data
|
|
801
|
-
<section className="py-
|
|
1235
|
+
{/* Server Data */}
|
|
1236
|
+
<section className="py-20 px-4 bg-gray-50 dark:bg-gray-800/50">
|
|
802
1237
|
<div className="max-w-4xl mx-auto">
|
|
803
|
-
<div className="card">
|
|
804
|
-
<
|
|
1238
|
+
<div className="card border border-gray-200 dark:border-gray-700">
|
|
1239
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-300 text-xs font-medium mb-6">
|
|
1240
|
+
Server Loader Data
|
|
1241
|
+
</div>
|
|
1242
|
+
<h2 className="text-2xl font-bold mb-2">Loaded at Build Time</h2>
|
|
805
1243
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
806
|
-
This data was
|
|
1244
|
+
This data was fetched on the server via a loader function \u2014 zero client-side fetching.
|
|
807
1245
|
</p>
|
|
808
|
-
<div className="grid
|
|
809
|
-
<div className="p-
|
|
1246
|
+
<div className="grid sm:grid-cols-2 gap-4 mb-6">
|
|
1247
|
+
<div className="p-5 bg-gray-50 dark:bg-gray-700/50 rounded-xl">
|
|
810
1248
|
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Blog Posts</div>
|
|
811
|
-
<div className="text-
|
|
1249
|
+
<div className="text-4xl font-extrabold">{stats.posts}</div>
|
|
812
1250
|
</div>
|
|
813
|
-
<div className="p-
|
|
1251
|
+
<div className="p-5 bg-gray-50 dark:bg-gray-700/50 rounded-xl">
|
|
814
1252
|
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Rendered At</div>
|
|
815
|
-
<div className="text-
|
|
1253
|
+
<div className="text-4xl font-extrabold font-mono">{stats.serverTime}</div>
|
|
816
1254
|
</div>
|
|
817
1255
|
</div>
|
|
818
1256
|
{featuredPost && (
|
|
819
|
-
<div className="
|
|
1257
|
+
<div className="pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
820
1258
|
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2">Featured Post</div>
|
|
821
1259
|
<h3 className="text-xl font-bold mb-2">
|
|
822
|
-
<a href={\`/blog/\${featuredPost.slug}\`} className="hover:text-primary-600">
|
|
1260
|
+
<a href={\`/blog/\${featuredPost.slug}\`} className="hover:text-primary-600 transition-colors">
|
|
823
1261
|
{featuredPost.title}
|
|
824
1262
|
</a>
|
|
825
1263
|
</h3>
|
|
@@ -829,6 +1267,26 @@ export default function HomePage({ loaderData }${ts ? ": HomePageProps" : ""}) {
|
|
|
829
1267
|
</div>
|
|
830
1268
|
</div>
|
|
831
1269
|
</section>
|
|
1270
|
+
|
|
1271
|
+
{/* CTA Footer */}
|
|
1272
|
+
<section className="py-24 px-4 text-center">
|
|
1273
|
+
<h2 className="text-3xl sm:text-4xl font-bold mb-4">Ready to Build?</h2>
|
|
1274
|
+
<p className="text-gray-600 dark:text-gray-400 text-lg mb-8">Get started with a single command.</p>
|
|
1275
|
+
<div className="code-window glow max-w-sm mx-auto mb-8">
|
|
1276
|
+
<div className="code-window-header">
|
|
1277
|
+
<span className="code-window-dot bg-red-500" />
|
|
1278
|
+
<span className="code-window-dot bg-yellow-500" />
|
|
1279
|
+
<span className="code-window-dot bg-green-500" />
|
|
1280
|
+
</div>
|
|
1281
|
+
<div className="px-5 py-3 font-mono text-sm text-primary-300">
|
|
1282
|
+
<span className="text-emerald-400">$</span> bunx create-ereo my-app
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
<div className="flex flex-wrap gap-4 justify-center">
|
|
1286
|
+
<a href="https://ereo.dev/docs" className="btn btn-primary text-base px-6 py-3">Documentation</a>
|
|
1287
|
+
<a href="https://github.com/ereo-js/ereo" target="_blank" rel="noopener" className="btn btn-secondary text-base px-6 py-3">GitHub</a>
|
|
1288
|
+
</div>
|
|
1289
|
+
</section>
|
|
832
1290
|
</div>
|
|
833
1291
|
);
|
|
834
1292
|
}
|
|
@@ -1311,6 +1769,8 @@ dist
|
|
|
1311
1769
|
.env
|
|
1312
1770
|
.env.local
|
|
1313
1771
|
.env.*.local`);
|
|
1772
|
+
await Bun.write(join(projectDir, "Dockerfile"), generateDockerfile(typescript));
|
|
1773
|
+
await Bun.write(join(projectDir, ".dockerignore"), generateDockerignore());
|
|
1314
1774
|
await Bun.write(join(projectDir, ".env.example"), `# Environment Variables
|
|
1315
1775
|
# Copy this file to .env and fill in your values
|
|
1316
1776
|
|
|
@@ -1438,8 +1898,18 @@ async function main() {
|
|
|
1438
1898
|
printHelp();
|
|
1439
1899
|
process.exit(1);
|
|
1440
1900
|
}
|
|
1901
|
+
if (/[<>:"|?*]/.test(projectName) || projectName.startsWith(".")) {
|
|
1902
|
+
console.error(` \x1B[31m\u2717\x1B[0m Invalid project name. Avoid special characters and leading dots.
|
|
1903
|
+
`);
|
|
1904
|
+
process.exit(1);
|
|
1905
|
+
}
|
|
1441
1906
|
const finalOptions = { ...defaultOptions, ...options };
|
|
1442
1907
|
const projectDir = resolve(process.cwd(), projectName);
|
|
1908
|
+
if (!projectDir.startsWith(process.cwd())) {
|
|
1909
|
+
console.error(` \x1B[31m\u2717\x1B[0m Invalid project name: path traversal detected.
|
|
1910
|
+
`);
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
}
|
|
1443
1913
|
console.log(` Creating \x1B[36m${projectName}\x1B[0m...
|
|
1444
1914
|
`);
|
|
1445
1915
|
console.log(` Template: ${finalOptions.template}`);
|