@flexireact/core 2.0.1 → 2.2.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.
@@ -32,7 +32,23 @@ export async function renderPage(options) {
32
32
 
33
33
  try {
34
34
  // Build the component tree - start with the page component
35
- let element = React.createElement(Component, props);
35
+ let element: any = React.createElement(Component, props);
36
+
37
+ // Wrap with error boundary if error component exists
38
+ if (error) {
39
+ element = React.createElement(ErrorBoundaryWrapper as any, {
40
+ fallback: error,
41
+ children: element
42
+ });
43
+ }
44
+
45
+ // Wrap with Suspense if loading component exists (for streaming/async)
46
+ if (loading) {
47
+ element = React.createElement(React.Suspense as any, {
48
+ fallback: React.createElement(loading),
49
+ children: element
50
+ });
51
+ }
36
52
 
37
53
  // Wrap with layouts (innermost to outermost)
38
54
  // Each layout receives children as a prop
@@ -75,6 +91,164 @@ export async function renderPage(options) {
75
91
  }
76
92
  }
77
93
 
94
+ /**
95
+ * Streaming SSR with React 18
96
+ * Renders the page progressively, sending HTML chunks as they become ready
97
+ */
98
+ export async function renderPageStream(options: {
99
+ Component: React.ComponentType<any>;
100
+ props?: Record<string, any>;
101
+ layouts?: Array<{ Component: React.ComponentType<any>; props?: Record<string, any> }>;
102
+ loading?: React.ComponentType | null;
103
+ error?: React.ComponentType<{ error: Error }> | null;
104
+ title?: string;
105
+ meta?: Record<string, string>;
106
+ scripts?: Array<string | { src?: string; content?: string; type?: string }>;
107
+ styles?: Array<string | { content: string }>;
108
+ favicon?: string | null;
109
+ route?: string;
110
+ onShellReady?: () => void;
111
+ onAllReady?: () => void;
112
+ onError?: (error: Error) => void;
113
+ }): Promise<{ stream: NodeJS.ReadableStream; shellReady: Promise<void> }> {
114
+ const {
115
+ Component,
116
+ props = {},
117
+ layouts = [],
118
+ loading = null,
119
+ error = null,
120
+ title = 'FlexiReact App',
121
+ meta = {},
122
+ scripts = [],
123
+ styles = [],
124
+ favicon = null,
125
+ route = '/',
126
+ onShellReady,
127
+ onAllReady,
128
+ onError
129
+ } = options;
130
+
131
+ const renderStart = Date.now();
132
+
133
+ // Build the component tree
134
+ let element: any = React.createElement(Component, props);
135
+
136
+ // Wrap with error boundary if error component exists
137
+ if (error) {
138
+ element = React.createElement(ErrorBoundaryWrapper as any, {
139
+ fallback: error,
140
+ children: element
141
+ });
142
+ }
143
+
144
+ // Wrap with Suspense if loading component exists
145
+ if (loading) {
146
+ element = React.createElement(React.Suspense as any, {
147
+ fallback: React.createElement(loading),
148
+ children: element
149
+ });
150
+ }
151
+
152
+ // Wrap with layouts
153
+ for (const layout of [...layouts].reverse()) {
154
+ if (layout.Component) {
155
+ element = React.createElement(layout.Component, layout.props, element);
156
+ }
157
+ }
158
+
159
+ // Create the full document wrapper
160
+ const DocumentWrapper = ({ children }: { children: React.ReactNode }) => {
161
+ return React.createElement('html', { lang: 'en', className: 'dark' },
162
+ React.createElement('head', null,
163
+ React.createElement('meta', { charSet: 'UTF-8' }),
164
+ React.createElement('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }),
165
+ React.createElement('title', null, title),
166
+ favicon && React.createElement('link', { rel: 'icon', href: favicon }),
167
+ ...Object.entries(meta).map(([name, content]) =>
168
+ React.createElement('meta', { key: name, name, content })
169
+ ),
170
+ ...styles.map((style, i) =>
171
+ typeof style === 'string'
172
+ ? React.createElement('link', { key: i, rel: 'stylesheet', href: style })
173
+ : React.createElement('style', { key: i, dangerouslySetInnerHTML: { __html: style.content } })
174
+ )
175
+ ),
176
+ React.createElement('body', null,
177
+ React.createElement('div', { id: 'root' }, children),
178
+ ...scripts.map((script, i) =>
179
+ typeof script === 'string'
180
+ ? React.createElement('script', { key: i, src: script })
181
+ : script.src
182
+ ? React.createElement('script', { key: i, src: script.src, type: script.type })
183
+ : React.createElement('script', { key: i, type: script.type, dangerouslySetInnerHTML: { __html: script.content } })
184
+ )
185
+ )
186
+ );
187
+ };
188
+
189
+ const fullElement = React.createElement(DocumentWrapper, null, element);
190
+
191
+ // Create streaming render
192
+ let shellReadyResolve: () => void;
193
+ const shellReady = new Promise<void>((resolve) => {
194
+ shellReadyResolve = resolve;
195
+ });
196
+
197
+ const { pipe, abort } = renderToPipeableStream(fullElement, {
198
+ onShellReady() {
199
+ const renderTime = Date.now() - renderStart;
200
+ console.log(`⚡ Shell ready in ${renderTime}ms`);
201
+ shellReadyResolve();
202
+ onShellReady?.();
203
+ },
204
+ onAllReady() {
205
+ const renderTime = Date.now() - renderStart;
206
+ console.log(`✨ All content ready in ${renderTime}ms`);
207
+ onAllReady?.();
208
+ },
209
+ onError(err: Error) {
210
+ console.error('Streaming SSR Error:', err);
211
+ onError?.(err);
212
+ }
213
+ });
214
+
215
+ // Create a passthrough stream
216
+ const { PassThrough } = await import('stream');
217
+ const passThrough = new PassThrough();
218
+
219
+ // Pipe the render stream to our passthrough
220
+ pipe(passThrough);
221
+
222
+ return {
223
+ stream: passThrough,
224
+ shellReady
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Render to stream for HTTP response
230
+ * Use this in the server to stream HTML to the client
231
+ */
232
+ export function streamToResponse(
233
+ res: { write: (chunk: string) => void; end: () => void },
234
+ stream: NodeJS.ReadableStream,
235
+ options: { onFinish?: () => void } = {}
236
+ ): void {
237
+ stream.on('data', (chunk) => {
238
+ res.write(chunk.toString());
239
+ });
240
+
241
+ stream.on('end', () => {
242
+ res.end();
243
+ options.onFinish?.();
244
+ });
245
+
246
+ stream.on('error', (err) => {
247
+ console.error('Stream error:', err);
248
+ res.end();
249
+ });
250
+ }
251
+
78
252
  /**
79
253
  * Error Boundary Wrapper for SSR
80
254
  */
@@ -96,13 +96,14 @@ export function buildRouteTree(pagesDir, layoutsDir, appDir = null, routesDir =
96
96
  * - api/hello.ts → /api/hello (API route)
97
97
  * - dashboard/layout.tsx → layout for /dashboard/*
98
98
  */
99
- function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null) {
99
+ function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null, parentMiddleware = null) {
100
100
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
101
101
 
102
102
  // Find special files in current directory
103
103
  let layoutFile = null;
104
104
  let loadingFile = null;
105
105
  let errorFile = null;
106
+ let middlewareFile = null;
106
107
 
107
108
  for (const entry of entries) {
108
109
  if (entry.isFile()) {
@@ -114,9 +115,10 @@ function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], p
114
115
  if (name === 'layout') layoutFile = fullPath;
115
116
  if (name === 'loading') loadingFile = fullPath;
116
117
  if (name === 'error') errorFile = fullPath;
118
+ if (name === '_middleware' || name === 'middleware') middlewareFile = fullPath;
117
119
 
118
120
  // Skip special files and non-route files
119
- if (['layout', 'loading', 'error', 'not-found'].includes(name)) continue;
121
+ if (['layout', 'loading', 'error', 'not-found', '_middleware', 'middleware'].includes(name)) continue;
120
122
  if (!['.tsx', '.jsx', '.ts', '.js'].includes(ext)) continue;
121
123
 
122
124
  // API routes (in api/ folder or .ts/.js files in api/)
@@ -171,6 +173,7 @@ function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], p
171
173
  layout: layoutFile || parentLayout,
172
174
  loading: loadingFile,
173
175
  error: errorFile,
176
+ middleware: middlewareFile || parentMiddleware,
174
177
  isFlexiRouter: true,
175
178
  isServerComponent: isServerComponent(fullPath),
176
179
  isClientComponent: isClientComponent(fullPath),
@@ -205,8 +208,9 @@ function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], p
205
208
 
206
209
  const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
207
210
  const newLayout = layoutFile || parentLayout;
211
+ const newMiddleware = middlewareFile || parentMiddleware;
208
212
 
209
- scanRoutesDirectory(baseDir, fullPath, routes, newSegments, newLayout);
213
+ scanRoutesDirectory(baseDir, fullPath, routes, newSegments, newLayout, newMiddleware);
210
214
  }
211
215
  }
212
216
  }
@@ -215,17 +219,18 @@ function scanRoutesDirectory(baseDir, currentDir, routes, parentSegments = [], p
215
219
  * Scans app directory for Next.js style routing
216
220
  * Supports: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx
217
221
  */
218
- function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null) {
222
+ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null, parentMiddleware = null) {
219
223
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
220
224
 
221
225
  // Find special files in current directory
222
- const specialFiles = {
226
+ const specialFiles: Record<string, string | null> = {
223
227
  page: null,
224
228
  layout: null,
225
229
  loading: null,
226
230
  error: null,
227
231
  notFound: null,
228
- template: null
232
+ template: null,
233
+ middleware: null
229
234
  };
230
235
 
231
236
  for (const entry of entries) {
@@ -239,6 +244,7 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
239
244
  if (name === 'error') specialFiles.error = fullPath;
240
245
  if (name === 'not-found') specialFiles.notFound = fullPath;
241
246
  if (name === 'template') specialFiles.template = fullPath;
247
+ if (name === 'middleware' || name === '_middleware') specialFiles.middleware = fullPath;
242
248
  }
243
249
  }
244
250
 
@@ -257,6 +263,7 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
257
263
  error: specialFiles.error,
258
264
  notFound: specialFiles.notFound,
259
265
  template: specialFiles.template,
266
+ middleware: specialFiles.middleware || parentMiddleware,
260
267
  isAppRouter: true,
261
268
  isServerComponent: isServerComponent(specialFiles.page),
262
269
  isClientComponent: isClientComponent(specialFiles.page),
@@ -289,8 +296,9 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
289
296
 
290
297
  const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
291
298
  const newLayout = specialFiles.layout || parentLayout;
299
+ const newMiddleware = specialFiles.middleware || parentMiddleware;
292
300
 
293
- scanAppDirectory(baseDir, fullPath, routes, newSegments, newLayout);
301
+ scanAppDirectory(baseDir, fullPath, routes, newSegments, newLayout, newMiddleware);
294
302
  }
295
303
  }
