@flexireact/core 3.0.1 → 3.0.3
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/cli/index.js +1514 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/client/index.js +373 -0
- package/dist/core/client/index.js.map +1 -0
- package/dist/core/index.js +6415 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/server/index.js +3094 -0
- package/dist/core/server/index.js.map +1 -0
- package/package.json +80 -80
- package/bin/flexireact.js +0 -23
- package/cli/generators.ts +0 -616
- package/cli/index.ts +0 -1182
- package/core/actions/index.ts +0 -364
- package/core/api.ts +0 -143
- package/core/build/index.ts +0 -425
- package/core/cli/logger.ts +0 -353
- package/core/client/Link.tsx +0 -345
- package/core/client/hydration.ts +0 -147
- package/core/client/index.ts +0 -12
- package/core/client/islands.ts +0 -143
- package/core/client/navigation.ts +0 -212
- package/core/client/runtime.ts +0 -52
- package/core/config.ts +0 -116
- package/core/context.ts +0 -83
- package/core/dev.ts +0 -47
- package/core/devtools/index.ts +0 -644
- package/core/edge/cache.ts +0 -344
- package/core/edge/fetch-polyfill.ts +0 -247
- package/core/edge/handler.ts +0 -248
- package/core/edge/index.ts +0 -81
- package/core/edge/ppr.ts +0 -264
- package/core/edge/runtime.ts +0 -161
- package/core/font/index.ts +0 -306
- package/core/helpers.ts +0 -494
- package/core/image/index.ts +0 -413
- package/core/index.ts +0 -218
- package/core/islands/index.ts +0 -293
- package/core/loader.ts +0 -111
- package/core/logger.ts +0 -242
- package/core/metadata/index.ts +0 -622
- package/core/middleware/index.ts +0 -416
- package/core/plugins/index.ts +0 -373
- package/core/render/index.ts +0 -1243
- package/core/render.ts +0 -136
- package/core/router/index.ts +0 -551
- package/core/router.ts +0 -141
- package/core/rsc/index.ts +0 -199
- package/core/server/index.ts +0 -779
- package/core/server.ts +0 -203
- package/core/ssg/index.ts +0 -346
- package/core/start-dev.ts +0 -6
- package/core/start-prod.ts +0 -6
- package/core/tsconfig.json +0 -30
- package/core/types.ts +0 -239
- package/core/utils.ts +0 -176
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/index.ts
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import pc2 from "picocolors";
|
|
9
|
+
import prompts2 from "prompts";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
|
|
12
|
+
// cli/generators.ts
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import pc from "picocolors";
|
|
16
|
+
import prompts from "prompts";
|
|
17
|
+
var log = {
|
|
18
|
+
info: (msg) => console.log(`${pc.cyan("\u2139")} ${msg}`),
|
|
19
|
+
success: (msg) => console.log(`${pc.green("\u2713")} ${msg}`),
|
|
20
|
+
warn: (msg) => console.log(`${pc.yellow("\u26A0")} ${pc.yellow(msg)}`),
|
|
21
|
+
error: (msg) => console.log(`${pc.red("\u2717")} ${pc.red(msg)}`),
|
|
22
|
+
blank: () => console.log("")
|
|
23
|
+
};
|
|
24
|
+
var templates = {
|
|
25
|
+
// Page template
|
|
26
|
+
page: (name, options) => `${options.client ? "'use client';\n\n" : ""}import React from 'react';
|
|
27
|
+
|
|
28
|
+
export default function ${toPascalCase(name)}Page() {
|
|
29
|
+
return (
|
|
30
|
+
<div className="min-h-screen p-8">
|
|
31
|
+
<h1 className="text-4xl font-bold">${toPascalCase(name)}</h1>
|
|
32
|
+
<p className="text-gray-400 mt-4">Welcome to ${name} page</p>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
`,
|
|
37
|
+
// Layout template
|
|
38
|
+
layout: (name) => `import React from 'react';
|
|
39
|
+
|
|
40
|
+
interface ${toPascalCase(name)}LayoutProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function ${toPascalCase(name)}Layout({ children }: ${toPascalCase(name)}LayoutProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="${name}-layout">
|
|
47
|
+
{/* Add your layout wrapper here */}
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`,
|
|
53
|
+
// Component template
|
|
54
|
+
component: (name, options) => {
|
|
55
|
+
const propsInterface = options.props ? `
|
|
56
|
+
interface ${toPascalCase(name)}Props {
|
|
57
|
+
className?: string;
|
|
58
|
+
children?: React.ReactNode;
|
|
59
|
+
}
|
|
60
|
+
` : "";
|
|
61
|
+
const propsType = options.props ? `{ className, children }: ${toPascalCase(name)}Props` : "{}";
|
|
62
|
+
return `${options.client ? "'use client';\n\n" : ""}import React from 'react';
|
|
63
|
+
import { cn } from '@/lib/utils';
|
|
64
|
+
${propsInterface}
|
|
65
|
+
export function ${toPascalCase(name)}(${propsType}) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn('${toKebabCase(name)}', ${options.props ? "className" : "''"})}>
|
|
68
|
+
${options.props ? "{children}" : `{/* ${toPascalCase(name)} content */}`}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default ${toPascalCase(name)};
|
|
74
|
+
`;
|
|
75
|
+
},
|
|
76
|
+
// Hook template
|
|
77
|
+
hook: (name) => `import { useState, useEffect, useCallback } from 'react';
|
|
78
|
+
|
|
79
|
+
export function use${toPascalCase(name)}() {
|
|
80
|
+
const [state, setState] = useState(null);
|
|
81
|
+
const [loading, setLoading] = useState(false);
|
|
82
|
+
const [error, setError] = useState<Error | null>(null);
|
|
83
|
+
|
|
84
|
+
const execute = useCallback(async () => {
|
|
85
|
+
setLoading(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
// Add your logic here
|
|
89
|
+
setState(null);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
92
|
+
} finally {
|
|
93
|
+
setLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
return { state, loading, error, execute };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default use${toPascalCase(name)};
|
|
101
|
+
`,
|
|
102
|
+
// API route template
|
|
103
|
+
api: (name) => `import type { IncomingMessage, ServerResponse } from 'http';
|
|
104
|
+
|
|
105
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
106
|
+
const method = req.method;
|
|
107
|
+
|
|
108
|
+
switch (method) {
|
|
109
|
+
case 'GET':
|
|
110
|
+
return handleGet(req, res);
|
|
111
|
+
case 'POST':
|
|
112
|
+
return handlePost(req, res);
|
|
113
|
+
default:
|
|
114
|
+
res.statusCode = 405;
|
|
115
|
+
res.setHeader('Content-Type', 'application/json');
|
|
116
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleGet(req: IncomingMessage, res: ServerResponse) {
|
|
121
|
+
res.statusCode = 200;
|
|
122
|
+
res.setHeader('Content-Type', 'application/json');
|
|
123
|
+
res.end(JSON.stringify({ message: '${name} API - GET' }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handlePost(req: IncomingMessage, res: ServerResponse) {
|
|
127
|
+
// Parse body
|
|
128
|
+
let body = '';
|
|
129
|
+
for await (const chunk of req) {
|
|
130
|
+
body += chunk;
|
|
131
|
+
}
|
|
132
|
+
const data = body ? JSON.parse(body) : {};
|
|
133
|
+
|
|
134
|
+
res.statusCode = 200;
|
|
135
|
+
res.setHeader('Content-Type', 'application/json');
|
|
136
|
+
res.end(JSON.stringify({ message: '${name} API - POST', data }));
|
|
137
|
+
}
|
|
138
|
+
`,
|
|
139
|
+
// Server Action template
|
|
140
|
+
action: (name) => `'use server';
|
|
141
|
+
|
|
142
|
+
import { revalidatePath } from '@flexireact/core';
|
|
143
|
+
|
|
144
|
+
export async function ${toCamelCase(name)}Action(formData: FormData) {
|
|
145
|
+
// Validate input
|
|
146
|
+
const data = Object.fromEntries(formData);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Add your server logic here
|
|
150
|
+
console.log('${toPascalCase(name)} action executed:', data);
|
|
151
|
+
|
|
152
|
+
// Revalidate cache if needed
|
|
153
|
+
// revalidatePath('/');
|
|
154
|
+
|
|
155
|
+
return { success: true, data };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
// Middleware template
|
|
165
|
+
middleware: (name) => `import type { IncomingMessage, ServerResponse } from 'http';
|
|
166
|
+
|
|
167
|
+
export interface ${toPascalCase(name)}MiddlewareOptions {
|
|
168
|
+
// Add your options here
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function ${toCamelCase(name)}Middleware(options: ${toPascalCase(name)}MiddlewareOptions = {}) {
|
|
172
|
+
return async (
|
|
173
|
+
req: IncomingMessage,
|
|
174
|
+
res: ServerResponse,
|
|
175
|
+
next: () => Promise<void>
|
|
176
|
+
) => {
|
|
177
|
+
// Before request handling
|
|
178
|
+
console.log('[${toPascalCase(name)}] Request:', req.url);
|
|
179
|
+
|
|
180
|
+
// Continue to next middleware/handler
|
|
181
|
+
await next();
|
|
182
|
+
|
|
183
|
+
// After request handling (optional)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default ${toCamelCase(name)}Middleware;
|
|
188
|
+
`,
|
|
189
|
+
// Context template
|
|
190
|
+
context: (name) => `'use client';
|
|
191
|
+
|
|
192
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
193
|
+
|
|
194
|
+
interface ${toPascalCase(name)}State {
|
|
195
|
+
// Add your state properties here
|
|
196
|
+
value: string | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface ${toPascalCase(name)}ContextValue extends ${toPascalCase(name)}State {
|
|
200
|
+
setValue: (value: string) => void;
|
|
201
|
+
reset: () => void;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ${toPascalCase(name)}Context = createContext<${toPascalCase(name)}ContextValue | null>(null);
|
|
205
|
+
|
|
206
|
+
export function ${toPascalCase(name)}Provider({ children }: { children: React.ReactNode }) {
|
|
207
|
+
const [state, setState] = useState<${toPascalCase(name)}State>({
|
|
208
|
+
value: null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const setValue = useCallback((value: string) => {
|
|
212
|
+
setState(prev => ({ ...prev, value }));
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
const reset = useCallback(() => {
|
|
216
|
+
setState({ value: null });
|
|
217
|
+
}, []);
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<${toPascalCase(name)}Context.Provider value={{ ...state, setValue, reset }}>
|
|
221
|
+
{children}
|
|
222
|
+
</${toPascalCase(name)}Context.Provider>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function use${toPascalCase(name)}() {
|
|
227
|
+
const context = useContext(${toPascalCase(name)}Context);
|
|
228
|
+
if (!context) {
|
|
229
|
+
throw new Error('use${toPascalCase(name)} must be used within a ${toPascalCase(name)}Provider');
|
|
230
|
+
}
|
|
231
|
+
return context;
|
|
232
|
+
}
|
|
233
|
+
`,
|
|
234
|
+
// Loading template
|
|
235
|
+
loading: () => `import React from 'react';
|
|
236
|
+
import { Spinner } from '@flexireact/flexi-ui';
|
|
237
|
+
|
|
238
|
+
export default function Loading() {
|
|
239
|
+
return (
|
|
240
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
241
|
+
<Spinner size="lg" />
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
`,
|
|
246
|
+
// Error template
|
|
247
|
+
error: () => `'use client';
|
|
248
|
+
|
|
249
|
+
import React from 'react';
|
|
250
|
+
import { Button, Alert } from '@flexireact/flexi-ui';
|
|
251
|
+
|
|
252
|
+
interface ErrorProps {
|
|
253
|
+
error: Error;
|
|
254
|
+
reset: () => void;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default function Error({ error, reset }: ErrorProps) {
|
|
258
|
+
return (
|
|
259
|
+
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
|
260
|
+
<Alert variant="error" className="max-w-md mb-8">
|
|
261
|
+
<h2 className="font-bold text-lg mb-2">Something went wrong</h2>
|
|
262
|
+
<p className="text-sm">{error.message}</p>
|
|
263
|
+
</Alert>
|
|
264
|
+
<Button onClick={reset}>Try again</Button>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
`,
|
|
269
|
+
// Not found template
|
|
270
|
+
notFound: () => `import React from 'react';
|
|
271
|
+
import { Button } from '@flexireact/flexi-ui';
|
|
272
|
+
|
|
273
|
+
export default function NotFound() {
|
|
274
|
+
return (
|
|
275
|
+
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
|
276
|
+
<h1 className="text-8xl font-bold text-primary mb-4">404</h1>
|
|
277
|
+
<p className="text-gray-400 text-xl mb-8">Page not found</p>
|
|
278
|
+
<Button asChild>
|
|
279
|
+
<a href="/">\u2190 Back Home</a>
|
|
280
|
+
</Button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
`
|
|
285
|
+
};
|
|
286
|
+
function toPascalCase(str) {
|
|
287
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
288
|
+
}
|
|
289
|
+
function toCamelCase(str) {
|
|
290
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
291
|
+
}
|
|
292
|
+
function toKebabCase(str) {
|
|
293
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
294
|
+
}
|
|
295
|
+
function ensureDir(filePath) {
|
|
296
|
+
const dir = path.dirname(filePath);
|
|
297
|
+
if (!fs.existsSync(dir)) {
|
|
298
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function writeFile(filePath, content) {
|
|
302
|
+
ensureDir(filePath);
|
|
303
|
+
fs.writeFileSync(filePath, content);
|
|
304
|
+
}
|
|
305
|
+
async function runGenerate(type, name) {
|
|
306
|
+
const cwd = process.cwd();
|
|
307
|
+
if (!fs.existsSync(path.join(cwd, "package.json"))) {
|
|
308
|
+
log.error("Not in a FlexiReact project. Run this command in your project root.");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
if (!type) {
|
|
312
|
+
const response = await prompts([
|
|
313
|
+
{
|
|
314
|
+
type: "select",
|
|
315
|
+
name: "type",
|
|
316
|
+
message: "What do you want to generate?",
|
|
317
|
+
choices: [
|
|
318
|
+
{ title: "\u{1F4C4} Page", value: "page", description: "A new page in app/ or pages/" },
|
|
319
|
+
{ title: "\u{1F4D0} Layout", value: "layout", description: "A layout wrapper component" },
|
|
320
|
+
{ title: "\u{1F9E9} Component", value: "component", description: "A reusable React component" },
|
|
321
|
+
{ title: "\u{1FA9D} Hook", value: "hook", description: "A custom React hook" },
|
|
322
|
+
{ title: "\u{1F50C} API Route", value: "api", description: "An API endpoint" },
|
|
323
|
+
{ title: "\u26A1 Server Action", value: "action", description: "A server action function" },
|
|
324
|
+
{ title: "\u{1F6E1}\uFE0F Middleware", value: "middleware", description: "Request middleware" },
|
|
325
|
+
{ title: "\u{1F310} Context", value: "context", description: "React context provider" },
|
|
326
|
+
{ title: "\u23F3 Loading", value: "loading", description: "Loading state component" },
|
|
327
|
+
{ title: "\u274C Error", value: "error", description: "Error boundary component" },
|
|
328
|
+
{ title: "\u{1F50D} Not Found", value: "not-found", description: "404 page component" }
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
type: (prev) => ["loading", "error", "not-found"].includes(prev) ? null : "text",
|
|
333
|
+
name: "name",
|
|
334
|
+
message: "Name:",
|
|
335
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
336
|
+
}
|
|
337
|
+
]);
|
|
338
|
+
if (!response.type) process.exit(0);
|
|
339
|
+
type = response.type;
|
|
340
|
+
name = response.name;
|
|
341
|
+
}
|
|
342
|
+
const validTypes = [
|
|
343
|
+
"page",
|
|
344
|
+
"layout",
|
|
345
|
+
"component",
|
|
346
|
+
"hook",
|
|
347
|
+
"api",
|
|
348
|
+
"action",
|
|
349
|
+
"middleware",
|
|
350
|
+
"context",
|
|
351
|
+
"loading",
|
|
352
|
+
"error",
|
|
353
|
+
"not-found"
|
|
354
|
+
];
|
|
355
|
+
if (!validTypes.includes(type)) {
|
|
356
|
+
log.error(`Invalid type: ${type}`);
|
|
357
|
+
log.info(`Valid types: ${validTypes.join(", ")}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
if (type && ["loading", "error", "not-found"].includes(type)) {
|
|
361
|
+
await generateSpecialFile(type, cwd);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
let finalName = name;
|
|
365
|
+
if (!finalName) {
|
|
366
|
+
const response = await prompts({
|
|
367
|
+
type: "text",
|
|
368
|
+
name: "name",
|
|
369
|
+
message: `${toPascalCase(type || "item")} name:`,
|
|
370
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
371
|
+
});
|
|
372
|
+
finalName = response.name;
|
|
373
|
+
if (!finalName) process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
switch (type) {
|
|
376
|
+
case "page":
|
|
377
|
+
await generatePage(finalName, cwd);
|
|
378
|
+
break;
|
|
379
|
+
case "layout":
|
|
380
|
+
await generateLayout(finalName, cwd);
|
|
381
|
+
break;
|
|
382
|
+
case "component":
|
|
383
|
+
await generateComponent(finalName, cwd);
|
|
384
|
+
break;
|
|
385
|
+
case "hook":
|
|
386
|
+
await generateHook(finalName, cwd);
|
|
387
|
+
break;
|
|
388
|
+
case "api":
|
|
389
|
+
await generateApi(finalName, cwd);
|
|
390
|
+
break;
|
|
391
|
+
case "action":
|
|
392
|
+
await generateAction(finalName, cwd);
|
|
393
|
+
break;
|
|
394
|
+
case "middleware":
|
|
395
|
+
await generateMiddleware(finalName, cwd);
|
|
396
|
+
break;
|
|
397
|
+
case "context":
|
|
398
|
+
await generateContext(finalName, cwd);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function generatePage(name, cwd) {
|
|
403
|
+
const response = await prompts([
|
|
404
|
+
{
|
|
405
|
+
type: "select",
|
|
406
|
+
name: "directory",
|
|
407
|
+
message: "Where to create the page?",
|
|
408
|
+
choices: [
|
|
409
|
+
{ title: "app/ (App Router)", value: "app" },
|
|
410
|
+
{ title: "pages/ (Pages Router)", value: "pages" }
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
type: "toggle",
|
|
415
|
+
name: "client",
|
|
416
|
+
message: "Client component? (use client)",
|
|
417
|
+
initial: false,
|
|
418
|
+
active: "Yes",
|
|
419
|
+
inactive: "No"
|
|
420
|
+
}
|
|
421
|
+
]);
|
|
422
|
+
const fileName = response.directory === "app" ? "page.tsx" : `${toKebabCase(name)}.tsx`;
|
|
423
|
+
const filePath = response.directory === "app" ? path.join(cwd, "app", toKebabCase(name), fileName) : path.join(cwd, "pages", fileName);
|
|
424
|
+
writeFile(filePath, templates.page(name, { client: response.client }));
|
|
425
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
426
|
+
}
|
|
427
|
+
async function generateLayout(name, cwd) {
|
|
428
|
+
const filePath = path.join(cwd, "app", toKebabCase(name), "layout.tsx");
|
|
429
|
+
writeFile(filePath, templates.layout(name));
|
|
430
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
431
|
+
}
|
|
432
|
+
async function generateComponent(name, cwd) {
|
|
433
|
+
const response = await prompts([
|
|
434
|
+
{
|
|
435
|
+
type: "select",
|
|
436
|
+
name: "directory",
|
|
437
|
+
message: "Where to create the component?",
|
|
438
|
+
choices: [
|
|
439
|
+
{ title: "components/", value: "components" },
|
|
440
|
+
{ title: "app/components/", value: "app/components" }
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
type: "toggle",
|
|
445
|
+
name: "client",
|
|
446
|
+
message: "Client component?",
|
|
447
|
+
initial: true,
|
|
448
|
+
active: "Yes",
|
|
449
|
+
inactive: "No"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
type: "toggle",
|
|
453
|
+
name: "props",
|
|
454
|
+
message: "Include props interface?",
|
|
455
|
+
initial: true,
|
|
456
|
+
active: "Yes",
|
|
457
|
+
inactive: "No"
|
|
458
|
+
}
|
|
459
|
+
]);
|
|
460
|
+
const filePath = path.join(cwd, response.directory, `${toPascalCase(name)}.tsx`);
|
|
461
|
+
writeFile(filePath, templates.component(name, { client: response.client, props: response.props }));
|
|
462
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
463
|
+
}
|
|
464
|
+
async function generateHook(name, cwd) {
|
|
465
|
+
const hookName = name.startsWith("use") ? name : `use-${name}`;
|
|
466
|
+
const filePath = path.join(cwd, "hooks", `${toKebabCase(hookName)}.ts`);
|
|
467
|
+
writeFile(filePath, templates.hook(hookName.replace(/^use-?/, "")));
|
|
468
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
469
|
+
}
|
|
470
|
+
async function generateApi(name, cwd) {
|
|
471
|
+
const filePath = path.join(cwd, "pages", "api", `${toKebabCase(name)}.ts`);
|
|
472
|
+
writeFile(filePath, templates.api(name));
|
|
473
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
474
|
+
}
|
|
475
|
+
async function generateAction(name, cwd) {
|
|
476
|
+
const filePath = path.join(cwd, "actions", `${toKebabCase(name)}.ts`);
|
|
477
|
+
writeFile(filePath, templates.action(name));
|
|
478
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
479
|
+
}
|
|
480
|
+
async function generateMiddleware(name, cwd) {
|
|
481
|
+
const filePath = path.join(cwd, "middleware", `${toKebabCase(name)}.ts`);
|
|
482
|
+
writeFile(filePath, templates.middleware(name));
|
|
483
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
484
|
+
}
|
|
485
|
+
async function generateContext(name, cwd) {
|
|
486
|
+
const filePath = path.join(cwd, "contexts", `${toPascalCase(name)}Context.tsx`);
|
|
487
|
+
writeFile(filePath, templates.context(name));
|
|
488
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
489
|
+
}
|
|
490
|
+
async function generateSpecialFile(type, cwd) {
|
|
491
|
+
const response = await prompts({
|
|
492
|
+
type: "text",
|
|
493
|
+
name: "path",
|
|
494
|
+
message: "Path (relative to app/):",
|
|
495
|
+
initial: ""
|
|
496
|
+
});
|
|
497
|
+
const basePath = response.path ? path.join(cwd, "app", response.path) : path.join(cwd, "app");
|
|
498
|
+
let fileName;
|
|
499
|
+
let content;
|
|
500
|
+
switch (type) {
|
|
501
|
+
case "loading":
|
|
502
|
+
fileName = "loading.tsx";
|
|
503
|
+
content = templates.loading();
|
|
504
|
+
break;
|
|
505
|
+
case "error":
|
|
506
|
+
fileName = "error.tsx";
|
|
507
|
+
content = templates.error();
|
|
508
|
+
break;
|
|
509
|
+
case "not-found":
|
|
510
|
+
fileName = "not-found.tsx";
|
|
511
|
+
content = templates.notFound();
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
const filePath = path.join(basePath, fileName);
|
|
515
|
+
writeFile(filePath, content);
|
|
516
|
+
log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
|
|
517
|
+
}
|
|
518
|
+
function listGenerators() {
|
|
519
|
+
console.log(`
|
|
520
|
+
${pc.bold("Available Generators:")}
|
|
521
|
+
|
|
522
|
+
${pc.cyan("page")} Create a new page (app/ or pages/)
|
|
523
|
+
${pc.cyan("layout")} Create a layout wrapper
|
|
524
|
+
${pc.cyan("component")} Create a React component
|
|
525
|
+
${pc.cyan("hook")} Create a custom hook
|
|
526
|
+
${pc.cyan("api")} Create an API route
|
|
527
|
+
${pc.cyan("action")} Create a server action
|
|
528
|
+
${pc.cyan("middleware")} Create request middleware
|
|
529
|
+
${pc.cyan("context")} Create a React context
|
|
530
|
+
${pc.cyan("loading")} Create a loading component
|
|
531
|
+
${pc.cyan("error")} Create an error boundary
|
|
532
|
+
${pc.cyan("not-found")} Create a 404 page
|
|
533
|
+
|
|
534
|
+
${pc.bold("Usage:")}
|
|
535
|
+
${pc.dim("$")} flexi generate ${pc.cyan("<type>")} ${pc.dim("[name]")}
|
|
536
|
+
${pc.dim("$")} flexi g ${pc.cyan("<type>")} ${pc.dim("[name]")}
|
|
537
|
+
|
|
538
|
+
${pc.bold("Examples:")}
|
|
539
|
+
${pc.dim("$")} flexi g page dashboard
|
|
540
|
+
${pc.dim("$")} flexi g component Button
|
|
541
|
+
${pc.dim("$")} flexi g hook auth
|
|
542
|
+
${pc.dim("$")} flexi g api users
|
|
543
|
+
`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// cli/index.ts
|
|
547
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
548
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
549
|
+
var VERSION = "3.0.0";
|
|
550
|
+
var LOGO = `
|
|
551
|
+
${pc2.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
|
|
552
|
+
${pc2.cyan("\u2551")} ${pc2.cyan("\u2551")}
|
|
553
|
+
${pc2.cyan("\u2551")} ${pc2.bold(pc2.magenta("\u26A1"))} ${pc2.bold(pc2.white("F L E X I R E A C T"))} ${pc2.cyan("\u2551")}
|
|
554
|
+
${pc2.cyan("\u2551")} ${pc2.cyan("\u2551")}
|
|
555
|
+
${pc2.cyan("\u2551")} ${pc2.dim("The Modern React Framework")} ${pc2.cyan("\u2551")}
|
|
556
|
+
${pc2.cyan("\u2551")} ${pc2.dim("TypeScript \u2022 Tailwind \u2022 SSR \u2022 Islands")} ${pc2.cyan("\u2551")}
|
|
557
|
+
${pc2.cyan("\u2551")} ${pc2.cyan("\u2551")}
|
|
558
|
+
${pc2.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
559
|
+
`;
|
|
560
|
+
var MINI_LOGO = `${pc2.magenta("\u26A1")} ${pc2.bold("FlexiReact")}`;
|
|
561
|
+
var log2 = {
|
|
562
|
+
info: (msg) => console.log(`${pc2.cyan("\u2139")} ${msg}`),
|
|
563
|
+
success: (msg) => console.log(`${pc2.green("\u2713")} ${msg}`),
|
|
564
|
+
warn: (msg) => console.log(`${pc2.yellow("\u26A0")} ${pc2.yellow(msg)}`),
|
|
565
|
+
error: (msg) => console.log(`${pc2.red("\u2717")} ${pc2.red(msg)}`),
|
|
566
|
+
step: (num, total, msg) => console.log(`${pc2.dim(`[${num}/${total}]`)} ${msg}`),
|
|
567
|
+
blank: () => console.log(""),
|
|
568
|
+
divider: () => console.log(pc2.dim("\u2500".repeat(60)))
|
|
569
|
+
};
|
|
570
|
+
function copyDirectory(src, dest) {
|
|
571
|
+
if (!fs2.existsSync(dest)) {
|
|
572
|
+
fs2.mkdirSync(dest, { recursive: true });
|
|
573
|
+
}
|
|
574
|
+
const entries = fs2.readdirSync(src, { withFileTypes: true });
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
const srcPath = path2.join(src, entry.name);
|
|
577
|
+
const destPath = path2.join(dest, entry.name);
|
|
578
|
+
if (entry.isDirectory()) {
|
|
579
|
+
copyDirectory(srcPath, destPath);
|
|
580
|
+
} else {
|
|
581
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function runCommand(cmd, cwd) {
|
|
586
|
+
return new Promise((resolve, reject) => {
|
|
587
|
+
const child = spawn(cmd, {
|
|
588
|
+
shell: true,
|
|
589
|
+
cwd,
|
|
590
|
+
stdio: "pipe"
|
|
591
|
+
});
|
|
592
|
+
child.on("close", (code) => {
|
|
593
|
+
if (code === 0) resolve();
|
|
594
|
+
else reject(new Error(`Command failed with code ${code}`));
|
|
595
|
+
});
|
|
596
|
+
child.on("error", reject);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
async function createProject(projectName) {
|
|
600
|
+
console.log(LOGO);
|
|
601
|
+
log2.blank();
|
|
602
|
+
let name = projectName;
|
|
603
|
+
if (!name) {
|
|
604
|
+
const response = await prompts2({
|
|
605
|
+
type: "text",
|
|
606
|
+
name: "projectName",
|
|
607
|
+
message: "Project name:",
|
|
608
|
+
initial: "my-flexi-app",
|
|
609
|
+
validate: (value) => value.length > 0 || "Project name is required"
|
|
610
|
+
});
|
|
611
|
+
name = response.projectName;
|
|
612
|
+
if (!name) process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const projectPath = path2.resolve(process.cwd(), name);
|
|
615
|
+
if (fs2.existsSync(projectPath)) {
|
|
616
|
+
log2.error(`Directory "${name}" already exists.`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
const options = await prompts2([
|
|
620
|
+
{
|
|
621
|
+
type: "select",
|
|
622
|
+
name: "template",
|
|
623
|
+
message: "Select a template:",
|
|
624
|
+
choices: [
|
|
625
|
+
{ title: "\u{1F680} Default (Tailwind + shadcn/ui)", value: "default" },
|
|
626
|
+
{ title: "\u{1F49A} FlexiUI (Landing page + @flexireact/flexi-ui)", value: "flexi-ui" },
|
|
627
|
+
{ title: "\u{1F4E6} Minimal (Clean slate)", value: "minimal" }
|
|
628
|
+
],
|
|
629
|
+
initial: 0
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: "toggle",
|
|
633
|
+
name: "typescript",
|
|
634
|
+
message: "Use TypeScript?",
|
|
635
|
+
initial: true,
|
|
636
|
+
active: "Yes",
|
|
637
|
+
inactive: "No"
|
|
638
|
+
}
|
|
639
|
+
]);
|
|
640
|
+
if (options.template === void 0) process.exit(1);
|
|
641
|
+
log2.blank();
|
|
642
|
+
log2.divider();
|
|
643
|
+
log2.blank();
|
|
644
|
+
const totalSteps = options.template === "default" ? 6 : options.template === "flexi-ui" ? 5 : 4;
|
|
645
|
+
let currentStep = 0;
|
|
646
|
+
currentStep++;
|
|
647
|
+
log2.step(currentStep, totalSteps, "Creating project directory...");
|
|
648
|
+
fs2.mkdirSync(projectPath, { recursive: true });
|
|
649
|
+
log2.success(`Created ${pc2.cyan(name)}/`);
|
|
650
|
+
currentStep++;
|
|
651
|
+
log2.step(currentStep, totalSteps, "Setting up project structure...");
|
|
652
|
+
const templateName = options.template;
|
|
653
|
+
const templatePath = path2.resolve(__dirname2, "..", "templates", templateName);
|
|
654
|
+
if (fs2.existsSync(templatePath)) {
|
|
655
|
+
copyDirectory(templatePath, projectPath);
|
|
656
|
+
log2.success("Project structure created");
|
|
657
|
+
} else {
|
|
658
|
+
await createDefaultTemplate(projectPath, name, options.typescript);
|
|
659
|
+
log2.success("Project structure created");
|
|
660
|
+
}
|
|
661
|
+
currentStep++;
|
|
662
|
+
log2.step(currentStep, totalSteps, "Configuring project...");
|
|
663
|
+
const packageJsonPath = path2.join(projectPath, "package.json");
|
|
664
|
+
if (fs2.existsSync(packageJsonPath)) {
|
|
665
|
+
const pkg = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
666
|
+
pkg.name = name;
|
|
667
|
+
fs2.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
668
|
+
}
|
|
669
|
+
log2.success("Project configured");
|
|
670
|
+
currentStep++;
|
|
671
|
+
log2.step(currentStep, totalSteps, "Installing dependencies...");
|
|
672
|
+
const spinner = ora({ text: "Installing packages...", color: "cyan" }).start();
|
|
673
|
+
try {
|
|
674
|
+
await runCommand("npm install", projectPath);
|
|
675
|
+
spinner.succeed("Dependencies installed");
|
|
676
|
+
} catch {
|
|
677
|
+
spinner.fail("Failed to install dependencies");
|
|
678
|
+
log2.warn('Run "npm install" manually in the project directory');
|
|
679
|
+
}
|
|
680
|
+
currentStep++;
|
|
681
|
+
log2.step(currentStep, totalSteps, "Linking FlexiReact...");
|
|
682
|
+
const linkSpinner = ora({ text: "Linking framework...", color: "cyan" }).start();
|
|
683
|
+
try {
|
|
684
|
+
const frameworkRoot = path2.resolve(__dirname2, "..");
|
|
685
|
+
await runCommand(`npm link "${frameworkRoot}"`, projectPath);
|
|
686
|
+
linkSpinner.succeed("FlexiReact linked");
|
|
687
|
+
} catch {
|
|
688
|
+
linkSpinner.fail("Failed to link FlexiReact");
|
|
689
|
+
log2.warn('Run "npm link flexireact" manually');
|
|
690
|
+
}
|
|
691
|
+
if (options.template === "default") {
|
|
692
|
+
currentStep++;
|
|
693
|
+
log2.step(currentStep, totalSteps, "Setting up shadcn/ui components...");
|
|
694
|
+
log2.success("shadcn/ui configured");
|
|
695
|
+
}
|
|
696
|
+
log2.blank();
|
|
697
|
+
log2.divider();
|
|
698
|
+
log2.blank();
|
|
699
|
+
console.log(` ${pc2.green("\u2728")} ${pc2.bold("Success!")} Your FlexiReact app is ready.`);
|
|
700
|
+
log2.blank();
|
|
701
|
+
console.log(` ${pc2.dim("$")} ${pc2.cyan(`cd ${name}`)}`);
|
|
702
|
+
console.log(` ${pc2.dim("$")} ${pc2.cyan("npm run dev")}`);
|
|
703
|
+
log2.blank();
|
|
704
|
+
console.log(` ${pc2.dim("Then open")} ${pc2.cyan("http://localhost:3000")} ${pc2.dim("in your browser.")}`);
|
|
705
|
+
log2.blank();
|
|
706
|
+
console.log(` ${pc2.dim("Documentation:")} ${pc2.cyan("https://github.com/flexireact/flexireact")}`);
|
|
707
|
+
log2.blank();
|
|
708
|
+
}
|
|
709
|
+
async function createDefaultTemplate(projectPath, name, useTypeScript) {
|
|
710
|
+
const ext = useTypeScript ? "tsx" : "jsx";
|
|
711
|
+
const configExt = useTypeScript ? "ts" : "js";
|
|
712
|
+
const dirs = [
|
|
713
|
+
"app/components",
|
|
714
|
+
"app/styles",
|
|
715
|
+
"pages/api",
|
|
716
|
+
"public"
|
|
717
|
+
];
|
|
718
|
+
for (const dir of dirs) {
|
|
719
|
+
fs2.mkdirSync(path2.join(projectPath, dir), { recursive: true });
|
|
720
|
+
}
|
|
721
|
+
const packageJson = {
|
|
722
|
+
name,
|
|
723
|
+
version: "0.1.0",
|
|
724
|
+
private: true,
|
|
725
|
+
type: "module",
|
|
726
|
+
scripts: {
|
|
727
|
+
dev: "flexi dev",
|
|
728
|
+
build: "flexi build",
|
|
729
|
+
start: "flexi start",
|
|
730
|
+
doctor: "flexi doctor"
|
|
731
|
+
},
|
|
732
|
+
dependencies: {
|
|
733
|
+
react: "^18.3.1",
|
|
734
|
+
"react-dom": "^18.3.1",
|
|
735
|
+
"class-variance-authority": "^0.7.0",
|
|
736
|
+
clsx: "^2.1.1",
|
|
737
|
+
"tailwind-merge": "^2.5.5",
|
|
738
|
+
"lucide-react": "^0.468.0"
|
|
739
|
+
},
|
|
740
|
+
devDependencies: {
|
|
741
|
+
tailwindcss: "^3.4.16",
|
|
742
|
+
postcss: "^8.4.49",
|
|
743
|
+
autoprefixer: "^10.4.20",
|
|
744
|
+
...useTypeScript ? {
|
|
745
|
+
typescript: "^5.7.2",
|
|
746
|
+
"@types/react": "^18.3.14",
|
|
747
|
+
"@types/react-dom": "^18.3.2",
|
|
748
|
+
"@types/node": "^22.10.1"
|
|
749
|
+
} : {}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
fs2.writeFileSync(
|
|
753
|
+
path2.join(projectPath, "package.json"),
|
|
754
|
+
JSON.stringify(packageJson, null, 2)
|
|
755
|
+
);
|
|
756
|
+
if (useTypeScript) {
|
|
757
|
+
const tsconfig = {
|
|
758
|
+
compilerOptions: {
|
|
759
|
+
target: "ES2022",
|
|
760
|
+
lib: ["dom", "dom.iterable", "ES2022"],
|
|
761
|
+
allowJs: true,
|
|
762
|
+
skipLibCheck: true,
|
|
763
|
+
strict: true,
|
|
764
|
+
noEmit: true,
|
|
765
|
+
esModuleInterop: true,
|
|
766
|
+
module: "ESNext",
|
|
767
|
+
moduleResolution: "bundler",
|
|
768
|
+
resolveJsonModule: true,
|
|
769
|
+
isolatedModules: true,
|
|
770
|
+
jsx: "react-jsx",
|
|
771
|
+
baseUrl: ".",
|
|
772
|
+
paths: {
|
|
773
|
+
"@/*": ["./*"],
|
|
774
|
+
"@/components/*": ["./app/components/*"]
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
778
|
+
exclude: ["node_modules"]
|
|
779
|
+
};
|
|
780
|
+
fs2.writeFileSync(
|
|
781
|
+
path2.join(projectPath, "tsconfig.json"),
|
|
782
|
+
JSON.stringify(tsconfig, null, 2)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
786
|
+
export default {
|
|
787
|
+
content: [
|
|
788
|
+
'./app/**/*.{js,ts,jsx,tsx}',
|
|
789
|
+
'./pages/**/*.{js,ts,jsx,tsx}',
|
|
790
|
+
'./components/**/*.{js,ts,jsx,tsx}',
|
|
791
|
+
],
|
|
792
|
+
darkMode: 'class',
|
|
793
|
+
theme: {
|
|
794
|
+
extend: {
|
|
795
|
+
colors: {
|
|
796
|
+
border: 'hsl(var(--border))',
|
|
797
|
+
background: 'hsl(var(--background))',
|
|
798
|
+
foreground: 'hsl(var(--foreground))',
|
|
799
|
+
primary: {
|
|
800
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
801
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
802
|
+
},
|
|
803
|
+
secondary: {
|
|
804
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
805
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
806
|
+
},
|
|
807
|
+
muted: {
|
|
808
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
809
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
810
|
+
},
|
|
811
|
+
accent: {
|
|
812
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
813
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
borderRadius: {
|
|
817
|
+
lg: 'var(--radius)',
|
|
818
|
+
md: 'calc(var(--radius) - 2px)',
|
|
819
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
plugins: [],
|
|
824
|
+
};
|
|
825
|
+
`;
|
|
826
|
+
fs2.writeFileSync(path2.join(projectPath, "tailwind.config.js"), tailwindConfig);
|
|
827
|
+
const postcssConfig = `export default {
|
|
828
|
+
plugins: {
|
|
829
|
+
tailwindcss: {},
|
|
830
|
+
autoprefixer: {},
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
`;
|
|
834
|
+
fs2.writeFileSync(path2.join(projectPath, "postcss.config.js"), postcssConfig);
|
|
835
|
+
const globalsCss = `@tailwind base;
|
|
836
|
+
@tailwind components;
|
|
837
|
+
@tailwind utilities;
|
|
838
|
+
|
|
839
|
+
@layer base {
|
|
840
|
+
:root {
|
|
841
|
+
--background: 222 47% 11%;
|
|
842
|
+
--foreground: 210 40% 98%;
|
|
843
|
+
--primary: 263 70% 50%;
|
|
844
|
+
--primary-foreground: 210 40% 98%;
|
|
845
|
+
--secondary: 217 33% 17%;
|
|
846
|
+
--secondary-foreground: 210 40% 98%;
|
|
847
|
+
--muted: 217 33% 17%;
|
|
848
|
+
--muted-foreground: 215 20% 65%;
|
|
849
|
+
--accent: 263 70% 50%;
|
|
850
|
+
--accent-foreground: 210 40% 98%;
|
|
851
|
+
--border: 217 33% 17%;
|
|
852
|
+
--radius: 0.5rem;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
@layer base {
|
|
857
|
+
* {
|
|
858
|
+
@apply border-border;
|
|
859
|
+
}
|
|
860
|
+
body {
|
|
861
|
+
@apply bg-background text-foreground antialiased;
|
|
862
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
`;
|
|
866
|
+
fs2.writeFileSync(path2.join(projectPath, "app/styles/globals.css"), globalsCss);
|
|
867
|
+
const flexiConfig = `export default {
|
|
868
|
+
server: {
|
|
869
|
+
port: 3000,
|
|
870
|
+
host: 'localhost'
|
|
871
|
+
},
|
|
872
|
+
islands: {
|
|
873
|
+
enabled: true
|
|
874
|
+
},
|
|
875
|
+
rsc: {
|
|
876
|
+
enabled: true
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
`;
|
|
880
|
+
fs2.writeFileSync(path2.join(projectPath, `flexireact.config.${configExt}`), flexiConfig);
|
|
881
|
+
await createComponents(projectPath, ext);
|
|
882
|
+
await createPages(projectPath, ext);
|
|
883
|
+
}
|
|
884
|
+
async function createComponents(projectPath, ext) {
|
|
885
|
+
const buttonComponent = `import React from 'react';
|
|
886
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
887
|
+
import { clsx } from 'clsx';
|
|
888
|
+
import { twMerge } from 'tailwind-merge';
|
|
889
|
+
|
|
890
|
+
function cn(...inputs${ext === "tsx" ? ": (string | undefined)[]" : ""}) {
|
|
891
|
+
return twMerge(clsx(inputs));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const buttonVariants = cva(
|
|
895
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
896
|
+
{
|
|
897
|
+
variants: {
|
|
898
|
+
variant: {
|
|
899
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
900
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
901
|
+
outline: 'border border-border bg-transparent hover:bg-accent hover:text-accent-foreground',
|
|
902
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
903
|
+
},
|
|
904
|
+
size: {
|
|
905
|
+
default: 'h-10 px-4 py-2',
|
|
906
|
+
sm: 'h-9 rounded-md px-3',
|
|
907
|
+
lg: 'h-11 rounded-md px-8',
|
|
908
|
+
icon: 'h-10 w-10',
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
defaultVariants: {
|
|
912
|
+
variant: 'default',
|
|
913
|
+
size: 'default',
|
|
914
|
+
},
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
${ext === "tsx" ? `interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}` : ""}
|
|
919
|
+
|
|
920
|
+
export function Button({ className, variant, size, ...props }${ext === "tsx" ? ": ButtonProps" : ""}) {
|
|
921
|
+
return (
|
|
922
|
+
<button
|
|
923
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
924
|
+
{...props}
|
|
925
|
+
/>
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
`;
|
|
929
|
+
fs2.writeFileSync(path2.join(projectPath, `app/components/Button.${ext}`), buttonComponent);
|
|
930
|
+
const cardComponent = `import React from 'react';
|
|
931
|
+
|
|
932
|
+
${ext === "tsx" ? `interface CardProps {
|
|
933
|
+
children: React.ReactNode;
|
|
934
|
+
className?: string;
|
|
935
|
+
}` : ""}
|
|
936
|
+
|
|
937
|
+
export function Card({ children, className = '' }${ext === "tsx" ? ": CardProps" : ""}) {
|
|
938
|
+
return (
|
|
939
|
+
<div className={\`rounded-lg border border-border bg-secondary/50 p-6 backdrop-blur-sm \${className}\`}>
|
|
940
|
+
{children}
|
|
941
|
+
</div>
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export function CardHeader({ children, className = '' }${ext === "tsx" ? ": CardProps" : ""}) {
|
|
946
|
+
return <div className={\`mb-4 \${className}\`}>{children}</div>;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function CardTitle({ children, className = '' }${ext === "tsx" ? ": CardProps" : ""}) {
|
|
950
|
+
return <h3 className={\`text-xl font-semibold \${className}\`}>{children}</h3>;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
export function CardDescription({ children, className = '' }${ext === "tsx" ? ": CardProps" : ""}) {
|
|
954
|
+
return <p className={\`text-muted-foreground \${className}\`}>{children}</p>;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export function CardContent({ children, className = '' }${ext === "tsx" ? ": CardProps" : ""}) {
|
|
958
|
+
return <div className={className}>{children}</div>;
|
|
959
|
+
}
|
|
960
|
+
`;
|
|
961
|
+
fs2.writeFileSync(path2.join(projectPath, `app/components/Card.${ext}`), cardComponent);
|
|
962
|
+
const navbarComponent = `import React from 'react';
|
|
963
|
+
|
|
964
|
+
export function Navbar() {
|
|
965
|
+
return (
|
|
966
|
+
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
|
|
967
|
+
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4">
|
|
968
|
+
<a href="/" className="flex items-center gap-2 text-xl font-bold">
|
|
969
|
+
<span className="text-2xl">\u26A1</span>
|
|
970
|
+
<span className="bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
|
971
|
+
FlexiReact
|
|
972
|
+
</span>
|
|
973
|
+
</a>
|
|
974
|
+
|
|
975
|
+
<div className="flex items-center gap-6">
|
|
976
|
+
<a href="/" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
977
|
+
Home
|
|
978
|
+
</a>
|
|
979
|
+
<a href="/docs" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
980
|
+
Docs
|
|
981
|
+
</a>
|
|
982
|
+
<a href="/api/hello" className="text-muted-foreground hover:text-foreground transition-colors">
|
|
983
|
+
API
|
|
984
|
+
</a>
|
|
985
|
+
<a
|
|
986
|
+
href="https://github.com/flexireact/flexireact"
|
|
987
|
+
target="_blank"
|
|
988
|
+
rel="noopener noreferrer"
|
|
989
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
990
|
+
>
|
|
991
|
+
GitHub
|
|
992
|
+
</a>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
</nav>
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
`;
|
|
999
|
+
fs2.writeFileSync(path2.join(projectPath, `app/components/Navbar.${ext}`), navbarComponent);
|
|
1000
|
+
}
|
|
1001
|
+
async function createPages(projectPath, ext) {
|
|
1002
|
+
const homePage = `import React from 'react';
|
|
1003
|
+
|
|
1004
|
+
export const title = 'FlexiReact - The Modern React Framework';
|
|
1005
|
+
|
|
1006
|
+
const features = [
|
|
1007
|
+
{ icon: '\u26A1', title: 'Lightning Fast', desc: 'Powered by esbuild for instant builds.' },
|
|
1008
|
+
{ icon: '\u{1F4D8}', title: 'TypeScript', desc: 'First-class TypeScript support.' },
|
|
1009
|
+
{ icon: '\u{1F3DD}\uFE0F', title: 'Islands', desc: 'Partial hydration for minimal JS.' },
|
|
1010
|
+
{ icon: '\u{1F4C1}', title: 'File Routing', desc: 'Create a file, get a route.' },
|
|
1011
|
+
{ icon: '\u{1F50C}', title: 'API Routes', desc: 'Build your API alongside frontend.' },
|
|
1012
|
+
{ icon: '\u{1F680}', title: 'SSR/SSG', desc: 'Server rendering and static generation.' },
|
|
1013
|
+
];
|
|
1014
|
+
|
|
1015
|
+
export default function HomePage() {
|
|
1016
|
+
return (
|
|
1017
|
+
<div style={styles.container}>
|
|
1018
|
+
{/* Navbar */}
|
|
1019
|
+
<nav style={styles.nav}>
|
|
1020
|
+
<a href="/" style={styles.logo}>
|
|
1021
|
+
<svg style={{ width: 32, height: 32 }} viewBox="0 0 200 200" fill="none">
|
|
1022
|
+
<defs>
|
|
1023
|
+
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
1024
|
+
<stop offset="0%" stopColor="#61DAFB"/>
|
|
1025
|
+
<stop offset="100%" stopColor="#21A1F1"/>
|
|
1026
|
+
</linearGradient>
|
|
1027
|
+
</defs>
|
|
1028
|
+
<circle cx="100" cy="100" r="12" fill="url(#g)"/>
|
|
1029
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" strokeWidth="6" transform="rotate(-30 100 100)"/>
|
|
1030
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" strokeWidth="6" transform="rotate(30 100 100)"/>
|
|
1031
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" strokeWidth="6" transform="rotate(90 100 100)"/>
|
|
1032
|
+
</svg>
|
|
1033
|
+
<span style={styles.logoText}>FlexiReact</span>
|
|
1034
|
+
</a>
|
|
1035
|
+
<div style={styles.navLinks}>
|
|
1036
|
+
<a href="/" style={styles.navLink}>Home</a>
|
|
1037
|
+
<a href="/api/hello" style={styles.navLink}>API</a>
|
|
1038
|
+
</div>
|
|
1039
|
+
</nav>
|
|
1040
|
+
|
|
1041
|
+
{/* Hero */}
|
|
1042
|
+
<section style={styles.hero}>
|
|
1043
|
+
<svg style={{ width: 120, height: 120, marginBottom: 24 }} viewBox="0 0 200 200" fill="none">
|
|
1044
|
+
<defs>
|
|
1045
|
+
<linearGradient id="hero" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
1046
|
+
<stop offset="0%" stopColor="#61DAFB"/>
|
|
1047
|
+
<stop offset="100%" stopColor="#21A1F1"/>
|
|
1048
|
+
</linearGradient>
|
|
1049
|
+
</defs>
|
|
1050
|
+
<circle cx="100" cy="100" r="12" fill="url(#hero)"/>
|
|
1051
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#hero)" strokeWidth="6" transform="rotate(-30 100 100)"/>
|
|
1052
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#hero)" strokeWidth="6" transform="rotate(30 100 100)"/>
|
|
1053
|
+
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#hero)" strokeWidth="6" transform="rotate(90 100 100)"/>
|
|
1054
|
+
<circle cx="28" cy="70" r="8" fill="url(#hero)"/>
|
|
1055
|
+
<circle cx="172" cy="130" r="8" fill="url(#hero)"/>
|
|
1056
|
+
<circle cx="100" cy="20" r="8" fill="url(#hero)"/>
|
|
1057
|
+
</svg>
|
|
1058
|
+
|
|
1059
|
+
<div style={styles.badge}>\u{1F680} v2.1 \u2014 TypeScript & Islands</div>
|
|
1060
|
+
|
|
1061
|
+
<h1 style={styles.title}>
|
|
1062
|
+
Build faster with<br/>
|
|
1063
|
+
<span style={styles.titleGradient}>FlexiReact</span>
|
|
1064
|
+
</h1>
|
|
1065
|
+
|
|
1066
|
+
<p style={styles.subtitle}>
|
|
1067
|
+
The modern React framework with SSR, SSG, Islands architecture,<br/>
|
|
1068
|
+
and file-based routing. Simple and powerful.
|
|
1069
|
+
</p>
|
|
1070
|
+
|
|
1071
|
+
<div style={styles.buttons}>
|
|
1072
|
+
<a href="/docs" style={styles.primaryBtn}>Get Started \u2192</a>
|
|
1073
|
+
<a href="/api/hello" style={styles.secondaryBtn}>View API</a>
|
|
1074
|
+
</div>
|
|
1075
|
+
</section>
|
|
1076
|
+
|
|
1077
|
+
{/* Features */}
|
|
1078
|
+
<section style={styles.features}>
|
|
1079
|
+
<h2 style={styles.featuresTitle}>Everything you need</h2>
|
|
1080
|
+
<div style={styles.grid}>
|
|
1081
|
+
{features.map((f, i) => (
|
|
1082
|
+
<div key={i} style={styles.card}>
|
|
1083
|
+
<div style={styles.cardIcon}>{f.icon}</div>
|
|
1084
|
+
<h3 style={styles.cardTitle}>{f.title}</h3>
|
|
1085
|
+
<p style={styles.cardDesc}>{f.desc}</p>
|
|
1086
|
+
</div>
|
|
1087
|
+
))}
|
|
1088
|
+
</div>
|
|
1089
|
+
</section>
|
|
1090
|
+
|
|
1091
|
+
{/* Footer */}
|
|
1092
|
+
<footer style={styles.footer}>
|
|
1093
|
+
Built with \u2764\uFE0F using FlexiReact
|
|
1094
|
+
</footer>
|
|
1095
|
+
</div>
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const styles = {
|
|
1100
|
+
container: {
|
|
1101
|
+
minHeight: '100vh',
|
|
1102
|
+
background: 'linear-gradient(180deg, #0f172a 0%, #1e293b 100%)',
|
|
1103
|
+
color: '#f8fafc',
|
|
1104
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
1105
|
+
},
|
|
1106
|
+
nav: {
|
|
1107
|
+
position: 'fixed' as const,
|
|
1108
|
+
top: 0,
|
|
1109
|
+
left: 0,
|
|
1110
|
+
right: 0,
|
|
1111
|
+
height: 64,
|
|
1112
|
+
display: 'flex',
|
|
1113
|
+
alignItems: 'center',
|
|
1114
|
+
justifyContent: 'space-between',
|
|
1115
|
+
padding: '0 24px',
|
|
1116
|
+
background: 'rgba(15, 23, 42, 0.8)',
|
|
1117
|
+
backdropFilter: 'blur(12px)',
|
|
1118
|
+
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
|
1119
|
+
zIndex: 100,
|
|
1120
|
+
},
|
|
1121
|
+
logo: {
|
|
1122
|
+
display: 'flex',
|
|
1123
|
+
alignItems: 'center',
|
|
1124
|
+
gap: 8,
|
|
1125
|
+
textDecoration: 'none',
|
|
1126
|
+
color: '#f8fafc',
|
|
1127
|
+
},
|
|
1128
|
+
logoText: {
|
|
1129
|
+
fontSize: 20,
|
|
1130
|
+
fontWeight: 700,
|
|
1131
|
+
background: 'linear-gradient(90deg, #61DAFB, #21A1F1)',
|
|
1132
|
+
WebkitBackgroundClip: 'text',
|
|
1133
|
+
WebkitTextFillColor: 'transparent',
|
|
1134
|
+
},
|
|
1135
|
+
navLinks: { display: 'flex', gap: 24 },
|
|
1136
|
+
navLink: { color: '#94a3b8', textDecoration: 'none', fontSize: 14 },
|
|
1137
|
+
hero: {
|
|
1138
|
+
display: 'flex',
|
|
1139
|
+
flexDirection: 'column' as const,
|
|
1140
|
+
alignItems: 'center',
|
|
1141
|
+
justifyContent: 'center',
|
|
1142
|
+
textAlign: 'center' as const,
|
|
1143
|
+
padding: '140px 24px 80px',
|
|
1144
|
+
},
|
|
1145
|
+
badge: {
|
|
1146
|
+
background: 'rgba(99, 102, 241, 0.2)',
|
|
1147
|
+
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
1148
|
+
borderRadius: 9999,
|
|
1149
|
+
padding: '8px 16px',
|
|
1150
|
+
fontSize: 14,
|
|
1151
|
+
marginBottom: 24,
|
|
1152
|
+
},
|
|
1153
|
+
title: {
|
|
1154
|
+
fontSize: 'clamp(2.5rem, 8vw, 4.5rem)',
|
|
1155
|
+
fontWeight: 800,
|
|
1156
|
+
lineHeight: 1.1,
|
|
1157
|
+
marginBottom: 24,
|
|
1158
|
+
},
|
|
1159
|
+
titleGradient: {
|
|
1160
|
+
background: 'linear-gradient(90deg, #61DAFB, #a78bfa, #61DAFB)',
|
|
1161
|
+
WebkitBackgroundClip: 'text',
|
|
1162
|
+
WebkitTextFillColor: 'transparent',
|
|
1163
|
+
},
|
|
1164
|
+
subtitle: {
|
|
1165
|
+
fontSize: 18,
|
|
1166
|
+
color: '#94a3b8',
|
|
1167
|
+
maxWidth: 600,
|
|
1168
|
+
lineHeight: 1.6,
|
|
1169
|
+
marginBottom: 32,
|
|
1170
|
+
},
|
|
1171
|
+
buttons: { display: 'flex', gap: 16, flexWrap: 'wrap' as const, justifyContent: 'center' },
|
|
1172
|
+
primaryBtn: {
|
|
1173
|
+
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
|
1174
|
+
color: '#fff',
|
|
1175
|
+
padding: '14px 28px',
|
|
1176
|
+
borderRadius: 12,
|
|
1177
|
+
textDecoration: 'none',
|
|
1178
|
+
fontWeight: 600,
|
|
1179
|
+
boxShadow: '0 4px 20px rgba(99, 102, 241, 0.4)',
|
|
1180
|
+
},
|
|
1181
|
+
secondaryBtn: {
|
|
1182
|
+
background: 'transparent',
|
|
1183
|
+
color: '#f8fafc',
|
|
1184
|
+
padding: '14px 28px',
|
|
1185
|
+
borderRadius: 12,
|
|
1186
|
+
textDecoration: 'none',
|
|
1187
|
+
fontWeight: 600,
|
|
1188
|
+
border: '1px solid rgba(255,255,255,0.2)',
|
|
1189
|
+
},
|
|
1190
|
+
features: {
|
|
1191
|
+
padding: '80px 24px',
|
|
1192
|
+
maxWidth: 1200,
|
|
1193
|
+
margin: '0 auto',
|
|
1194
|
+
},
|
|
1195
|
+
featuresTitle: {
|
|
1196
|
+
fontSize: 32,
|
|
1197
|
+
fontWeight: 700,
|
|
1198
|
+
textAlign: 'center' as const,
|
|
1199
|
+
marginBottom: 48,
|
|
1200
|
+
},
|
|
1201
|
+
grid: {
|
|
1202
|
+
display: 'grid',
|
|
1203
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
|
1204
|
+
gap: 24,
|
|
1205
|
+
},
|
|
1206
|
+
card: {
|
|
1207
|
+
background: 'rgba(255,255,255,0.05)',
|
|
1208
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
1209
|
+
borderRadius: 16,
|
|
1210
|
+
padding: 24,
|
|
1211
|
+
},
|
|
1212
|
+
cardIcon: { fontSize: 32, marginBottom: 12 },
|
|
1213
|
+
cardTitle: { fontSize: 18, fontWeight: 600, marginBottom: 8 },
|
|
1214
|
+
cardDesc: { fontSize: 14, color: '#94a3b8', lineHeight: 1.5 },
|
|
1215
|
+
footer: {
|
|
1216
|
+
textAlign: 'center' as const,
|
|
1217
|
+
padding: 32,
|
|
1218
|
+
color: '#64748b',
|
|
1219
|
+
borderTop: '1px solid rgba(255,255,255,0.1)',
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1222
|
+
`;
|
|
1223
|
+
fs2.writeFileSync(path2.join(projectPath, "pages/index.jsx"), homePage);
|
|
1224
|
+
const apiRoute = `/**
|
|
1225
|
+
* API Route: /api/hello
|
|
1226
|
+
*/
|
|
1227
|
+
|
|
1228
|
+
export function get(req${ext === "tsx" ? ": any" : ""}, res${ext === "tsx" ? ": any" : ""}) {
|
|
1229
|
+
res.json({
|
|
1230
|
+
message: 'Hello from FlexiReact API! \u{1F680}',
|
|
1231
|
+
timestamp: new Date().toISOString(),
|
|
1232
|
+
framework: 'FlexiReact v2.1'
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
export function post(req${ext === "tsx" ? ": any" : ""}, res${ext === "tsx" ? ": any" : ""}) {
|
|
1237
|
+
const { name } = req.body || {};
|
|
1238
|
+
res.json({
|
|
1239
|
+
message: \`Hello, \${name || 'World'}!\`,
|
|
1240
|
+
timestamp: new Date().toISOString()
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
fs2.writeFileSync(path2.join(projectPath, `pages/api/hello.${ext === "tsx" ? "ts" : "js"}`), apiRoute);
|
|
1245
|
+
fs2.writeFileSync(path2.join(projectPath, "public/.gitkeep"), "");
|
|
1246
|
+
}
|
|
1247
|
+
async function runDev() {
|
|
1248
|
+
console.log(`
|
|
1249
|
+
${pc2.cyan(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")}
|
|
1250
|
+
${pc2.cyan(" \u2502")} ${pc2.cyan("\u2502")}
|
|
1251
|
+
${pc2.cyan(" \u2502")} ${pc2.bold(pc2.cyan("\u269B"))} ${pc2.bold(pc2.white("F L E X I R E A C T"))} ${pc2.cyan("\u2502")}
|
|
1252
|
+
${pc2.cyan(" \u2502")} ${pc2.cyan("\u2502")}
|
|
1253
|
+
${pc2.cyan(" \u2502")} ${pc2.dim("The Modern React Framework")} ${pc2.cyan("\u2502")}
|
|
1254
|
+
${pc2.cyan(" \u2502")} ${pc2.cyan("\u2502")}
|
|
1255
|
+
${pc2.cyan(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")}
|
|
1256
|
+
`);
|
|
1257
|
+
const startDevPath = path2.join(__dirname2, "..", "core", "start-dev.ts");
|
|
1258
|
+
const child = spawn(
|
|
1259
|
+
"npx",
|
|
1260
|
+
["tsx", startDevPath],
|
|
1261
|
+
{
|
|
1262
|
+
stdio: "inherit",
|
|
1263
|
+
cwd: process.cwd(),
|
|
1264
|
+
shell: true,
|
|
1265
|
+
env: { ...process.env, NODE_ENV: "development", FORCE_COLOR: "1" }
|
|
1266
|
+
}
|
|
1267
|
+
);
|
|
1268
|
+
child.on("error", (error) => {
|
|
1269
|
+
log2.error(`Failed to start dev server: ${error.message}`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
});
|
|
1272
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
1273
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
1274
|
+
}
|
|
1275
|
+
async function runBuild(options = {}) {
|
|
1276
|
+
console.log(MINI_LOGO);
|
|
1277
|
+
log2.blank();
|
|
1278
|
+
log2.info("Building for production...");
|
|
1279
|
+
if (options.analyze) {
|
|
1280
|
+
log2.info("Bundle analysis enabled");
|
|
1281
|
+
}
|
|
1282
|
+
log2.blank();
|
|
1283
|
+
const spinner = ora({ text: "Compiling...", color: "cyan" }).start();
|
|
1284
|
+
try {
|
|
1285
|
+
const buildPath = path2.join(__dirname2, "..", "core", "build", "index.js");
|
|
1286
|
+
const configPath = path2.join(__dirname2, "..", "core", "config.js");
|
|
1287
|
+
const buildModule = await import(pathToFileURL(buildPath).href);
|
|
1288
|
+
const configModule = await import(pathToFileURL(configPath).href);
|
|
1289
|
+
const projectRoot = process.cwd();
|
|
1290
|
+
const rawConfig = await configModule.loadConfig(projectRoot);
|
|
1291
|
+
const config = configModule.resolvePaths(rawConfig, projectRoot);
|
|
1292
|
+
const result = await buildModule.build({
|
|
1293
|
+
projectRoot,
|
|
1294
|
+
config,
|
|
1295
|
+
mode: "production",
|
|
1296
|
+
analyze: options.analyze
|
|
1297
|
+
});
|
|
1298
|
+
spinner.succeed("Build complete!");
|
|
1299
|
+
log2.blank();
|
|
1300
|
+
log2.success(`Output: ${pc2.cyan(".flexi/")}`);
|
|
1301
|
+
if (options.analyze && result?.analysis) {
|
|
1302
|
+
log2.blank();
|
|
1303
|
+
log2.info("\u{1F4CA} Bundle Analysis:");
|
|
1304
|
+
log2.blank();
|
|
1305
|
+
const analysis = result.analysis;
|
|
1306
|
+
const sorted = Object.entries(analysis.files || {}).sort((a, b) => b[1].size - a[1].size);
|
|
1307
|
+
console.log(pc2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1308
|
+
console.log(` ${pc2.bold("File")}${" ".repeat(35)}${pc2.bold("Size")}`);
|
|
1309
|
+
console.log(pc2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1310
|
+
for (const [file, info] of sorted.slice(0, 15)) {
|
|
1311
|
+
const name = file.length > 35 ? "..." + file.slice(-32) : file;
|
|
1312
|
+
const size = formatBytes(info.size);
|
|
1313
|
+
const gzip = info.gzipSize ? pc2.dim(` (${formatBytes(info.gzipSize)} gzip)`) : "";
|
|
1314
|
+
console.log(` ${name.padEnd(38)} ${pc2.cyan(size)}${gzip}`);
|
|
1315
|
+
}
|
|
1316
|
+
console.log(pc2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1317
|
+
console.log(` ${pc2.bold("Total:")}${" ".repeat(31)} ${pc2.green(formatBytes(analysis.totalSize || 0))}`);
|
|
1318
|
+
if (analysis.totalGzipSize) {
|
|
1319
|
+
console.log(` ${pc2.dim("Gzipped:")}${" ".repeat(29)} ${pc2.dim(formatBytes(analysis.totalGzipSize))}`);
|
|
1320
|
+
}
|
|
1321
|
+
log2.blank();
|
|
1322
|
+
}
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
spinner.fail("Build failed");
|
|
1325
|
+
log2.error(error.message);
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function formatBytes(bytes) {
|
|
1330
|
+
if (bytes === 0) return "0 B";
|
|
1331
|
+
const k = 1024;
|
|
1332
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
1333
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1334
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
1335
|
+
}
|
|
1336
|
+
async function runStart() {
|
|
1337
|
+
console.log(MINI_LOGO);
|
|
1338
|
+
log2.blank();
|
|
1339
|
+
log2.info("Starting production server...");
|
|
1340
|
+
log2.blank();
|
|
1341
|
+
const startProdPath = path2.join(__dirname2, "..", "core", "start-prod.ts");
|
|
1342
|
+
const child = spawn(
|
|
1343
|
+
"npx",
|
|
1344
|
+
["tsx", startProdPath],
|
|
1345
|
+
{
|
|
1346
|
+
stdio: "inherit",
|
|
1347
|
+
cwd: process.cwd(),
|
|
1348
|
+
shell: true,
|
|
1349
|
+
env: { ...process.env, NODE_ENV: "production" }
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
child.on("error", (error) => {
|
|
1353
|
+
log2.error(`Failed to start server: ${error.message}`);
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
});
|
|
1356
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
1357
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
1358
|
+
}
|
|
1359
|
+
async function runDoctor() {
|
|
1360
|
+
console.log(MINI_LOGO);
|
|
1361
|
+
log2.blank();
|
|
1362
|
+
log2.info("Checking your project...");
|
|
1363
|
+
log2.blank();
|
|
1364
|
+
const checks = [];
|
|
1365
|
+
const projectRoot = process.cwd();
|
|
1366
|
+
const nodeVersion = process.version;
|
|
1367
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
1368
|
+
checks.push({
|
|
1369
|
+
name: "Node.js version",
|
|
1370
|
+
status: nodeMajor >= 18 ? "pass" : "fail",
|
|
1371
|
+
message: nodeMajor >= 18 ? `${nodeVersion} \u2713` : `${nodeVersion} (requires 18+)`
|
|
1372
|
+
});
|
|
1373
|
+
const packageJsonPath = path2.join(projectRoot, "package.json");
|
|
1374
|
+
const hasPackageJson = fs2.existsSync(packageJsonPath);
|
|
1375
|
+
checks.push({
|
|
1376
|
+
name: "package.json",
|
|
1377
|
+
status: hasPackageJson ? "pass" : "fail",
|
|
1378
|
+
message: hasPackageJson ? "Found" : "Not found"
|
|
1379
|
+
});
|
|
1380
|
+
const pagesDir = path2.join(projectRoot, "pages");
|
|
1381
|
+
const hasPages = fs2.existsSync(pagesDir);
|
|
1382
|
+
checks.push({
|
|
1383
|
+
name: "pages/ directory",
|
|
1384
|
+
status: hasPages ? "pass" : "warn",
|
|
1385
|
+
message: hasPages ? "Found" : "Not found"
|
|
1386
|
+
});
|
|
1387
|
+
const tsconfigPath = path2.join(projectRoot, "tsconfig.json");
|
|
1388
|
+
const hasTypeScript = fs2.existsSync(tsconfigPath);
|
|
1389
|
+
checks.push({
|
|
1390
|
+
name: "TypeScript",
|
|
1391
|
+
status: "info",
|
|
1392
|
+
message: hasTypeScript ? "Enabled" : "Not configured"
|
|
1393
|
+
});
|
|
1394
|
+
const tailwindPath = path2.join(projectRoot, "tailwind.config.js");
|
|
1395
|
+
const hasTailwind = fs2.existsSync(tailwindPath);
|
|
1396
|
+
checks.push({
|
|
1397
|
+
name: "Tailwind CSS",
|
|
1398
|
+
status: "info",
|
|
1399
|
+
message: hasTailwind ? "Configured" : "Not configured"
|
|
1400
|
+
});
|
|
1401
|
+
let hasErrors = false;
|
|
1402
|
+
let hasWarnings = false;
|
|
1403
|
+
for (const check of checks) {
|
|
1404
|
+
let icon;
|
|
1405
|
+
let color;
|
|
1406
|
+
switch (check.status) {
|
|
1407
|
+
case "pass":
|
|
1408
|
+
icon = "\u2713";
|
|
1409
|
+
color = pc2.green;
|
|
1410
|
+
break;
|
|
1411
|
+
case "fail":
|
|
1412
|
+
icon = "\u2717";
|
|
1413
|
+
color = pc2.red;
|
|
1414
|
+
hasErrors = true;
|
|
1415
|
+
break;
|
|
1416
|
+
case "warn":
|
|
1417
|
+
icon = "\u26A0";
|
|
1418
|
+
color = pc2.yellow;
|
|
1419
|
+
hasWarnings = true;
|
|
1420
|
+
break;
|
|
1421
|
+
default:
|
|
1422
|
+
icon = "\u25CB";
|
|
1423
|
+
color = pc2.cyan;
|
|
1424
|
+
}
|
|
1425
|
+
console.log(` ${color(icon)} ${check.name}: ${pc2.dim(check.message)}`);
|
|
1426
|
+
}
|
|
1427
|
+
log2.blank();
|
|
1428
|
+
if (hasErrors) {
|
|
1429
|
+
log2.error("Some checks failed. Please fix the issues above.");
|
|
1430
|
+
} else if (hasWarnings) {
|
|
1431
|
+
log2.warn("All critical checks passed with some warnings.");
|
|
1432
|
+
} else {
|
|
1433
|
+
log2.success("All checks passed! Your project is ready.");
|
|
1434
|
+
}
|
|
1435
|
+
log2.blank();
|
|
1436
|
+
}
|
|
1437
|
+
function showHelp() {
|
|
1438
|
+
console.log(LOGO);
|
|
1439
|
+
console.log(` ${pc2.bold("Usage:")}`);
|
|
1440
|
+
console.log(` ${pc2.cyan("flexi")} ${pc2.dim("<command>")} ${pc2.dim("[options]")}`);
|
|
1441
|
+
log2.blank();
|
|
1442
|
+
console.log(` ${pc2.bold("Commands:")}`);
|
|
1443
|
+
console.log(` ${pc2.cyan("create")} ${pc2.dim("<name>")} Create a new FlexiReact project`);
|
|
1444
|
+
console.log(` ${pc2.cyan("dev")} Start development server`);
|
|
1445
|
+
console.log(` ${pc2.cyan("build")} Build for production`);
|
|
1446
|
+
console.log(` ${pc2.cyan("start")} Start production server`);
|
|
1447
|
+
console.log(` ${pc2.cyan("generate")} ${pc2.dim("<type>")} Generate component/page/hook/etc (alias: g)`);
|
|
1448
|
+
console.log(` ${pc2.cyan("doctor")} Check project health`);
|
|
1449
|
+
console.log(` ${pc2.cyan("help")} Show this help message`);
|
|
1450
|
+
log2.blank();
|
|
1451
|
+
console.log(` ${pc2.bold("Generate Types:")}`);
|
|
1452
|
+
console.log(` ${pc2.dim("page, layout, component, hook, api, action, middleware, context")}`);
|
|
1453
|
+
console.log(` ${pc2.dim("loading, error, not-found")}`);
|
|
1454
|
+
log2.blank();
|
|
1455
|
+
console.log(` ${pc2.bold("Examples:")}`);
|
|
1456
|
+
console.log(` ${pc2.dim("$")} flexi create my-app`);
|
|
1457
|
+
console.log(` ${pc2.dim("$")} flexi dev`);
|
|
1458
|
+
console.log(` ${pc2.dim("$")} flexi g component Button`);
|
|
1459
|
+
console.log(` ${pc2.dim("$")} flexi g page dashboard`);
|
|
1460
|
+
console.log(` ${pc2.dim("$")} flexi build --analyze`);
|
|
1461
|
+
log2.blank();
|
|
1462
|
+
}
|
|
1463
|
+
async function main() {
|
|
1464
|
+
const args = process.argv.slice(2);
|
|
1465
|
+
const command = args[0];
|
|
1466
|
+
switch (command) {
|
|
1467
|
+
case "create":
|
|
1468
|
+
await createProject(args[1]);
|
|
1469
|
+
break;
|
|
1470
|
+
case "dev":
|
|
1471
|
+
await runDev();
|
|
1472
|
+
break;
|
|
1473
|
+
case "build":
|
|
1474
|
+
const analyzeFlag = args.includes("--analyze") || args.includes("-a");
|
|
1475
|
+
await runBuild({ analyze: analyzeFlag });
|
|
1476
|
+
break;
|
|
1477
|
+
case "start":
|
|
1478
|
+
await runStart();
|
|
1479
|
+
break;
|
|
1480
|
+
case "doctor":
|
|
1481
|
+
await runDoctor();
|
|
1482
|
+
break;
|
|
1483
|
+
case "generate":
|
|
1484
|
+
case "g":
|
|
1485
|
+
await runGenerate(args[1], args[2]);
|
|
1486
|
+
break;
|
|
1487
|
+
case "generate:list":
|
|
1488
|
+
case "g:list":
|
|
1489
|
+
listGenerators();
|
|
1490
|
+
break;
|
|
1491
|
+
case "version":
|
|
1492
|
+
case "-v":
|
|
1493
|
+
case "--version":
|
|
1494
|
+
console.log(`${MINI_LOGO} ${pc2.dim(`v${VERSION}`)}`);
|
|
1495
|
+
break;
|
|
1496
|
+
case "help":
|
|
1497
|
+
case "--help":
|
|
1498
|
+
case "-h":
|
|
1499
|
+
showHelp();
|
|
1500
|
+
break;
|
|
1501
|
+
default:
|
|
1502
|
+
if (command) {
|
|
1503
|
+
log2.error(`Unknown command: ${command}`);
|
|
1504
|
+
log2.blank();
|
|
1505
|
+
}
|
|
1506
|
+
showHelp();
|
|
1507
|
+
process.exit(command ? 1 : 0);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
main().catch((error) => {
|
|
1511
|
+
log2.error(error.message);
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
});
|
|
1514
|
+
//# sourceMappingURL=index.js.map
|