@flexireact/core 2.2.0 → 2.4.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,264 @@
1
+ /**
2
+ * FlexiReact Partial Prerendering (PPR)
3
+ *
4
+ * Combines static shell with dynamic content:
5
+ * - Static parts are prerendered at build time
6
+ * - Dynamic parts stream in at request time
7
+ * - Best of both SSG and SSR
8
+ */
9
+
10
+ import React from 'react';
11
+ import { renderToString } from 'react-dom/server';
12
+ import { cache } from './cache.js';
13
+
14
+ // PPR configuration
15
+ export interface PPRConfig {
16
+ // Static shell cache duration
17
+ shellCacheTTL?: number;
18
+ // Dynamic content timeout
19
+ dynamicTimeout?: number;
20
+ // Fallback for dynamic parts
21
+ fallback?: React.ReactNode;
22
+ }
23
+
24
+ // Mark component as dynamic (not prerendered)
25
+ export function dynamic<T extends React.ComponentType<any>>(
26
+ Component: T,
27
+ options?: { fallback?: React.ReactNode }
28
+ ): T {
29
+ (Component as any).__flexi_dynamic = true;
30
+ (Component as any).__flexi_fallback = options?.fallback;
31
+ return Component;
32
+ }
33
+
34
+ // Mark component as static (prerendered)
35
+ export function staticComponent<T extends React.ComponentType<any>>(Component: T): T {
36
+ (Component as any).__flexi_static = true;
37
+ return Component;
38
+ }
39
+
40
+ // Suspense boundary for PPR
41
+ export interface SuspenseBoundaryProps {
42
+ children: React.ReactNode;
43
+ fallback?: React.ReactNode;
44
+ id?: string;
45
+ }
46
+
47
+ export function PPRBoundary({ children, fallback, id }: SuspenseBoundaryProps): React.ReactElement {
48
+ return React.createElement(
49
+ React.Suspense,
50
+ {
51
+ fallback: fallback || React.createElement('div', {
52
+ 'data-ppr-placeholder': id || 'loading',
53
+ className: 'ppr-loading'
54
+ }, '⏳')
55
+ },
56
+ children
57
+ );
58
+ }
59
+
60
+ // PPR Shell - static wrapper
61
+ export interface PPRShellProps {
62
+ children: React.ReactNode;
63
+ fallback?: React.ReactNode;
64
+ }
65
+
66
+ export function PPRShell({ children, fallback }: PPRShellProps): React.ReactElement {
67
+ return React.createElement(
68
+ 'div',
69
+ { 'data-ppr-shell': 'true' },
70
+ React.createElement(
71
+ React.Suspense,
72
+ { fallback: fallback || null },
73
+ children
74
+ )
75
+ );
76
+ }
77
+
78
+ // Prerender a page with PPR
79
+ export interface PPRRenderResult {
80
+ staticShell: string;
81
+ dynamicParts: Map<string, () => Promise<string>>;
82
+ fullHtml: string;
83
+ }
84
+
85
+ export async function prerenderWithPPR(
86
+ Component: React.ComponentType<any>,
87
+ props: any,
88
+ config: PPRConfig = {}
89
+ ): Promise<PPRRenderResult> {
90
+ const { shellCacheTTL = 3600 } = config;
91
+
92
+ // Track dynamic parts
93
+ const dynamicParts = new Map<string, () => Promise<string>>();
94
+ let dynamicCounter = 0;
95
+
96
+ // Create element
97
+ const element = React.createElement(Component, props);
98
+
99
+ // Render static shell (with placeholders for dynamic parts)
100
+ const staticShell = renderToString(element);
101
+
102
+ // Cache the static shell
103
+ const cacheKey = `ppr:${Component.name || 'page'}:${JSON.stringify(props)}`;
104
+ await cache.set(cacheKey, staticShell, { ttl: shellCacheTTL, tags: ['ppr'] });
105
+
106
+ return {
107
+ staticShell,
108
+ dynamicParts,
109
+ fullHtml: staticShell
110
+ };
111
+ }
112
+
113
+ // Stream PPR response
114
+ export async function streamPPR(
115
+ staticShell: string,
116
+ dynamicParts: Map<string, () => Promise<string>>,
117
+ options?: { onError?: (error: Error) => string }
118
+ ): Promise<ReadableStream<Uint8Array>> {
119
+ const encoder = new TextEncoder();
120
+
121
+ return new ReadableStream({
122
+ async start(controller) {
123
+ // Send static shell immediately
124
+ controller.enqueue(encoder.encode(staticShell));
125
+
126
+ // Stream dynamic parts as they resolve
127
+ const promises = Array.from(dynamicParts.entries()).map(async ([id, render]) => {
128
+ try {
129
+ const html = await render();
130
+ // Send script to replace placeholder
131
+ const script = `<script>
132
+ (function() {
133
+ var placeholder = document.querySelector('[data-ppr-placeholder="${id}"]');
134
+ if (placeholder) {
135
+ var temp = document.createElement('div');
136
+ temp.innerHTML = ${JSON.stringify(html)};
137
+ placeholder.replaceWith(...temp.childNodes);
138
+ }
139
+ })();
140
+ </script>`;
141
+ controller.enqueue(encoder.encode(script));
142
+ } catch (error: any) {
143
+ const errorHtml = options?.onError?.(error) || `<div class="ppr-error">Error loading content</div>`;
144
+ const script = `<script>
145
+ (function() {
146
+ var placeholder = document.querySelector('[data-ppr-placeholder="${id}"]');
147
+ if (placeholder) {
148
+ placeholder.innerHTML = ${JSON.stringify(errorHtml)};
149
+ }
150
+ })();
151
+ </script>`;
152
+ controller.enqueue(encoder.encode(script));
153
+ }
154
+ });
155
+
156
+ await Promise.all(promises);
157
+ controller.close();
158
+ }
159
+ });
160
+ }
161
+
162
+ // PPR-aware fetch wrapper
163
+ export function pprFetch(
164
+ input: RequestInfo | URL,
165
+ init?: RequestInit & {
166
+ cache?: 'force-cache' | 'no-store' | 'no-cache';
167
+ next?: { revalidate?: number; tags?: string[] };
168
+ }
169
+ ): Promise<Response> {
170
+ const cacheMode = init?.cache || 'force-cache';
171
+ const revalidate = init?.next?.revalidate;
172
+ const tags = init?.next?.tags || [];
173
+
174
+ // If no-store, always fetch fresh
175
+ if (cacheMode === 'no-store') {
176
+ return fetch(input, init);
177
+ }
178
+
179
+ // Create cache key
180
+ const url = typeof input === 'string' ? input : input.toString();
181
+ const cacheKey = `fetch:${url}:${JSON.stringify(init?.body || '')}`;
182
+
183
+ // Try cache first
184
+ return cache.wrap(
185
+ async () => {
186
+ const response = await fetch(input, init);
187
+ return response;
188
+ },
189
+ {
190
+ key: cacheKey,
191
+ ttl: revalidate || 3600,
192
+ tags
193
+ }
194
+ )();
195
+ }
196
+
197
+ // Export directive markers
198
+ export const experimental_ppr = true;
199
+
200
+ // Page config for PPR
201
+ export interface PPRPageConfig {
202
+ experimental_ppr?: boolean;
203
+ revalidate?: number | false;
204
+ dynamic?: 'auto' | 'force-dynamic' | 'force-static' | 'error';
205
+ dynamicParams?: boolean;
206
+ fetchCache?: 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store';
207
+ }
208
+
209
+ // Generate static params (for SSG with PPR)
210
+ export type GenerateStaticParams<T = any> = () => Promise<T[]> | T[];
211
+
212
+ // Default PPR loading component
213
+ export function PPRLoading(): React.ReactElement {
214
+ return React.createElement('div', {
215
+ className: 'ppr-loading animate-pulse',
216
+ style: {
217
+ background: 'linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%)',
218
+ backgroundSize: '200% 100%',
219
+ animation: 'shimmer 1.5s infinite',
220
+ borderRadius: '4px',
221
+ height: '1em',
222
+ width: '100%'
223
+ }
224
+ });
225
+ }
226
+
227
+ // Inject PPR styles
228
+ export function getPPRStyles(): string {
229
+ return `
230
+ @keyframes shimmer {
231
+ 0% { background-position: 200% 0; }
232
+ 100% { background-position: -200% 0; }
233
+ }
234
+
235
+ .ppr-loading {
236
+ background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
237
+ background-size: 200% 100%;
238
+ animation: shimmer 1.5s infinite;
239
+ border-radius: 4px;
240
+ min-height: 1em;
241
+ }
242
+
243
+ .ppr-error {
244
+ color: #ef4444;
245
+ padding: 1rem;
246
+ border: 1px solid #ef4444;
247
+ border-radius: 4px;
248
+ background: rgba(239, 68, 68, 0.1);
249
+ }
250
+ `;
251
+ }
252
+
253
+ export default {
254
+ dynamic,
255
+ staticComponent,
256
+ PPRBoundary,
257
+ PPRShell,
258
+ prerenderWithPPR,
259
+ streamPPR,
260
+ pprFetch,
261
+ PPRLoading,
262
+ getPPRStyles,
263
+ experimental_ppr
264
+ };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * FlexiReact Universal Edge Runtime
3
+ *
4
+ * Works on:
5
+ * - Node.js
6
+ * - Bun
7
+ * - Deno
8
+ * - Cloudflare Workers
9
+ * - Vercel Edge
10
+ * - Any Web-standard runtime
11
+ */
12
+
13
+ // Detect runtime environment
14
+ export type RuntimeEnvironment =
15
+ | 'node'
16
+ | 'bun'
17
+ | 'deno'
18
+ | 'cloudflare'
19
+ | 'vercel-edge'
20
+ | 'netlify-edge'
21
+ | 'fastly'
22
+ | 'unknown';
23
+
24
+ export function detectRuntime(): RuntimeEnvironment {
25
+ // Bun
26
+ if (typeof globalThis.Bun !== 'undefined') {
27
+ return 'bun';
28
+ }
29
+
30
+ // Deno
31
+ if (typeof globalThis.Deno !== 'undefined') {
32
+ return 'deno';
33
+ }
34
+
35
+ // Cloudflare Workers
36
+ if (typeof globalThis.caches !== 'undefined' && typeof (globalThis as any).WebSocketPair !== 'undefined') {
37
+ return 'cloudflare';
38
+ }
39
+
40
+ // Vercel Edge
41
+ if (typeof process !== 'undefined' && process.env?.VERCEL_EDGE === '1') {
42
+ return 'vercel-edge';
43
+ }
44
+
45
+ // Netlify Edge
46
+ if (typeof globalThis.Netlify !== 'undefined') {
47
+ return 'netlify-edge';
48
+ }
49
+
50
+ // Node.js
51
+ if (typeof process !== 'undefined' && process.versions?.node) {
52
+ return 'node';
53
+ }
54
+
55
+ return 'unknown';
56
+ }
57
+
58
+ // Runtime capabilities
59
+ export interface RuntimeCapabilities {
60
+ hasFileSystem: boolean;
61
+ hasWebCrypto: boolean;
62
+ hasWebStreams: boolean;
63
+ hasFetch: boolean;
64
+ hasWebSocket: boolean;
65
+ hasKV: boolean;
66
+ hasCache: boolean;
67
+ maxExecutionTime: number; // ms, 0 = unlimited
68
+ maxMemory: number; // bytes, 0 = unlimited
69
+ }
70
+
71
+ export function getRuntimeCapabilities(): RuntimeCapabilities {
72
+ const runtime = detectRuntime();
73
+
74
+ switch (runtime) {
75
+ case 'cloudflare':
76
+ return {
77
+ hasFileSystem: false,
78
+ hasWebCrypto: true,
79
+ hasWebStreams: true,
80
+ hasFetch: true,
81
+ hasWebSocket: true,
82
+ hasKV: true,
83
+ hasCache: true,
84
+ maxExecutionTime: 30000, // 30s for paid, 10ms for free
85
+ maxMemory: 128 * 1024 * 1024 // 128MB
86
+ };
87
+
88
+ case 'vercel-edge':
89
+ return {
90
+ hasFileSystem: false,
91
+ hasWebCrypto: true,
92
+ hasWebStreams: true,
93
+ hasFetch: true,
94
+ hasWebSocket: false,
95
+ hasKV: true, // Vercel KV
96
+ hasCache: true,
97
+ maxExecutionTime: 30000,
98
+ maxMemory: 128 * 1024 * 1024
99
+ };
100
+
101
+ case 'deno':
102
+ return {
103
+ hasFileSystem: true,
104
+ hasWebCrypto: true,
105
+ hasWebStreams: true,
106
+ hasFetch: true,
107
+ hasWebSocket: true,
108
+ hasKV: true, // Deno KV
109
+ hasCache: true,
110
+ maxExecutionTime: 0,
111
+ maxMemory: 0
112
+ };
113
+
114
+ case 'bun':
115
+ return {
116
+ hasFileSystem: true,
117
+ hasWebCrypto: true,
118
+ hasWebStreams: true,
119
+ hasFetch: true,
120
+ hasWebSocket: true,
121
+ hasKV: false,
122
+ hasCache: false,
123
+ maxExecutionTime: 0,
124
+ maxMemory: 0
125
+ };
126
+
127
+ case 'node':
128
+ default:
129
+ return {
130
+ hasFileSystem: true,
131
+ hasWebCrypto: true,
132
+ hasWebStreams: true,
133
+ hasFetch: true,
134
+ hasWebSocket: true,
135
+ hasKV: false,
136
+ hasCache: false,
137
+ maxExecutionTime: 0,
138
+ maxMemory: 0
139
+ };
140
+ }
141
+ }
142
+
143
+ // Runtime info
144
+ export const runtime = {
145
+ name: detectRuntime(),
146
+ capabilities: getRuntimeCapabilities(),
147
+
148
+ get isEdge(): boolean {
149
+ return ['cloudflare', 'vercel-edge', 'netlify-edge', 'fastly'].includes(this.name);
150
+ },
151
+
152
+ get isServer(): boolean {
153
+ return ['node', 'bun', 'deno'].includes(this.name);
154
+ },
155
+
156
+ get supportsStreaming(): boolean {
157
+ return this.capabilities.hasWebStreams;
158
+ }
159
+ };
160
+
161
+ export default runtime;