@flexireact/core 1.0.1 → 2.0.0
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/README.md +117 -116
- package/bin/flexireact.js +23 -0
- package/cli/index.ts +9 -21
- package/core/cli/{logger.js → logger.ts} +8 -2
- package/core/client/{hydration.js → hydration.ts} +10 -0
- package/core/client/{islands.js → islands.ts} +6 -1
- package/core/client/{navigation.js → navigation.ts} +10 -2
- package/core/client/{runtime.js → runtime.ts} +16 -0
- package/core/{index.js → index.ts} +2 -1
- package/core/islands/{index.js → index.ts} +16 -4
- package/core/{logger.js → logger.ts} +1 -1
- package/core/middleware/{index.js → index.ts} +32 -9
- package/core/plugins/{index.js → index.ts} +9 -6
- package/core/render/index.ts +1069 -0
- package/core/{render.js → render.ts} +7 -5
- package/core/router/index.ts +543 -0
- package/core/rsc/{index.js → index.ts} +6 -5
- package/core/server/{index.js → index.ts} +25 -6
- package/core/{server.js → server.ts} +8 -2
- package/core/ssg/{index.js → index.ts} +30 -5
- package/core/start-dev.ts +6 -0
- package/core/start-prod.ts +6 -0
- package/core/tsconfig.json +28 -0
- package/core/types.ts +239 -0
- package/package.json +19 -14
- package/cli/index.js +0 -992
- package/core/render/index.js +0 -765
- package/core/router/index.js +0 -296
- /package/core/{api.js → api.ts} +0 -0
- /package/core/build/{index.js → index.ts} +0 -0
- /package/core/client/{index.js → index.ts} +0 -0
- /package/core/{config.js → config.ts} +0 -0
- /package/core/{context.js → context.ts} +0 -0
- /package/core/{dev.js → dev.ts} +0 -0
- /package/core/{loader.js → loader.ts} +0 -0
- /package/core/{router.js → router.ts} +0 -0
- /package/core/{utils.js → utils.ts} +0 -0
|
@@ -3,12 +3,14 @@ import React from 'react';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Renders a React component to a full HTML page
|
|
6
|
-
* @param {React.Component} Component - The React component to render
|
|
7
|
-
* @param {Object} props - Props to pass to the component
|
|
8
|
-
* @param {Object} options - Rendering options
|
|
9
|
-
* @returns {string} Complete HTML string
|
|
10
6
|
*/
|
|
11
|
-
|
|
7
|
+
interface RenderOptions {
|
|
8
|
+
title?: string;
|
|
9
|
+
scripts?: string[];
|
|
10
|
+
styles?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function render(Component: React.ComponentType<any>, props: Record<string, any> = {}, options: RenderOptions = {}) {
|
|
12
14
|
const {
|
|
13
15
|
title = 'FlexiReact App',
|
|
14
16
|
scripts = [],
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Router v2
|
|
3
|
+
* Advanced file-based routing with nested routes, loading, and error boundaries
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple routing conventions:
|
|
6
|
+
* - pages/ : Traditional file-based routing (index.tsx, about.tsx)
|
|
7
|
+
* - app/ : Next.js style App Router (page.tsx, layout.tsx)
|
|
8
|
+
* - routes/ : FlexiReact v2 routes directory (home.tsx → /, [slug].tsx → /:slug)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { isServerComponent, isClientComponent, isIsland } from '../utils.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Route types
|
|
17
|
+
*/
|
|
18
|
+
export const RouteType = {
|
|
19
|
+
PAGE: 'page',
|
|
20
|
+
API: 'api',
|
|
21
|
+
LAYOUT: 'layout',
|
|
22
|
+
LOADING: 'loading',
|
|
23
|
+
ERROR: 'error',
|
|
24
|
+
NOT_FOUND: 'not-found'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the complete route tree from all routing directories
|
|
29
|
+
*/
|
|
30
|
+
export function buildRouteTree(pagesDir, layoutsDir, appDir = null, routesDir = null) {
|
|
31
|
+
const projectRoot = path.dirname(pagesDir);
|
|
32
|
+
|
|
33
|
+
const routes: {
|
|
34
|
+
pages: any[];
|
|
35
|
+
api: any[];
|
|
36
|
+
layouts: Map<any, any>;
|
|
37
|
+
tree: Record<string, any>;
|
|
38
|
+
appRoutes: any[];
|
|
39
|
+
flexiRoutes: any[];
|
|
40
|
+
rootLayout?: string;
|
|
41
|
+
} = {
|
|
42
|
+
pages: [],
|
|
43
|
+
api: [],
|
|
44
|
+
layouts: new Map(),
|
|
45
|
+
tree: {},
|
|
46
|
+
appRoutes: [], // Next.js style app router routes
|
|
47
|
+
flexiRoutes: [] // FlexiReact v2 routes/ directory
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 1. Scan routes/ directory (FlexiReact v2 - priority)
|
|
51
|
+
const routesDirPath = routesDir || path.join(projectRoot, 'routes');
|
|
52
|
+
if (fs.existsSync(routesDirPath)) {
|
|
53
|
+
scanRoutesDirectory(routesDirPath, routesDirPath, routes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Scan app/ directory (Next.js style App Router)
|
|
57
|
+
const appDirPath = appDir || path.join(projectRoot, 'app');
|
|
58
|
+
if (fs.existsSync(appDirPath)) {
|
|
59
|
+
scanAppDirectory(appDirPath, appDirPath, routes);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Scan pages/ directory (traditional routing - fallback)
|
|
63
|
+
if (fs.existsSync(pagesDir)) {
|
|
64
|
+
scanDirectory(pagesDir, pagesDir, routes);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Scan layouts/ directory
|
|
68
|
+
if (fs.existsSync(layoutsDir)) {
|
|
69
|
+
scanLayouts(layoutsDir, routes.layouts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. Check for root layout in app/ directory
|
|
73
|
+
const rootLayoutPath = path.join(appDirPath, 'layout.tsx');
|
|
74
|
+
const rootLayoutPathJs = path.join(appDirPath, 'layout.jsx');
|
|
75
|
+
if (fs.existsSync(rootLayoutPath)) {
|
|
76
|
+
routes.rootLayout = rootLayoutPath;
|
|
77
|
+
} else if (fs.existsSync(rootLayoutPathJs)) {
|
|
78
|
+
routes.rootLayout = rootLayoutPathJs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build route tree for nested routes
|
|
82
|
+
routes.tree = buildTree([...routes.flexiRoutes, ...routes.appRoutes, ...routes.pages]);
|
|
83
|
+
|
|
84
|
+
return routes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scans routes/ directory for FlexiReact v2 style routing
|
|
89
|
+
*
|
|
90
|
+
* Convention:
|
|
91
|
+
* - home.tsx → /
|
|
92
|
+
* - about.tsx → /about
|
|
93
|
+
* - blog/index.tsx → /blog
|
|
94
|
+
* - blog/[slug].tsx → /blog/:slug
|
|
95
|
+
* - (public)/home.tsx → / (route group, not in URL)
|
|
96
|
+
* - api/hello.ts → /api/hello (API route)
|
|
97
|
+
* - dashboard/layout.tsx → layout for /dashboard/*
|
|
98
|
+
*/
|
|
99
|
+
function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null) {
|
|
100
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
101
|
+
|
|
102
|
+
// Find special files in current directory
|
|
103
|
+
let layoutFile = null;
|
|
104
|
+
let loadingFile = null;
|
|
105
|
+
let errorFile = null;
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.isFile()) {
|
|
109
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
110
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
111
|
+
const ext = path.extname(entry.name);
|
|
112
|
+
|
|
113
|
+
// Special files
|
|
114
|
+
if (name === 'layout') layoutFile = fullPath;
|
|
115
|
+
if (name === 'loading') loadingFile = fullPath;
|
|
116
|
+
if (name === 'error') errorFile = fullPath;
|
|
117
|
+
|
|
118
|
+
// Skip special files and non-route files
|
|
119
|
+
if (['layout', 'loading', 'error', 'not-found'].includes(name)) continue;
|
|
120
|
+
if (!['.tsx', '.jsx', '.ts', '.js'].includes(ext)) continue;
|
|
121
|
+
|
|
122
|
+
// API routes (in api/ folder or .ts/.js files in api/)
|
|
123
|
+
const relativePath = path.relative(baseDir, currentDir);
|
|
124
|
+
const isApiRoute = relativePath.startsWith('api') || relativePath.startsWith('api/');
|
|
125
|
+
|
|
126
|
+
if (isApiRoute && ['.ts', '.js'].includes(ext)) {
|
|
127
|
+
const apiPath = '/' + [...parentSegments, name === 'index' ? '' : name].filter(Boolean).join('/');
|
|
128
|
+
routes.api.push({
|
|
129
|
+
type: RouteType.API,
|
|
130
|
+
path: apiPath.replace(/\/+/g, '/') || '/',
|
|
131
|
+
filePath: fullPath,
|
|
132
|
+
pattern: createRoutePattern(apiPath),
|
|
133
|
+
segments: [...parentSegments, name === 'index' ? '' : name].filter(Boolean)
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Page routes
|
|
139
|
+
if (['.tsx', '.jsx'].includes(ext)) {
|
|
140
|
+
let routePath;
|
|
141
|
+
|
|
142
|
+
// home.tsx → /
|
|
143
|
+
if (name === 'home' && parentSegments.length === 0) {
|
|
144
|
+
routePath = '/';
|
|
145
|
+
}
|
|
146
|
+
// index.tsx → parent path
|
|
147
|
+
else if (name === 'index') {
|
|
148
|
+
routePath = '/' + parentSegments.join('/') || '/';
|
|
149
|
+
}
|
|
150
|
+
// [param].tsx → /:param
|
|
151
|
+
else if (name.startsWith('[') && name.endsWith(']')) {
|
|
152
|
+
const paramName = name.slice(1, -1);
|
|
153
|
+
// Handle catch-all [...slug]
|
|
154
|
+
if (paramName.startsWith('...')) {
|
|
155
|
+
routePath = '/' + [...parentSegments, '*' + paramName.slice(3)].join('/');
|
|
156
|
+
} else {
|
|
157
|
+
routePath = '/' + [...parentSegments, ':' + paramName].join('/');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// regular.tsx → /regular
|
|
161
|
+
else {
|
|
162
|
+
routePath = '/' + [...parentSegments, name].join('/');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
routes.flexiRoutes.push({
|
|
166
|
+
type: RouteType.PAGE,
|
|
167
|
+
path: routePath.replace(/\/+/g, '/'),
|
|
168
|
+
filePath: fullPath,
|
|
169
|
+
pattern: createRoutePattern(routePath),
|
|
170
|
+
segments: routePath.split('/').filter(Boolean),
|
|
171
|
+
layout: layoutFile || parentLayout,
|
|
172
|
+
loading: loadingFile,
|
|
173
|
+
error: errorFile,
|
|
174
|
+
isFlexiRouter: true,
|
|
175
|
+
isServerComponent: isServerComponent(fullPath),
|
|
176
|
+
isClientComponent: isClientComponent(fullPath),
|
|
177
|
+
isIsland: isIsland(fullPath)
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Recursively scan subdirectories
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
187
|
+
const dirName = entry.name;
|
|
188
|
+
|
|
189
|
+
// Skip special directories
|
|
190
|
+
if (dirName.startsWith('_') || dirName.startsWith('.')) continue;
|
|
191
|
+
|
|
192
|
+
// Handle route groups (parentheses) - don't add to URL
|
|
193
|
+
const isGroup = dirName.startsWith('(') && dirName.endsWith(')');
|
|
194
|
+
|
|
195
|
+
// Handle dynamic segments [param]
|
|
196
|
+
let segmentName = dirName;
|
|
197
|
+
if (dirName.startsWith('[') && dirName.endsWith(']')) {
|
|
198
|
+
const paramName = dirName.slice(1, -1);
|
|
199
|
+
if (paramName.startsWith('...')) {
|
|
200
|
+
segmentName = '*' + paramName.slice(3);
|
|
201
|
+
} else {
|
|
202
|
+
segmentName = ':' + paramName;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
|
|
207
|
+
const newLayout = layoutFile || parentLayout;
|
|
208
|
+
|
|
209
|
+
scanRoutesDirectory(baseDir, fullPath, routes, newSegments, newLayout);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Scans app directory for Next.js style routing
|
|
216
|
+
* Supports: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx
|
|
217
|
+
*/
|
|
218
|
+
function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null) {
|
|
219
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
220
|
+
|
|
221
|
+
// Find special files in current directory
|
|
222
|
+
const specialFiles = {
|
|
223
|
+
page: null,
|
|
224
|
+
layout: null,
|
|
225
|
+
loading: null,
|
|
226
|
+
error: null,
|
|
227
|
+
notFound: null,
|
|
228
|
+
template: null
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (entry.isFile()) {
|
|
233
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
234
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
235
|
+
|
|
236
|
+
if (name === 'page') specialFiles.page = fullPath;
|
|
237
|
+
if (name === 'layout') specialFiles.layout = fullPath;
|
|
238
|
+
if (name === 'loading') specialFiles.loading = fullPath;
|
|
239
|
+
if (name === 'error') specialFiles.error = fullPath;
|
|
240
|
+
if (name === 'not-found') specialFiles.notFound = fullPath;
|
|
241
|
+
if (name === 'template') specialFiles.template = fullPath;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If there's a page.tsx, create a route
|
|
246
|
+
if (specialFiles.page) {
|
|
247
|
+
const routePath = '/' + parentSegments.join('/') || '/';
|
|
248
|
+
|
|
249
|
+
routes.appRoutes.push({
|
|
250
|
+
type: RouteType.PAGE,
|
|
251
|
+
path: routePath.replace(/\/+/g, '/'),
|
|
252
|
+
filePath: specialFiles.page,
|
|
253
|
+
pattern: createRoutePattern(routePath),
|
|
254
|
+
segments: parentSegments,
|
|
255
|
+
layout: specialFiles.layout || parentLayout,
|
|
256
|
+
loading: specialFiles.loading,
|
|
257
|
+
error: specialFiles.error,
|
|
258
|
+
notFound: specialFiles.notFound,
|
|
259
|
+
template: specialFiles.template,
|
|
260
|
+
isAppRouter: true,
|
|
261
|
+
isServerComponent: isServerComponent(specialFiles.page),
|
|
262
|
+
isClientComponent: isClientComponent(specialFiles.page),
|
|
263
|
+
isIsland: isIsland(specialFiles.page)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Recursively scan subdirectories
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
if (entry.isDirectory()) {
|
|
270
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
271
|
+
|
|
272
|
+
// Handle route groups (parentheses) - don't add to URL
|
|
273
|
+
const isGroup = entry.name.startsWith('(') && entry.name.endsWith(')');
|
|
274
|
+
|
|
275
|
+
// Handle dynamic segments [param]
|
|
276
|
+
let segmentName = entry.name;
|
|
277
|
+
if (entry.name.startsWith('[') && entry.name.endsWith(']')) {
|
|
278
|
+
// Convert [param] to :param
|
|
279
|
+
segmentName = ':' + entry.name.slice(1, -1);
|
|
280
|
+
// Handle catch-all [...param]
|
|
281
|
+
if (entry.name.startsWith('[...')) {
|
|
282
|
+
segmentName = '*' + entry.name.slice(4, -1);
|
|
283
|
+
}
|
|
284
|
+
// Handle optional catch-all [[...param]]
|
|
285
|
+
if (entry.name.startsWith('[[...')) {
|
|
286
|
+
segmentName = '*' + entry.name.slice(5, -2);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
|
|
291
|
+
const newLayout = specialFiles.layout || parentLayout;
|
|
292
|
+
|
|
293
|
+
scanAppDirectory(baseDir, fullPath, routes, newSegments, newLayout);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Scans directory recursively for route files
|
|
300
|
+
*/
|
|
301
|
+
function scanDirectory(baseDir, currentDir, routes, parentSegments = []) {
|
|
302
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
303
|
+
|
|
304
|
+
// First, find special files in current directory
|
|
305
|
+
const specialFiles = {
|
|
306
|
+
layout: null,
|
|
307
|
+
loading: null,
|
|
308
|
+
error: null,
|
|
309
|
+
notFound: null
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (entry.isFile()) {
|
|
314
|
+
const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
315
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
316
|
+
|
|
317
|
+
if (name === 'layout') specialFiles.layout = fullPath;
|
|
318
|
+
if (name === 'loading') specialFiles.loading = fullPath;
|
|
319
|
+
if (name === 'error') specialFiles.error = fullPath;
|
|
320
|
+
if (name === 'not-found' || name === '404') specialFiles.notFound = fullPath;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
326
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
327
|
+
|
|
328
|
+
if (entry.isDirectory()) {
|
|
329
|
+
// Handle route groups (parentheses)
|
|
330
|
+
const isGroup = entry.name.startsWith('(') && entry.name.endsWith(')');
|
|
331
|
+
const newSegments = isGroup ? parentSegments : [...parentSegments, entry.name];
|
|
332
|
+
|
|
333
|
+
scanDirectory(baseDir, fullPath, routes, newSegments);
|
|
334
|
+
} else if (entry.isFile()) {
|
|
335
|
+
const ext = path.extname(entry.name);
|
|
336
|
+
const baseName = path.basename(entry.name, ext);
|
|
337
|
+
|
|
338
|
+
// Skip special files (already processed)
|
|
339
|
+
if (['layout', 'loading', 'error', 'not-found', '404'].includes(baseName)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (['.jsx', '.js', '.tsx', '.ts'].includes(ext)) {
|
|
344
|
+
const isApi = relativePath.startsWith('api' + path.sep) || relativePath.startsWith('api/');
|
|
345
|
+
|
|
346
|
+
if (isApi && ['.js', '.ts'].includes(ext)) {
|
|
347
|
+
routes.api.push(createRoute(fullPath, baseDir, specialFiles, RouteType.API));
|
|
348
|
+
} else if (!isApi && ['.jsx', '.tsx'].includes(ext)) {
|
|
349
|
+
routes.pages.push(createRoute(fullPath, baseDir, specialFiles, RouteType.PAGE));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Creates a route object from file path
|
|
358
|
+
*/
|
|
359
|
+
function createRoute(filePath, baseDir, specialFiles, type) {
|
|
360
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
361
|
+
const routePath = filePathToRoute(relativePath);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
type,
|
|
365
|
+
path: routePath,
|
|
366
|
+
filePath,
|
|
367
|
+
pattern: createRoutePattern(routePath),
|
|
368
|
+
segments: routePath.split('/').filter(Boolean),
|
|
369
|
+
layout: specialFiles.layout,
|
|
370
|
+
loading: specialFiles.loading,
|
|
371
|
+
error: specialFiles.error,
|
|
372
|
+
notFound: specialFiles.notFound,
|
|
373
|
+
isServerComponent: isServerComponent(filePath),
|
|
374
|
+
isClientComponent: isClientComponent(filePath),
|
|
375
|
+
isIsland: isIsland(filePath)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Scans layouts directory
|
|
381
|
+
*/
|
|
382
|
+
function scanLayouts(layoutsDir, layoutsMap) {
|
|
383
|
+
const entries = fs.readdirSync(layoutsDir, { withFileTypes: true });
|
|
384
|
+
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
if (entry.isFile() && /\.(jsx|tsx)$/.test(entry.name)) {
|
|
387
|
+
const name = entry.name.replace(/\.(jsx|tsx)$/, '');
|
|
388
|
+
layoutsMap.set(name, path.join(layoutsDir, entry.name));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Converts file path to route path
|
|
395
|
+
*/
|
|
396
|
+
function filePathToRoute(filePath) {
|
|
397
|
+
let route = filePath.replace(/\\/g, '/');
|
|
398
|
+
|
|
399
|
+
// Remove extension
|
|
400
|
+
route = route.replace(/\.(jsx|js|tsx|ts)$/, '');
|
|
401
|
+
|
|
402
|
+
// Convert [param] to :param
|
|
403
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*$1'); // Catch-all [...slug]
|
|
404
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
405
|
+
|
|
406
|
+
// Handle index files
|
|
407
|
+
if (route.endsWith('/index')) {
|
|
408
|
+
route = route.slice(0, -6) || '/';
|
|
409
|
+
} else if (route === 'index') {
|
|
410
|
+
route = '/';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Handle route groups - remove (groupName) from path
|
|
414
|
+
route = route.replace(/\/?\([^)]+\)\/?/g, '/');
|
|
415
|
+
|
|
416
|
+
// Ensure leading slash and clean up
|
|
417
|
+
if (!route.startsWith('/')) {
|
|
418
|
+
route = '/' + route;
|
|
419
|
+
}
|
|
420
|
+
route = route.replace(/\/+/g, '/');
|
|
421
|
+
|
|
422
|
+
return route;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Creates regex pattern for route matching
|
|
427
|
+
*/
|
|
428
|
+
function createRoutePattern(routePath) {
|
|
429
|
+
let pattern = routePath
|
|
430
|
+
.replace(/\*[^/]*/g, '(.*)') // Catch-all
|
|
431
|
+
.replace(/:[^/]+/g, '([^/]+)') // Dynamic segments
|
|
432
|
+
.replace(/\//g, '\\/');
|
|
433
|
+
|
|
434
|
+
return new RegExp(`^${pattern}$`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Builds a tree structure for nested routes
|
|
439
|
+
*/
|
|
440
|
+
function buildTree(routes) {
|
|
441
|
+
const tree = { children: {}, routes: [] };
|
|
442
|
+
|
|
443
|
+
for (const route of routes) {
|
|
444
|
+
let current = tree;
|
|
445
|
+
|
|
446
|
+
for (const segment of route.segments) {
|
|
447
|
+
if (!current.children[segment]) {
|
|
448
|
+
current.children[segment] = { children: {}, routes: [] };
|
|
449
|
+
}
|
|
450
|
+
current = current.children[segment];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
current.routes.push(route);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return tree;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Matches URL path against routes
|
|
461
|
+
*/
|
|
462
|
+
export function matchRoute(urlPath, routes) {
|
|
463
|
+
const normalizedPath = urlPath === '' ? '/' : urlPath.split('?')[0];
|
|
464
|
+
|
|
465
|
+
for (const route of routes) {
|
|
466
|
+
const match = normalizedPath.match(route.pattern);
|
|
467
|
+
|
|
468
|
+
if (match) {
|
|
469
|
+
const params = extractParams(route.path, match);
|
|
470
|
+
return { ...route, params };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Extracts parameters from route match
|
|
479
|
+
*/
|
|
480
|
+
function extractParams(routePath, match) {
|
|
481
|
+
const params = {};
|
|
482
|
+
const paramNames = [];
|
|
483
|
+
|
|
484
|
+
// Extract param names from route path
|
|
485
|
+
const paramRegex = /:([^/]+)|\*([^/]*)/g;
|
|
486
|
+
let paramMatch;
|
|
487
|
+
|
|
488
|
+
while ((paramMatch = paramRegex.exec(routePath)) !== null) {
|
|
489
|
+
paramNames.push(paramMatch[1] || paramMatch[2] || 'splat');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
paramNames.forEach((name, index) => {
|
|
493
|
+
params[name] = match[index + 1];
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return params;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Finds all layouts that apply to a route
|
|
501
|
+
*/
|
|
502
|
+
export function findRouteLayouts(route, layoutsMap) {
|
|
503
|
+
const layouts = [];
|
|
504
|
+
|
|
505
|
+
// Check for segment-based layouts
|
|
506
|
+
let currentPath = '';
|
|
507
|
+
for (const segment of route.segments) {
|
|
508
|
+
currentPath += '/' + segment;
|
|
509
|
+
const layoutName = segment;
|
|
510
|
+
|
|
511
|
+
if (layoutsMap.has(layoutName)) {
|
|
512
|
+
layouts.push({
|
|
513
|
+
name: layoutName,
|
|
514
|
+
filePath: layoutsMap.get(layoutName)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check for route-specific layout
|
|
520
|
+
if (route.layout) {
|
|
521
|
+
layouts.push({
|
|
522
|
+
name: 'route',
|
|
523
|
+
filePath: route.layout
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check for root layout
|
|
528
|
+
if (layoutsMap.has('root')) {
|
|
529
|
+
layouts.unshift({
|
|
530
|
+
name: 'root',
|
|
531
|
+
filePath: layoutsMap.get('root')
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return layouts;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export default {
|
|
539
|
+
buildRouteTree,
|
|
540
|
+
matchRoute,
|
|
541
|
+
findRouteLayouts,
|
|
542
|
+
RouteType
|
|
543
|
+
};
|
|
@@ -94,16 +94,17 @@ function serializeNode(node) {
|
|
|
94
94
|
const { type, props } = node;
|
|
95
95
|
|
|
96
96
|
// Handle client component references
|
|
97
|
-
|
|
97
|
+
const typeAny = type as any;
|
|
98
|
+
if (typeAny.$$typeof === Symbol.for('react.client.reference')) {
|
|
98
99
|
return {
|
|
99
100
|
$$type: 'client',
|
|
100
|
-
$$id:
|
|
101
|
+
$$id: typeAny.$$id,
|
|
101
102
|
props: serializeProps(props)
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// Handle regular elements
|
|
106
|
-
const typeName = typeof type === 'string' ? type :
|
|
107
|
+
const typeName = typeof type === 'string' ? type : (typeAny.displayName || typeAny.name || 'Unknown');
|
|
107
108
|
|
|
108
109
|
return {
|
|
109
110
|
$$type: 'element',
|
|
@@ -118,8 +119,8 @@ function serializeNode(node) {
|
|
|
118
119
|
/**
|
|
119
120
|
* Serializes props, handling special cases
|
|
120
121
|
*/
|
|
121
|
-
function serializeProps(props) {
|
|
122
|
-
const serialized = {};
|
|
122
|
+
function serializeProps(props: Record<string, any>) {
|
|
123
|
+
const serialized: Record<string, any> = {};
|
|
123
124
|
|
|
124
125
|
for (const [key, value] of Object.entries(props)) {
|
|
125
126
|
if (key === 'children') {
|
|
@@ -44,7 +44,14 @@ const MIME_TYPES = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Creates the FlexiReact server
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
interface CreateServerOptions {
|
|
48
|
+
projectRoot?: string;
|
|
49
|
+
mode?: 'development' | 'production';
|
|
50
|
+
port?: number;
|
|
51
|
+
host?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createServer(options: CreateServerOptions = {}) {
|
|
48
55
|
const serverStartTime = Date.now();
|
|
49
56
|
const projectRoot = options.projectRoot || process.cwd();
|
|
50
57
|
const isDev = options.mode === 'development';
|
|
@@ -128,7 +135,19 @@ export async function createServer(options = {}) {
|
|
|
128
135
|
return await handleApiRoute(req, res, apiRoute, loadModule);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
// Match
|
|
138
|
+
// Match FlexiReact v2 routes (routes/ directory - priority)
|
|
139
|
+
const flexiRoute = matchRoute(effectivePath, routes.flexiRoutes || []);
|
|
140
|
+
if (flexiRoute) {
|
|
141
|
+
return await handlePageRoute(req, res, flexiRoute, routes, config, loadModule, url);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Match app routes (app/ directory - Next.js style)
|
|
145
|
+
const appRoute = matchRoute(effectivePath, routes.appRoutes || []);
|
|
146
|
+
if (appRoute) {
|
|
147
|
+
return await handlePageRoute(req, res, appRoute, routes, config, loadModule, url);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Match page routes (pages/ directory - legacy fallback)
|
|
132
151
|
const pageRoute = matchRoute(effectivePath, routes.pages);
|
|
133
152
|
if (pageRoute) {
|
|
134
153
|
return await handlePageRoute(req, res, pageRoute, routes, config, loadModule, url);
|
|
@@ -166,7 +185,7 @@ export async function createServer(options = {}) {
|
|
|
166
185
|
|
|
167
186
|
return new Promise((resolve, reject) => {
|
|
168
187
|
// Handle port in use error
|
|
169
|
-
server.on('error', (err) => {
|
|
188
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
170
189
|
if (err.code === 'EADDRINUSE') {
|
|
171
190
|
logger.portInUse(port);
|
|
172
191
|
process.exit(1);
|
|
@@ -264,8 +283,8 @@ async function handleApiRoute(req, res, route, loadModule) {
|
|
|
264
283
|
// Enhanced response
|
|
265
284
|
const enhancedRes = createApiResponse(res);
|
|
266
285
|
|
|
267
|
-
// Find handler
|
|
268
|
-
const handler = module[method] || module.default;
|
|
286
|
+
// Find handler (check both lowercase and uppercase method names)
|
|
287
|
+
const handler = module[method] || module[method.toUpperCase()] || module.default;
|
|
269
288
|
|
|
270
289
|
if (!handler) {
|
|
271
290
|
enhancedRes.status(405).json({ error: 'Method not allowed' });
|
|
@@ -457,7 +476,7 @@ async function handlePageRoute(req, res, route, routes, config, loadModule, url)
|
|
|
457
476
|
favicon: config.favicon || null,
|
|
458
477
|
needsHydration: isClientComponent,
|
|
459
478
|
componentPath: route.filePath,
|
|
460
|
-
route: route.path || pathname,
|
|
479
|
+
route: route.path || url.pathname,
|
|
461
480
|
isSSG: !!pageModule.getStaticProps
|
|
462
481
|
});
|
|
463
482
|
|
|
@@ -39,7 +39,13 @@ const MIME_TYPES = {
|
|
|
39
39
|
/**
|
|
40
40
|
* Creates and starts the FlexiReact dev server
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
interface ServerOptions {
|
|
43
|
+
port?: number;
|
|
44
|
+
host?: string;
|
|
45
|
+
pagesDir?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createServer(options: ServerOptions = {}) {
|
|
43
49
|
const {
|
|
44
50
|
port = PORT,
|
|
45
51
|
host = HOST,
|
|
@@ -97,7 +103,7 @@ export function createServer(options = {}) {
|
|
|
97
103
|
}
|
|
98
104
|
});
|
|
99
105
|
|
|
100
|
-
server.listen(port, host, () => {
|
|
106
|
+
server.listen(port as number, host as string, () => {
|
|
101
107
|
console.log('');
|
|
102
108
|
console.log(' ⚡ FlexiReact Dev Server');
|
|
103
109
|
console.log(' ─────────────────────────');
|