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