296
304
  }
@@ -15,6 +15,8 @@ import { loadPlugins, pluginManager, PluginHooks } from '../plugins/index.js';
15
15
  import { getRegisteredIslands, generateAdvancedHydrationScript } from '../islands/index.js';
16
16
  import { createRequestContext, RequestContext, RouteContext } from '../context.js';
17
17
  import { logger } from '../logger.js';
18
+ import { RedirectError, NotFoundError } from '../helpers.js';
19
+ import { executeAction, deserializeArgs } from '../actions/index.js';
18
20
  import React from 'react';
19
21
 
20
22
  const __filename = fileURLToPath(import.meta.url);
@@ -124,6 +126,11 @@ export async function createServer(options: CreateServerOptions = {}) {
124
126
  return await serveClientComponent(res, config.pagesDir, componentName);
125
127
  }
126
128
 
129
+ // Handle server actions
130
+ if (effectivePath === '/_flexi/action' && req.method === 'POST') {
131
+ return await handleServerAction(req, res);
132
+ }
133
+
127
134
  // Rebuild routes in dev mode for hot reload
128
135
  if (isDev) {
129
136
  routes = buildRouteTree(config.pagesDir, config.layoutsDir);
@@ -157,7 +164,21 @@ export async function createServer(options: CreateServerOptions = {}) {
157
164
  res.writeHead(404, { 'Content-Type': 'text/html' });
158
165
  res.end(renderError(404, 'Page not found'));
159
166
 
160
- } catch (error) {
167
+ } catch (error: any) {
168
+ // Handle redirect() calls
169
+ if (error instanceof RedirectError) {
170
+ res.writeHead(error.statusCode, { 'Location': error.url });
171
+ res.end();
172
+ return;
173
+ }
174
+
175
+ // Handle notFound() calls
176
+ if (error instanceof NotFoundError) {
177
+ res.writeHead(404, { 'Content-Type': 'text/html' });
178
+ res.end(renderError(404, error.message));
179
+ return;
180
+ }
181
+
161
182
  console.error('Server Error:', error);
162
183
 
163
184
  if (!res.headersSent) {
@@ -302,6 +323,49 @@ async function handleApiRoute(req, res, route, loadModule) {
302
323
  }
303
324
  }
304
325
 
326
+ /**
327
+ * Handles server action requests
328
+ */
329
+ async function handleServerAction(req, res) {
330
+ try {
331
+ // Parse request body
332
+ const body: any = await parseBody(req);
333
+ const { actionId, args } = body;
334
+
335
+ if (!actionId) {
336
+ res.writeHead(400, { 'Content-Type': 'application/json' });
337
+ res.end(JSON.stringify({ success: false, error: 'Missing actionId' }));
338
+ return;
339
+ }
340
+
341
+ // Deserialize arguments
342
+ const deserializedArgs = deserializeArgs(args || []);
343
+
344
+ // Execute the action
345
+ const result = await executeAction(actionId, deserializedArgs, {
346
+ request: new Request(`http://${req.headers.host}${req.url}`, {
347
+ method: req.method,
348
+ headers: req.headers as any
349
+ })
350
+ });
351
+
352
+ // Send response
353
+ res.writeHead(200, {
354
+ 'Content-Type': 'application/json',
355
+ 'X-Flexi-Action': actionId
356
+ });
357
+ res.end(JSON.stringify(result));
358
+
359
+ } catch (error: any) {
360
+ console.error('Server Action Error:', error);
361
+ res.writeHead(500, { 'Content-Type': 'application/json' });
362
+ res.end(JSON.stringify({
363
+ success: false,
364
+ error: error.message || 'Action execution failed'
365
+ }));
366
+ }
367
+ }
368
+
305
369
  /**
306
370
  * Creates an enhanced API response object
307
371
  */
@@ -360,6 +424,37 @@ function createApiResponse(res) {
360
424
  */
361
425
  async function handlePageRoute(req, res, route, routes, config, loadModule, url) {
362
426
  try {
427
+ // Run route-specific middleware if exists
428
+ if (route.middleware) {
429
+ try {
430
+ const middlewareModule = await loadModule(route.middleware);
431
+ const middlewareFn = middlewareModule.default || middlewareModule.middleware;
432
+
433
+ if (typeof middlewareFn === 'function') {
434
+ const result = await middlewareFn(req, res, { route, params: route.params });
435
+
436
+ // If middleware returns a response, use it
437
+ if (result?.redirect) {
438
+ res.writeHead(result.statusCode || 307, { 'Location': result.redirect });
439
+ res.end();
440
+ return;
441
+ }
442
+
443
+ if (result?.rewrite) {
444
+ // Rewrite to different path
445
+ req.url = result.rewrite;
446
+ }
447
+
448
+ if (result === false || result?.stop) {
449
+ // Middleware stopped the request
450
+ return;
451
+ }
452
+ }
453
+ } catch (middlewareError: any) {
454
+ console.error('Route middleware error:', middlewareError.message);
455
+ }
456
+ }
457
+
363
458
  // Load page module
364
459
  const pageModule = await loadModule(route.filePath);
365
460
  const Component = pageModule.default;
@@ -4,6 +4,7 @@
4
4
  "module": "NodeNext",
5
5
  "moduleResolution": "NodeNext",
6
6
  "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "jsx": "react-jsx",
7
8
  "outDir": "./dist",
8
9
  "rootDir": ".",
9
10
  "strict": false,
@@ -22,7 +23,8 @@
22
23
  },
23
24
  "include": [
24
25
  "*.ts",
25
- "**/*.ts"
26
+ "**/*.ts",
27
+ "**/*.tsx"
26
28
  ],
27
29
  "exclude": ["node_modules", "dist"]
28
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexireact/core",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "The Modern React Framework v2 - SSR, SSG, Islands, App Router, TypeScript, Tailwind",
5
5
  "main": "core/index.ts",
6
6
  "types": "core/types.ts",