@flexireact/core 2.1.0 → 2.3.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.
@@ -91,6 +91,164 @@ export async function renderPage(options) {
91
91
  }
92
92
  }
93
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
+
94
252
  /**
95
253
  * Error Boundary Wrapper for SSR
96
254
  */
@@ -16,6 +16,9 @@ import { getRegisteredIslands, generateAdvancedHydrationScript } from '../island
16
16
  import { createRequestContext, RequestContext, RouteContext } from '../context.js';
17
17
  import { logger } from '../logger.js';
18
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';
19
22
  import React from 'react';
20
23
 
21
24
  const __filename = fileURLToPath(import.meta.url);
@@ -125,6 +128,21 @@ export async function createServer(options: CreateServerOptions = {}) {
125
128
  return await serveClientComponent(res, config.pagesDir, componentName);
126
129
  }
127
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
+
128
146
  // Rebuild routes in dev mode for hot reload
129
147
  if (isDev) {
130
148
  routes = buildRouteTree(config.pagesDir, config.layoutsDir);
@@ -317,6 +335,49 @@ async function handleApiRoute(req, res, route, loadModule) {
317
335
  }
318
336
  }
319
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
+
320
381
  /**
321
382
  * Creates an enhanced API response object
322
383
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexireact/core",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",