@flexireact/core 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
package/core/server/index.ts
DELETED
|
@@ -1,779 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FlexiReact Server v2
|
|
3
|
-
* Production-ready server with SSR, RSC, Islands, and more
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import http from 'http';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
-
import { loadConfig, resolvePaths } from '../config.js';
|
|
11
|
-
import { buildRouteTree, matchRoute, findRouteLayouts } from '../router/index.js';
|
|
12
|
-
import { renderPage, renderError, renderLoading } from '../render/index.js';
|
|
13
|
-
import { loadMiddleware, runMiddleware } from '../middleware/index.js';
|
|
14
|
-
import { loadPlugins, pluginManager, PluginHooks } from '../plugins/index.js';
|
|
15
|
-
import { getRegisteredIslands, generateAdvancedHydrationScript } from '../islands/index.js';
|
|
16
|
-
import { createRequestContext, RequestContext, RouteContext } from '../context.js';
|
|
17
|
-
import { logger } from '../logger.js';
|
|
18
|
-
import { RedirectError, NotFoundError } from '../helpers.js';
|
|
19
|
-
import { executeAction, deserializeArgs } from '../actions/index.js';
|
|
20
|
-
import { handleImageOptimization } from '../image/index.js';
|
|
21
|
-
import { handleFontRequest } from '../font/index.js';
|
|
22
|
-
import React from 'react';
|
|
23
|
-
|
|
24
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
-
const __dirname = path.dirname(__filename);
|
|
26
|
-
|
|
27
|
-
// MIME types
|
|
28
|
-
const MIME_TYPES = {
|
|
29
|
-
'.html': 'text/html',
|
|
30
|
-
'.css': 'text/css',
|
|
31
|
-
'.js': 'application/javascript',
|
|
32
|
-
'.mjs': 'application/javascript',
|
|
33
|
-
'.json': 'application/json',
|
|
34
|
-
'.png': 'image/png',
|
|
35
|
-
'.jpg': 'image/jpeg',
|
|
36
|
-
'.jpeg': 'image/jpeg',
|
|
37
|
-
'.gif': 'image/gif',
|
|
38
|
-
'.svg': 'image/svg+xml',
|
|
39
|
-
'.ico': 'image/x-icon',
|
|
40
|
-
'.woff': 'font/woff',
|
|
41
|
-
'.woff2': 'font/woff2',
|
|
42
|
-
'.ttf': 'font/ttf',
|
|
43
|
-
'.webp': 'image/webp',
|
|
44
|
-
'.mp4': 'video/mp4',
|
|
45
|
-
'.webm': 'video/webm'
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates the FlexiReact server
|
|
50
|
-
*/
|
|
51
|
-
interface CreateServerOptions {
|
|
52
|
-
projectRoot?: string;
|
|
53
|
-
mode?: 'development' | 'production';
|
|
54
|
-
port?: number;
|
|
55
|
-
host?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function createServer(options: CreateServerOptions = {}) {
|
|
59
|
-
const serverStartTime = Date.now();
|
|
60
|
-
const projectRoot = options.projectRoot || process.cwd();
|
|
61
|
-
const isDev = options.mode === 'development';
|
|
62
|
-
|
|
63
|
-
// Show logo
|
|
64
|
-
logger.logo();
|
|
65
|
-
|
|
66
|
-
// Load configuration
|
|
67
|
-
const rawConfig = await loadConfig(projectRoot);
|
|
68
|
-
const config = resolvePaths(rawConfig, projectRoot);
|
|
69
|
-
|
|
70
|
-
// Load plugins
|
|
71
|
-
await loadPlugins(projectRoot, config);
|
|
72
|
-
|
|
73
|
-
// Run config hook
|
|
74
|
-
await pluginManager.runHook(PluginHooks.CONFIG, config);
|
|
75
|
-
|
|
76
|
-
// Load middleware
|
|
77
|
-
const middleware = await loadMiddleware(projectRoot);
|
|
78
|
-
|
|
79
|
-
// Build routes
|
|
80
|
-
let routes = buildRouteTree(config.pagesDir, config.layoutsDir);
|
|
81
|
-
|
|
82
|
-
// Run routes loaded hook
|
|
83
|
-
await pluginManager.runHook(PluginHooks.ROUTES_LOADED, routes);
|
|
84
|
-
|
|
85
|
-
// Create module loader with cache busting for dev
|
|
86
|
-
const loadModule = createModuleLoader(isDev);
|
|
87
|
-
|
|
88
|
-
// Create HTTP server
|
|
89
|
-
const server = http.createServer(async (req, res) => {
|
|
90
|
-
const startTime = Date.now();
|
|
91
|
-
|
|
92
|
-
// Parse URL early so it's available in finally block
|
|
93
|
-
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
94
|
-
const pathname = url.pathname;
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
|
|
98
|
-
// Run request hook
|
|
99
|
-
await pluginManager.runHook(PluginHooks.REQUEST, req, res);
|
|
100
|
-
|
|
101
|
-
// Run middleware
|
|
102
|
-
const middlewareResult = await runMiddleware(req, res, middleware);
|
|
103
|
-
if (!middlewareResult.continue) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Handle rewritten URL
|
|
108
|
-
const effectivePath = middlewareResult.rewritten
|
|
109
|
-
? new URL(req.url, `http://${req.headers.host}`).pathname
|
|
110
|
-
: pathname;
|
|
111
|
-
|
|
112
|
-
// Serve static files from public directory
|
|
113
|
-
if (await serveStaticFile(res, config.publicDir, effectivePath)) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Serve built assets in production
|
|
118
|
-
if (!isDev && effectivePath.startsWith('/_flexi/')) {
|
|
119
|
-
const assetPath = path.join(config.outDir, 'client', effectivePath.slice(8));
|
|
120
|
-
if (await serveStaticFile(res, path.dirname(assetPath), path.basename(assetPath))) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Serve client components (for hydration)
|
|
126
|
-
if (effectivePath.startsWith('/_flexi/component/')) {
|
|
127
|
-
const componentName = effectivePath.slice(18).replace('.js', '');
|
|
128
|
-
return await serveClientComponent(res, config.pagesDir, componentName);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Handle server actions
|
|
132
|
-
if (effectivePath === '/_flexi/action' && req.method === 'POST') {
|
|
133
|
-
return await handleServerAction(req, res);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Handle image optimization
|
|
137
|
-
if (effectivePath.startsWith('/_flexi/image')) {
|
|
138
|
-
return await handleImageOptimization(req, res, config.images || {});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Handle font requests
|
|
142
|
-
if (effectivePath.startsWith('/_flexi/font')) {
|
|
143
|
-
return await handleFontRequest(req, res);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Rebuild routes in dev mode for hot reload
|
|
147
|
-
if (isDev) {
|
|
148
|
-
routes = buildRouteTree(config.pagesDir, config.layoutsDir);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Match API routes
|
|
152
|
-
const apiRoute = matchRoute(effectivePath, routes.api);
|
|
153
|
-
if (apiRoute) {
|
|
154
|
-
return await handleApiRoute(req, res, apiRoute, loadModule);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Match FlexiReact v2 routes (routes/ directory - priority)
|
|
158
|
-
const flexiRoute = matchRoute(effectivePath, routes.flexiRoutes || []);
|
|
159
|
-
if (flexiRoute) {
|
|
160
|
-
return await handlePageRoute(req, res, flexiRoute, routes, config, loadModule, url);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Match app routes (app/ directory - Next.js style)
|
|
164
|
-
const appRoute = matchRoute(effectivePath, routes.appRoutes || []);
|
|
165
|
-
if (appRoute) {
|
|
166
|
-
return await handlePageRoute(req, res, appRoute, routes, config, loadModule, url);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Match page routes (pages/ directory - legacy fallback)
|
|
170
|
-
const pageRoute = matchRoute(effectivePath, routes.pages);
|
|
171
|
-
if (pageRoute) {
|
|
172
|
-
return await handlePageRoute(req, res, pageRoute, routes, config, loadModule, url);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 404 Not Found
|
|
176
|
-
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
177
|
-
res.end(renderError(404, 'Page not found'));
|
|
178
|
-
|
|
179
|
-
} catch (error: any) {
|
|
180
|
-
// Handle redirect() calls
|
|
181
|
-
if (error instanceof RedirectError) {
|
|
182
|
-
res.writeHead(error.statusCode, { 'Location': error.url });
|
|
183
|
-
res.end();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Handle notFound() calls
|
|
188
|
-
if (error instanceof NotFoundError) {
|
|
189
|
-
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
190
|
-
res.end(renderError(404, error.message));
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
console.error('Server Error:', error);
|
|
195
|
-
|
|
196
|
-
if (!res.headersSent) {
|
|
197
|
-
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
198
|
-
res.end(renderError(500, error.message, isDev ? error.stack : null));
|
|
199
|
-
}
|
|
200
|
-
} finally {
|
|
201
|
-
const duration = Date.now() - startTime;
|
|
202
|
-
if (isDev) {
|
|
203
|
-
// Determine route type for logging
|
|
204
|
-
const routeType = pathname.startsWith('/api/') ? 'api' :
|
|
205
|
-
pathname.startsWith('/_flexi/') ? 'asset' :
|
|
206
|
-
pathname.match(/\.(js|css|png|jpg|svg|ico)$/) ? 'asset' : 'dynamic';
|
|
207
|
-
logger.request(req.method, pathname, res.statusCode, duration, { type: routeType });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Run response hook
|
|
211
|
-
await pluginManager.runHook(PluginHooks.RESPONSE, req, res, duration);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Start server
|
|
216
|
-
const port = process.env.PORT || options.port || config.server.port;
|
|
217
|
-
const host = options.host || config.server.host;
|
|
218
|
-
|
|
219
|
-
return new Promise((resolve, reject) => {
|
|
220
|
-
// Handle port in use error
|
|
221
|
-
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
222
|
-
if (err.code === 'EADDRINUSE') {
|
|
223
|
-
logger.portInUse(port);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
} else {
|
|
226
|
-
logger.error('Server error', err);
|
|
227
|
-
reject(err);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
server.listen(port, host, async () => {
|
|
232
|
-
// Show startup info with styled logger
|
|
233
|
-
logger.serverStart({
|
|
234
|
-
port,
|
|
235
|
-
host,
|
|
236
|
-
mode: isDev ? 'development' : 'production',
|
|
237
|
-
pagesDir: config.pagesDir,
|
|
238
|
-
islands: config.islands?.enabled,
|
|
239
|
-
rsc: config.rsc?.enabled
|
|
240
|
-
}, serverStartTime);
|
|
241
|
-
|
|
242
|
-
// Run server start hook
|
|
243
|
-
await pluginManager.runHook(PluginHooks.SERVER_START, server);
|
|
244
|
-
|
|
245
|
-
resolve(server);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Creates a module loader with optional cache busting
|
|
252
|
-
*/
|
|
253
|
-
function createModuleLoader(isDev) {
|
|
254
|
-
return async (filePath) => {
|
|
255
|
-
const url = pathToFileURL(filePath).href;
|
|
256
|
-
const cacheBuster = isDev ? `?t=${Date.now()}` : '';
|
|
257
|
-
return import(`${url}${cacheBuster}`);
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Serves static files
|
|
263
|
-
*/
|
|
264
|
-
async function serveStaticFile(res, baseDir, pathname) {
|
|
265
|
-
// Prevent directory traversal
|
|
266
|
-
const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
|
|
267
|
-
const filePath = path.join(baseDir, safePath);
|
|
268
|
-
|
|
269
|
-
// Check if file exists and is within base directory
|
|
270
|
-
if (!filePath.startsWith(baseDir) || !fs.existsSync(filePath)) {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const stat = fs.statSync(filePath);
|
|
275
|
-
if (!stat.isFile()) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
280
|
-
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
281
|
-
|
|
282
|
-
res.writeHead(200, {
|
|
283
|
-
'Content-Type': contentType,
|
|
284
|
-
'Content-Length': stat.size,
|
|
285
|
-
'Cache-Control': 'public, max-age=31536000'
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
fs.createReadStream(filePath).pipe(res);
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Handles API route requests
|
|
294
|
-
*/
|
|
295
|
-
async function handleApiRoute(req, res, route, loadModule) {
|
|
296
|
-
try {
|
|
297
|
-
const module = await loadModule(route.filePath);
|
|
298
|
-
const method = req.method.toLowerCase();
|
|
299
|
-
|
|
300
|
-
// Parse request body
|
|
301
|
-
const body = await parseBody(req);
|
|
302
|
-
|
|
303
|
-
// Parse query
|
|
304
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
305
|
-
const query = Object.fromEntries(url.searchParams);
|
|
306
|
-
|
|
307
|
-
// Enhanced request
|
|
308
|
-
const enhancedReq = {
|
|
309
|
-
...req,
|
|
310
|
-
body,
|
|
311
|
-
query,
|
|
312
|
-
params: route.params,
|
|
313
|
-
method: req.method
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
// Enhanced response
|
|
317
|
-
const enhancedRes = createApiResponse(res);
|
|
318
|
-
|
|
319
|
-
// Find handler (check both lowercase and uppercase method names)
|
|
320
|
-
const handler = module[method] || module[method.toUpperCase()] || module.default;
|
|
321
|
-
|
|
322
|
-
if (!handler) {
|
|
323
|
-
enhancedRes.status(405).json({ error: 'Method not allowed' });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
await handler(enhancedReq, enhancedRes);
|
|
328
|
-
|
|
329
|
-
} catch (error) {
|
|
330
|
-
console.error('API Error:', error);
|
|
331
|
-
if (!res.headersSent) {
|
|
332
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
333
|
-
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Handles server action requests
|
|
340
|
-
*/
|
|
341
|
-
async function handleServerAction(req, res) {
|
|
342
|
-
try {
|
|
343
|
-
// Parse request body
|
|
344
|
-
const body: any = await parseBody(req);
|
|
345
|
-
const { actionId, args } = body;
|
|
346
|
-
|
|
347
|
-
if (!actionId) {
|
|
348
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
349
|
-
res.end(JSON.stringify({ success: false, error: 'Missing actionId' }));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Deserialize arguments
|
|
354
|
-
const deserializedArgs = deserializeArgs(args || []);
|
|
355
|
-
|
|
356
|
-
// Execute the action
|
|
357
|
-
const result = await executeAction(actionId, deserializedArgs, {
|
|
358
|
-
request: new Request(`http://${req.headers.host}${req.url}`, {
|
|
359
|
-
method: req.method,
|
|
360
|
-
headers: req.headers as any
|
|
361
|
-
})
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// Send response
|
|
365
|
-
res.writeHead(200, {
|
|
366
|
-
'Content-Type': 'application/json',
|
|
367
|
-
'X-Flexi-Action': actionId
|
|
368
|
-
});
|
|
369
|
-
res.end(JSON.stringify(result));
|
|
370
|
-
|
|
371
|
-
} catch (error: any) {
|
|
372
|
-
console.error('Server Action Error:', error);
|
|
373
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
374
|
-
res.end(JSON.stringify({
|
|
375
|
-
success: false,
|
|
376
|
-
error: error.message || 'Action execution failed'
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Creates an enhanced API response object
|
|
383
|
-
*/
|
|
384
|
-
function createApiResponse(res) {
|
|
385
|
-
return {
|
|
386
|
-
_res: res,
|
|
387
|
-
_status: 200,
|
|
388
|
-
_headers: {},
|
|
389
|
-
|
|
390
|
-
status(code) {
|
|
391
|
-
this._status = code;
|
|
392
|
-
return this;
|
|
393
|
-
},
|
|
394
|
-
|
|
395
|
-
setHeader(name, value) {
|
|
396
|
-
this._headers[name] = value;
|
|
397
|
-
return this;
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
json(data) {
|
|
401
|
-
this._headers['Content-Type'] = 'application/json';
|
|
402
|
-
this._send(JSON.stringify(data));
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
send(data) {
|
|
406
|
-
if (typeof data === 'object') {
|
|
407
|
-
this.json(data);
|
|
408
|
-
} else {
|
|
409
|
-
this._headers['Content-Type'] = this._headers['Content-Type'] || 'text/plain';
|
|
410
|
-
this._send(String(data));
|
|
411
|
-
}
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
html(data) {
|
|
415
|
-
this._headers['Content-Type'] = 'text/html';
|
|
416
|
-
this._send(data);
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
redirect(url, status = 302) {
|
|
420
|
-
this._status = status;
|
|
421
|
-
this._headers['Location'] = url;
|
|
422
|
-
this._send('');
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
_send(body) {
|
|
426
|
-
if (!this._res.headersSent) {
|
|
427
|
-
this._res.writeHead(this._status, this._headers);
|
|
428
|
-
this._res.end(body);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Handles page route requests with SSR
|
|
436
|
-
*/
|
|
437
|
-
async function handlePageRoute(req, res, route, routes, config, loadModule, url) {
|
|
438
|
-
try {
|
|
439
|
-
// Run route-specific middleware if exists
|
|
440
|
-
if (route.middleware) {
|
|
441
|
-
try {
|
|
442
|
-
const middlewareModule = await loadModule(route.middleware);
|
|
443
|
-
const middlewareFn = middlewareModule.default || middlewareModule.middleware;
|
|
444
|
-
|
|
445
|
-
if (typeof middlewareFn === 'function') {
|
|
446
|
-
const result = await middlewareFn(req, res, { route, params: route.params });
|
|
447
|
-
|
|
448
|
-
// If middleware returns a response, use it
|
|
449
|
-
if (result?.redirect) {
|
|
450
|
-
res.writeHead(result.statusCode || 307, { 'Location': result.redirect });
|
|
451
|
-
res.end();
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (result?.rewrite) {
|
|
456
|
-
// Rewrite to different path
|
|
457
|
-
req.url = result.rewrite;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (result === false || result?.stop) {
|
|
461
|
-
// Middleware stopped the request
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} catch (middlewareError: any) {
|
|
466
|
-
console.error('Route middleware error:', middlewareError.message);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Load page module
|
|
471
|
-
const pageModule = await loadModule(route.filePath);
|
|
472
|
-
const Component = pageModule.default;
|
|
473
|
-
|
|
474
|
-
if (!Component) {
|
|
475
|
-
throw new Error(`No default export in ${route.filePath}`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Create request context
|
|
479
|
-
const query = Object.fromEntries(url.searchParams);
|
|
480
|
-
const context = createRequestContext(req, res, route.params, query);
|
|
481
|
-
|
|
482
|
-
// Get page props
|
|
483
|
-
let props = { params: route.params, query };
|
|
484
|
-
|
|
485
|
-
// Handle getServerSideProps
|
|
486
|
-
if (pageModule.getServerSideProps) {
|
|
487
|
-
const result = await pageModule.getServerSideProps({
|
|
488
|
-
params: route.params,
|
|
489
|
-
query,
|
|
490
|
-
req,
|
|
491
|
-
res
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
if (result.redirect) {
|
|
495
|
-
res.writeHead(result.redirect.statusCode || 302, {
|
|
496
|
-
Location: result.redirect.destination
|
|
497
|
-
});
|
|
498
|
-
res.end();
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (result.notFound) {
|
|
503
|
-
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
504
|
-
res.end(renderError(404, 'Page not found'));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
props = { ...props, ...result.props };
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Handle getStaticProps (for ISR)
|
|
512
|
-
if (pageModule.getStaticProps) {
|
|
513
|
-
const result = await pageModule.getStaticProps({ params: route.params });
|
|
514
|
-
|
|
515
|
-
if (result.notFound) {
|
|
516
|
-
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
517
|
-
res.end(renderError(404, 'Page not found'));
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
props = { ...props, ...result.props };
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Load layouts (only if layouts directory exists and has layouts)
|
|
525
|
-
const layouts = [];
|
|
526
|
-
|
|
527
|
-
try {
|
|
528
|
-
const layoutConfigs = findRouteLayouts(route, routes.layouts);
|
|
529
|
-
for (const layoutConfig of layoutConfigs) {
|
|
530
|
-
if (layoutConfig.filePath) {
|
|
531
|
-
const layoutModule = await loadModule(layoutConfig.filePath);
|
|
532
|
-
if (layoutModule.default) {
|
|
533
|
-
layouts.push({
|
|
534
|
-
Component: layoutModule.default,
|
|
535
|
-
props: {}
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} catch (layoutError) {
|
|
541
|
-
// Layouts are optional, continue without them
|
|
542
|
-
console.warn('Layout loading skipped:', layoutError.message);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Load loading component if exists
|
|
546
|
-
let LoadingComponent = null;
|
|
547
|
-
if (route.loading) {
|
|
548
|
-
const loadingModule = await loadModule(route.loading);
|
|
549
|
-
LoadingComponent = loadingModule.default;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Load error component if exists
|
|
553
|
-
let ErrorComponent = null;
|
|
554
|
-
if (route.error) {
|
|
555
|
-
const errorModule = await loadModule(route.error);
|
|
556
|
-
ErrorComponent = errorModule.default;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Run before render hook
|
|
560
|
-
props = await pluginManager.runWaterfallHook(
|
|
561
|
-
PluginHooks.BEFORE_RENDER,
|
|
562
|
-
props,
|
|
563
|
-
{ route, Component }
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
// Check if this is a client component (needs hydration)
|
|
567
|
-
const isClientComponent = route.isClientComponent ||
|
|
568
|
-
(pageModule.__isClient) ||
|
|
569
|
-
(typeof pageModule.default === 'function' && pageModule.default.toString().includes('useState'));
|
|
570
|
-
|
|
571
|
-
// Render the page
|
|
572
|
-
let html = await renderPage({
|
|
573
|
-
Component,
|
|
574
|
-
props,
|
|
575
|
-
layouts,
|
|
576
|
-
loading: LoadingComponent,
|
|
577
|
-
error: ErrorComponent,
|
|
578
|
-
islands: getRegisteredIslands(),
|
|
579
|
-
title: pageModule.title || pageModule.metadata?.title || 'FlexiReact App',
|
|
580
|
-
meta: pageModule.metadata || {},
|
|
581
|
-
styles: config.styles || [],
|
|
582
|
-
scripts: config.scripts || [],
|
|
583
|
-
favicon: config.favicon || null,
|
|
584
|
-
needsHydration: isClientComponent,
|
|
585
|
-
componentPath: route.filePath,
|
|
586
|
-
route: route.path || url.pathname,
|
|
587
|
-
isSSG: !!pageModule.getStaticProps
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Add island hydration script
|
|
591
|
-
const islands = getRegisteredIslands();
|
|
592
|
-
if (islands.length > 0 && config.islands.enabled) {
|
|
593
|
-
const hydrationScript = generateAdvancedHydrationScript(islands);
|
|
594
|
-
html = html.replace('</body>', `${hydrationScript}</body>`);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Add client hydration for 'use client' components
|
|
598
|
-
if (isClientComponent) {
|
|
599
|
-
const hydrationScript = generateClientHydrationScript(route.filePath, props);
|
|
600
|
-
html = html.replace('</body>', `${hydrationScript}</body>`);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Run after render hook
|
|
604
|
-
html = await pluginManager.runWaterfallHook(
|
|
605
|
-
PluginHooks.AFTER_RENDER,
|
|
606
|
-
html,
|
|
607
|
-
{ route, Component, props }
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
611
|
-
res.end(html);
|
|
612
|
-
|
|
613
|
-
} catch (error) {
|
|
614
|
-
console.error('Page Render Error:', error);
|
|
615
|
-
throw error;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Serves a client component as JavaScript for hydration
|
|
621
|
-
*/
|
|
622
|
-
async function serveClientComponent(res, pagesDir, componentName) {
|
|
623
|
-
const { transformSync } = await import('esbuild');
|
|
624
|
-
|
|
625
|
-
// Remove .tsx.js or .jsx.js suffix if present
|
|
626
|
-
const cleanName = componentName.replace(/\.(tsx|jsx|ts|js)\.js$/, '').replace(/\.js$/, '');
|
|
627
|
-
|
|
628
|
-
// Find the component file (support TypeScript)
|
|
629
|
-
const possiblePaths = [
|
|
630
|
-
path.join(pagesDir, `${cleanName}.tsx`),
|
|
631
|
-
path.join(pagesDir, `${cleanName}.ts`),
|
|
632
|
-
path.join(pagesDir, `${cleanName}.jsx`),
|
|
633
|
-
path.join(pagesDir, `${cleanName}.js`),
|
|
634
|
-
];
|
|
635
|
-
|
|
636
|
-
let componentPath = null;
|
|
637
|
-
for (const p of possiblePaths) {
|
|
638
|
-
if (fs.existsSync(p)) {
|
|
639
|
-
componentPath = p;
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (!componentPath) {
|
|
645
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
646
|
-
res.end(`Component not found: ${cleanName}`);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Determine loader based on extension
|
|
651
|
-
const ext = path.extname(componentPath);
|
|
652
|
-
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
653
|
-
|
|
654
|
-
try {
|
|
655
|
-
let source = fs.readFileSync(componentPath, 'utf-8');
|
|
656
|
-
|
|
657
|
-
// Remove 'use client' directive
|
|
658
|
-
source = source.replace(/^['"]use (client|server|island)['"];?\s*/m, '');
|
|
659
|
-
|
|
660
|
-
// Transform for browser
|
|
661
|
-
const result = transformSync(source, {
|
|
662
|
-
loader,
|
|
663
|
-
format: 'esm',
|
|
664
|
-
jsx: 'transform',
|
|
665
|
-
jsxFactory: 'React.createElement',
|
|
666
|
-
jsxFragment: 'React.Fragment',
|
|
667
|
-
target: 'es2020',
|
|
668
|
-
// Replace React imports with global
|
|
669
|
-
banner: `
|
|
670
|
-
const React = window.React;
|
|
671
|
-
const useState = window.useState;
|
|
672
|
-
const useEffect = window.useEffect;
|
|
673
|
-
const useCallback = window.useCallback;
|
|
674
|
-
const useMemo = window.useMemo;
|
|
675
|
-
const useRef = window.useRef;
|
|
676
|
-
`
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
// Remove all React imports since we're using globals
|
|
680
|
-
let code = result.code;
|
|
681
|
-
// Remove: import React from 'react'
|
|
682
|
-
code = code.replace(/import\s+React\s+from\s+['"]react['"];?\s*/g, '');
|
|
683
|
-
// Remove: import { useState } from 'react'
|
|
684
|
-
code = code.replace(/import\s+\{[^}]+\}\s+from\s+['"]react['"];?\s*/g, '');
|
|
685
|
-
// Remove: import React, { useState } from 'react'
|
|
686
|
-
code = code.replace(/import\s+React\s*,\s*\{[^}]+\}\s+from\s+['"]react['"];?\s*/g, '');
|
|
687
|
-
|
|
688
|
-
res.writeHead(200, {
|
|
689
|
-
'Content-Type': 'application/javascript',
|
|
690
|
-
'Cache-Control': 'no-cache'
|
|
691
|
-
});
|
|
692
|
-
res.end(code);
|
|
693
|
-
|
|
694
|
-
} catch (error) {
|
|
695
|
-
console.error('Error serving client component:', error);
|
|
696
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
697
|
-
res.end('Error compiling component');
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Generates client hydration script for 'use client' components
|
|
703
|
-
*/
|
|
704
|
-
function generateClientHydrationScript(componentPath, props) {
|
|
705
|
-
// Create a relative path for the client bundle (handle .tsx, .ts, .jsx, .js)
|
|
706
|
-
const ext = path.extname(componentPath);
|
|
707
|
-
const componentName = path.basename(componentPath, ext);
|
|
708
|
-
|
|
709
|
-
return `
|
|
710
|
-
<script type="module">
|
|
711
|
-
// FlexiReact Client Hydration
|
|
712
|
-
(async function() {
|
|
713
|
-
try {
|
|
714
|
-
const React = await import('https://esm.sh/react@18.3.1');
|
|
715
|
-
const ReactDOM = await import('https://esm.sh/react-dom@18.3.1/client');
|
|
716
|
-
|
|
717
|
-
// Make React available globally for the component
|
|
718
|
-
window.React = React.default || React;
|
|
719
|
-
window.useState = React.useState;
|
|
720
|
-
window.useEffect = React.useEffect;
|
|
721
|
-
window.useCallback = React.useCallback;
|
|
722
|
-
window.useMemo = React.useMemo;
|
|
723
|
-
window.useRef = React.useRef;
|
|
724
|
-
|
|
725
|
-
// Fetch the component code
|
|
726
|
-
const response = await fetch('/_flexi/component/${componentName}.js');
|
|
727
|
-
const code = await response.text();
|
|
728
|
-
|
|
729
|
-
// Create and import the module
|
|
730
|
-
const blob = new Blob([code], { type: 'application/javascript' });
|
|
731
|
-
const moduleUrl = URL.createObjectURL(blob);
|
|
732
|
-
const module = await import(moduleUrl);
|
|
733
|
-
|
|
734
|
-
const Component = module.default;
|
|
735
|
-
const props = ${JSON.stringify(props)};
|
|
736
|
-
|
|
737
|
-
// Hydrate the root
|
|
738
|
-
const root = document.getElementById('root');
|
|
739
|
-
ReactDOM.hydrateRoot(root, window.React.createElement(Component, props));
|
|
740
|
-
|
|
741
|
-
console.log('⚡ FlexiReact: Component hydrated successfully');
|
|
742
|
-
} catch (error) {
|
|
743
|
-
console.error('⚡ FlexiReact: Hydration failed', error);
|
|
744
|
-
}
|
|
745
|
-
})();
|
|
746
|
-
</script>`;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Parses request body
|
|
751
|
-
*/
|
|
752
|
-
async function parseBody(req) {
|
|
753
|
-
return new Promise((resolve) => {
|
|
754
|
-
const contentType = req.headers['content-type'] || '';
|
|
755
|
-
let body = '';
|
|
756
|
-
|
|
757
|
-
req.on('data', chunk => {
|
|
758
|
-
body += chunk.toString();
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
req.on('end', () => {
|
|
762
|
-
try {
|
|
763
|
-
if (contentType.includes('application/json') && body) {
|
|
764
|
-
resolve(JSON.parse(body));
|
|
765
|
-
} else if (contentType.includes('application/x-www-form-urlencoded') && body) {
|
|
766
|
-
resolve(Object.fromEntries(new URLSearchParams(body)));
|
|
767
|
-
} else {
|
|
768
|
-
resolve(body || null);
|
|
769
|
-
}
|
|
770
|
-
} catch {
|
|
771
|
-
resolve(body);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
req.on('error', () => resolve(null));
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
export default createServer;
|