@flexireact/core 2.3.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,344 @@
1
+ /**
2
+ * FlexiReact Universal Cache System
3
+ *
4
+ * Smart caching that works on:
5
+ * - Cloudflare Workers (Cache API + KV)
6
+ * - Vercel Edge (Edge Config + KV)
7
+ * - Deno (Deno KV)
8
+ * - Node.js/Bun (In-memory + File cache)
9
+ */
10
+
11
+ import { detectRuntime } from './runtime.js';
12
+
13
+ // Cache entry
14
+ export interface CacheEntry<T = any> {
15
+ value: T;
16
+ expires: number; // timestamp
17
+ stale?: number; // stale-while-revalidate timestamp
18
+ tags?: string[];
19
+ etag?: string;
20
+ }
21
+
22
+ // Cache options
23
+ export interface CacheOptions {
24
+ ttl?: number; // seconds
25
+ staleWhileRevalidate?: number; // seconds
26
+ tags?: string[];
27
+ key?: string;
28
+ revalidate?: number | false; // ISR-style revalidation
29
+ }
30
+
31
+ // Cache storage interface
32
+ export interface CacheStorage {
33
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
34
+ set<T>(key: string, entry: CacheEntry<T>): Promise<void>;
35
+ delete(key: string): Promise<void>;
36
+ deleteByTag(tag: string): Promise<void>;
37
+ clear(): Promise<void>;
38
+ }
39
+
40
+ // In-memory cache (fallback)
41
+ class MemoryCache implements CacheStorage {
42
+ private store = new Map<string, CacheEntry>();
43
+ private tagIndex = new Map<string, Set<string>>();
44
+
45
+ async get<T>(key: string): Promise<CacheEntry<T> | null> {
46
+ const entry = this.store.get(key);
47
+ if (!entry) return null;
48
+
49
+ // Check expiration
50
+ if (entry.expires && entry.expires < Date.now()) {
51
+ // Check stale-while-revalidate
52
+ if (entry.stale && entry.stale > Date.now()) {
53
+ return { ...entry, value: entry.value as T };
54
+ }
55
+ this.store.delete(key);
56
+ return null;
57
+ }
58
+
59
+ return { ...entry, value: entry.value as T };
60
+ }
61
+
62
+ async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
63
+ this.store.set(key, entry);
64
+
65
+ // Index by tags
66
+ if (entry.tags) {
67
+ entry.tags.forEach(tag => {
68
+ if (!this.tagIndex.has(tag)) {
69
+ this.tagIndex.set(tag, new Set());
70
+ }
71
+ this.tagIndex.get(tag)!.add(key);
72
+ });
73
+ }
74
+ }
75
+
76
+ async delete(key: string): Promise<void> {
77
+ this.store.delete(key);
78
+ }
79
+
80
+ async deleteByTag(tag: string): Promise<void> {
81
+ const keys = this.tagIndex.get(tag);
82
+ if (keys) {
83
+ keys.forEach(key => this.store.delete(key));
84
+ this.tagIndex.delete(tag);
85
+ }
86
+ }
87
+
88
+ async clear(): Promise<void> {
89
+ this.store.clear();
90
+ this.tagIndex.clear();
91
+ }
92
+ }
93
+
94
+ // Cloudflare KV cache
95
+ class CloudflareCache implements CacheStorage {
96
+ private kv: any;
97
+
98
+ constructor(kv: any) {
99
+ this.kv = kv;
100
+ }
101
+
102
+ async get<T>(key: string): Promise<CacheEntry<T> | null> {
103
+ try {
104
+ const data = await this.kv.get(key, 'json');
105
+ if (!data) return null;
106
+
107
+ const entry = data as CacheEntry<T>;
108
+ if (entry.expires && entry.expires < Date.now()) {
109
+ if (entry.stale && entry.stale > Date.now()) {
110
+ return entry;
111
+ }
112
+ await this.kv.delete(key);
113
+ return null;
114
+ }
115
+
116
+ return entry;
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
123
+ const ttl = entry.expires ? Math.ceil((entry.expires - Date.now()) / 1000) : undefined;
124
+ await this.kv.put(key, JSON.stringify(entry), { expirationTtl: ttl });
125
+ }
126
+
127
+ async delete(key: string): Promise<void> {
128
+ await this.kv.delete(key);
129
+ }
130
+
131
+ async deleteByTag(tag: string): Promise<void> {
132
+ // KV doesn't support tag-based deletion natively
133
+ // Would need to maintain a tag index
134
+ console.warn('Tag-based deletion not fully supported in Cloudflare KV');
135
+ }
136
+
137
+ async clear(): Promise<void> {
138
+ // KV doesn't support clear
139
+ console.warn('Clear not supported in Cloudflare KV');
140
+ }
141
+ }
142
+
143
+ // Deno KV cache
144
+ class DenoKVCache implements CacheStorage {
145
+ private kv: any;
146
+
147
+ constructor() {
148
+ // @ts-ignore - Deno global
149
+ this.kv = Deno.openKv();
150
+ }
151
+
152
+ async get<T>(key: string): Promise<CacheEntry<T> | null> {
153
+ try {
154
+ const kv = await this.kv;
155
+ const result = await kv.get(['cache', key]);
156
+ if (!result.value) return null;
157
+
158
+ const entry = result.value as CacheEntry<T>;
159
+ if (entry.expires && entry.expires < Date.now()) {
160
+ if (entry.stale && entry.stale > Date.now()) {
161
+ return entry;
162
+ }
163
+ await kv.delete(['cache', key]);
164
+ return null;
165
+ }
166
+
167
+ return entry;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
174
+ const kv = await this.kv;
175
+ await kv.set(['cache', key], entry);
176
+ }
177
+
178
+ async delete(key: string): Promise<void> {
179
+ const kv = await this.kv;
180
+ await kv.delete(['cache', key]);
181
+ }
182
+
183
+ async deleteByTag(tag: string): Promise<void> {
184
+ // Would need tag index
185
+ console.warn('Tag-based deletion requires tag index in Deno KV');
186
+ }
187
+
188
+ async clear(): Promise<void> {
189
+ // Would need to iterate all keys
190
+ console.warn('Clear requires iteration in Deno KV');
191
+ }
192
+ }
193
+
194
+ // Create cache based on runtime
195
+ function createCacheStorage(options?: { kv?: any }): CacheStorage {
196
+ const runtime = detectRuntime();
197
+
198
+ switch (runtime) {
199
+ case 'cloudflare':
200
+ if (options?.kv) {
201
+ return new CloudflareCache(options.kv);
202
+ }
203
+ return new MemoryCache();
204
+
205
+ case 'deno':
206
+ return new DenoKVCache();
207
+
208
+ case 'node':
209
+ case 'bun':
210
+ default:
211
+ return new MemoryCache();
212
+ }
213
+ }
214
+
215
+ // Main cache instance
216
+ let cacheStorage: CacheStorage = new MemoryCache();
217
+
218
+ // Initialize cache with platform-specific storage
219
+ export function initCache(options?: { kv?: any }): void {
220
+ cacheStorage = createCacheStorage(options);
221
+ }
222
+
223
+ // Cache function wrapper (like React cache)
224
+ export function cacheFunction<T extends (...args: any[]) => Promise<any>>(
225
+ fn: T,
226
+ options: CacheOptions = {}
227
+ ): T {
228
+ const { ttl = 60, staleWhileRevalidate = 0, tags = [] } = options;
229
+
230
+ return (async (...args: any[]) => {
231
+ const key = options.key || `fn:${fn.name}:${JSON.stringify(args)}`;
232
+
233
+ // Try cache first
234
+ const cached = await cacheStorage.get(key);
235
+ if (cached) {
236
+ // Check if stale and needs revalidation
237
+ if (cached.expires < Date.now() && cached.stale && cached.stale > Date.now()) {
238
+ // Return stale data, revalidate in background
239
+ queueMicrotask(async () => {
240
+ try {
241
+ const fresh = await fn(...args);
242
+ await cacheStorage.set(key, {
243
+ value: fresh,
244
+ expires: Date.now() + ttl * 1000,
245
+ stale: Date.now() + (ttl + staleWhileRevalidate) * 1000,
246
+ tags
247
+ });
248
+ } catch (e) {
249
+ console.error('Background revalidation failed:', e);
250
+ }
251
+ });
252
+ }
253
+ return cached.value;
254
+ }
255
+
256
+ // Execute function
257
+ const result = await fn(...args);
258
+
259
+ // Cache result
260
+ await cacheStorage.set(key, {
261
+ value: result,
262
+ expires: Date.now() + ttl * 1000,
263
+ stale: staleWhileRevalidate ? Date.now() + (ttl + staleWhileRevalidate) * 1000 : undefined,
264
+ tags
265
+ });
266
+
267
+ return result;
268
+ }) as T;
269
+ }
270
+
271
+ // Unstable cache (Next.js compatible API)
272
+ export function unstable_cache<T extends (...args: any[]) => Promise<any>>(
273
+ fn: T,
274
+ keyParts?: string[],
275
+ options?: { revalidate?: number | false; tags?: string[] }
276
+ ): T {
277
+ return cacheFunction(fn, {
278
+ key: keyParts?.join(':'),
279
+ ttl: typeof options?.revalidate === 'number' ? options.revalidate : 3600,
280
+ tags: options?.tags
281
+ });
282
+ }
283
+
284
+ // Revalidate by tag
285
+ export async function revalidateTag(tag: string): Promise<void> {
286
+ await cacheStorage.deleteByTag(tag);
287
+ }
288
+
289
+ // Revalidate by path
290
+ export async function revalidatePath(path: string): Promise<void> {
291
+ await cacheStorage.delete(`page:${path}`);
292
+ }
293
+
294
+ // Cache object for direct access
295
+ export const cache = {
296
+ get: <T>(key: string) => cacheStorage.get<T>(key),
297
+ set: <T>(key: string, value: T, options: CacheOptions = {}) => {
298
+ const { ttl = 60, staleWhileRevalidate = 0, tags = [] } = options;
299
+ return cacheStorage.set(key, {
300
+ value,
301
+ expires: Date.now() + ttl * 1000,
302
+ stale: staleWhileRevalidate ? Date.now() + (ttl + staleWhileRevalidate) * 1000 : undefined,
303
+ tags
304
+ });
305
+ },
306
+ delete: (key: string) => cacheStorage.delete(key),
307
+ deleteByTag: (tag: string) => cacheStorage.deleteByTag(tag),
308
+ clear: () => cacheStorage.clear(),
309
+
310
+ // Wrap function with caching
311
+ wrap: cacheFunction,
312
+
313
+ // Next.js compatible
314
+ unstable_cache,
315
+ revalidateTag,
316
+ revalidatePath
317
+ };
318
+
319
+ // Request-level cache (per-request deduplication)
320
+ const requestCache = new WeakMap<Request, Map<string, any>>();
321
+
322
+ export function getRequestCache(request: Request): Map<string, any> {
323
+ if (!requestCache.has(request)) {
324
+ requestCache.set(request, new Map());
325
+ }
326
+ return requestCache.get(request)!;
327
+ }
328
+
329
+ // React-style cache for request deduplication
330
+ export function reactCache<T extends (...args: any[]) => any>(fn: T): T {
331
+ const cache = new Map<string, any>();
332
+
333
+ return ((...args: any[]) => {
334
+ const key = JSON.stringify(args);
335
+ if (cache.has(key)) {
336
+ return cache.get(key);
337
+ }
338
+ const result = fn(...args);
339
+ cache.set(key, result);
340
+ return result;
341
+ }) as T;
342
+ }
343
+
344
+ export default cache;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Fetch API Polyfill for Universal Compatibility
3
+ *
4
+ * Ensures Request, Response, Headers work everywhere
5
+ */
6
+
7
+ // Use native Web APIs if available, otherwise polyfill
8
+ const globalFetch = globalThis.fetch;
9
+ const GlobalRequest = globalThis.Request;
10
+ const GlobalResponse = globalThis.Response;
11
+ const GlobalHeaders = globalThis.Headers;
12
+
13
+ // Extended Request with FlexiReact helpers
14
+ export class FlexiRequest extends GlobalRequest {
15
+ private _parsedUrl?: URL;
16
+ private _cookies?: Map<string, string>;
17
+
18
+ constructor(input: RequestInfo | URL, init?: RequestInit) {
19
+ super(input, init);
20
+ }
21
+
22
+ get pathname(): string {
23
+ if (!this._parsedUrl) {
24
+ this._parsedUrl = new URL(this.url);
25
+ }
26
+ return this._parsedUrl.pathname;
27
+ }
28
+
29
+ get searchParams(): URLSearchParams {
30
+ if (!this._parsedUrl) {
31
+ this._parsedUrl = new URL(this.url);
32
+ }
33
+ return this._parsedUrl.searchParams;
34
+ }
35
+
36
+ get cookies(): Map<string, string> {
37
+ if (!this._cookies) {
38
+ this._cookies = new Map();
39
+ const cookieHeader = this.headers.get('cookie');
40
+ if (cookieHeader) {
41
+ cookieHeader.split(';').forEach(cookie => {
42
+ const [name, ...rest] = cookie.trim().split('=');
43
+ if (name) {
44
+ this._cookies!.set(name, rest.join('='));
45
+ }
46
+ });
47
+ }
48
+ }
49
+ return this._cookies;
50
+ }
51
+
52
+ cookie(name: string): string | undefined {
53
+ return this.cookies.get(name);
54
+ }
55
+
56
+ // Parse JSON body
57
+ async jsonBody<T = any>(): Promise<T> {
58
+ return this.json();
59
+ }
60
+
61
+ // Parse form data
62
+ async formBody(): Promise<FormData> {
63
+ return this.formData();
64
+ }
65
+
66
+ // Get query param
67
+ query(name: string): string | null {
68
+ return this.searchParams.get(name);
69
+ }
70
+
71
+ // Get all query params
72
+ queryAll(name: string): string[] {
73
+ return this.searchParams.getAll(name);
74
+ }
75
+ }
76
+
77
+ // Extended Response with FlexiReact helpers
78
+ export class FlexiResponse extends GlobalResponse {
79
+ // Static helpers for common responses
80
+
81
+ static json(data: any, init?: ResponseInit): FlexiResponse {
82
+ const headers = new Headers(init?.headers);
83
+ headers.set('Content-Type', 'application/json');
84
+
85
+ return new FlexiResponse(JSON.stringify(data), {
86
+ ...init,
87
+ headers
88
+ });
89
+ }
90
+
91
+ static html(html: string, init?: ResponseInit): FlexiResponse {
92
+ const headers = new Headers(init?.headers);
93
+ headers.set('Content-Type', 'text/html; charset=utf-8');
94
+
95
+ return new FlexiResponse(html, {
96
+ ...init,
97
+ headers
98
+ });
99
+ }
100
+
101
+ static text(text: string, init?: ResponseInit): FlexiResponse {
102
+ const headers = new Headers(init?.headers);
103
+ headers.set('Content-Type', 'text/plain; charset=utf-8');
104
+
105
+ return new FlexiResponse(text, {
106
+ ...init,
107
+ headers
108
+ });
109
+ }
110
+
111
+ static redirect(url: string, status: 301 | 302 | 303 | 307 | 308 = 307): FlexiResponse {
112
+ return new FlexiResponse(null, {
113
+ status,
114
+ headers: { Location: url }
115
+ });
116
+ }
117
+
118
+ static notFound(message: string = 'Not Found'): FlexiResponse {
119
+ return new FlexiResponse(message, {
120
+ status: 404,
121
+ headers: { 'Content-Type': 'text/plain' }
122
+ });
123
+ }
124
+
125
+ static error(message: string = 'Internal Server Error', status: number = 500): FlexiResponse {
126
+ return new FlexiResponse(message, {
127
+ status,
128
+ headers: { 'Content-Type': 'text/plain' }
129
+ });
130
+ }
131
+
132
+ // Stream response
133
+ static stream(
134
+ stream: ReadableStream,
135
+ init?: ResponseInit
136
+ ): FlexiResponse {
137
+ return new FlexiResponse(stream, init);
138
+ }
139
+
140
+ // Set cookie helper
141
+ withCookie(
142
+ name: string,
143
+ value: string,
144
+ options: {
145
+ maxAge?: number;
146
+ expires?: Date;
147
+ path?: string;
148
+ domain?: string;
149
+ secure?: boolean;
150
+ httpOnly?: boolean;
151
+ sameSite?: 'Strict' | 'Lax' | 'None';
152
+ } = {}
153
+ ): FlexiResponse {
154
+ const parts = [`${name}=${encodeURIComponent(value)}`];
155
+
156
+ if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
157
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
158
+ if (options.path) parts.push(`Path=${options.path}`);
159
+ if (options.domain) parts.push(`Domain=${options.domain}`);
160
+ if (options.secure) parts.push('Secure');
161
+ if (options.httpOnly) parts.push('HttpOnly');
162
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
163
+
164
+ const headers = new Headers(this.headers);
165
+ headers.append('Set-Cookie', parts.join('; '));
166
+
167
+ return new FlexiResponse(this.body, {
168
+ status: this.status,
169
+ statusText: this.statusText,
170
+ headers
171
+ });
172
+ }
173
+
174
+ // Add headers helper
175
+ withHeaders(newHeaders: Record<string, string>): FlexiResponse {
176
+ const headers = new Headers(this.headers);
177
+ Object.entries(newHeaders).forEach(([key, value]) => {
178
+ headers.set(key, value);
179
+ });
180
+
181
+ return new FlexiResponse(this.body, {
182
+ status: this.status,
183
+ statusText: this.statusText,
184
+ headers
185
+ });
186
+ }
187
+ }
188
+
189
+ // Extended Headers
190
+ export class FlexiHeaders extends GlobalHeaders {
191
+ // Get bearer token
192
+ getBearerToken(): string | null {
193
+ const auth = this.get('Authorization');
194
+ if (auth?.startsWith('Bearer ')) {
195
+ return auth.slice(7);
196
+ }
197
+ return null;
198
+ }
199
+
200
+ // Get basic auth credentials
201
+ getBasicAuth(): { username: string; password: string } | null {
202
+ const auth = this.get('Authorization');
203
+ if (auth?.startsWith('Basic ')) {
204
+ try {
205
+ const decoded = atob(auth.slice(6));
206
+ const [username, password] = decoded.split(':');
207
+ return { username, password };
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ // Check content type
216
+ isJson(): boolean {
217
+ return this.get('Content-Type')?.includes('application/json') ?? false;
218
+ }
219
+
220
+ isFormData(): boolean {
221
+ const ct = this.get('Content-Type') ?? '';
222
+ return ct.includes('multipart/form-data') || ct.includes('application/x-www-form-urlencoded');
223
+ }
224
+
225
+ isHtml(): boolean {
226
+ return this.get('Accept')?.includes('text/html') ?? false;
227
+ }
228
+ }
229
+
230
+ // Export both native and extended versions
231
+ export {
232
+ FlexiRequest as Request,
233
+ FlexiResponse as Response,
234
+ FlexiHeaders as Headers
235
+ };
236
+
237
+ // Also export native versions for compatibility
238
+ export const NativeRequest = GlobalRequest;
239
+ export const NativeResponse = GlobalResponse;
240
+ export const NativeHeaders = GlobalHeaders;
241
+
242
+ export default {
243
+ Request: FlexiRequest,
244
+ Response: FlexiResponse,
245
+ Headers: FlexiHeaders,
246
+ fetch: globalFetch
247
+ };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * FlexiReact Universal Edge Handler
3
+ *
4
+ * Single handler that works on all platforms:
5
+ * - Node.js (http.createServer)
6
+ * - Bun (Bun.serve)
7
+ * - Deno (Deno.serve)
8
+ * - Cloudflare Workers (fetch handler)
9
+ * - Vercel Edge (edge function)
10
+ */
11
+
12
+ import { FlexiRequest, FlexiResponse } from './fetch-polyfill.js';
13
+ import runtime, { detectRuntime } from './runtime.js';
14
+ import { cache, CacheOptions } from './cache.js';
15
+
16
+ // Handler context
17
+ export interface EdgeContext {
18
+ runtime: typeof runtime;
19
+ cache: typeof cache;
20
+ env: Record<string, string | undefined>;
21
+ waitUntil: (promise: Promise<any>) => void;
22
+ passThroughOnException?: () => void;
23
+ }
24
+
25
+ // Route handler type
26
+ export type EdgeHandler = (
27
+ request: FlexiRequest,
28
+ context: EdgeContext
29
+ ) => Promise<FlexiResponse> | FlexiResponse;
30
+
31
+ // Middleware type
32
+ export type EdgeMiddleware = (
33
+ request: FlexiRequest,
34
+ context: EdgeContext,
35
+ next: () => Promise<FlexiResponse>
36
+ ) => Promise<FlexiResponse> | FlexiResponse;
37
+
38
+ // App configuration
39
+ export interface EdgeAppConfig {
40
+ routes?: Map<string, EdgeHandler>;
41
+ middleware?: EdgeMiddleware[];
42
+ notFound?: EdgeHandler;
43
+ onError?: (error: Error, request: FlexiRequest) => FlexiResponse;
44
+ basePath?: string;
45
+ }
46
+
47
+ // Create universal edge app
48
+ export function createEdgeApp(config: EdgeAppConfig = {}) {
49
+ const {
50
+ routes = new Map(),
51
+ middleware = [],
52
+ notFound = () => FlexiResponse.notFound(),
53
+ onError = (error) => FlexiResponse.error(error.message),
54
+ basePath = ''
55
+ } = config;
56
+
57
+ // Main fetch handler (Web Standard)
58
+ async function handleRequest(
59
+ request: Request,
60
+ env: Record<string, any> = {},
61
+ executionContext?: { waitUntil: (p: Promise<any>) => void; passThroughOnException?: () => void }
62
+ ): Promise<Response> {
63
+ const flexiRequest = new FlexiRequest(request.url, {
64
+ method: request.method,
65
+ headers: request.headers,
66
+ body: request.body,
67
+ // @ts-ignore - duplex is needed for streaming
68
+ duplex: 'half'
69
+ });
70
+
71
+ const context: EdgeContext = {
72
+ runtime,
73
+ cache,
74
+ env: env as Record<string, string | undefined>,
75
+ waitUntil: executionContext?.waitUntil || (() => {}),
76
+ passThroughOnException: executionContext?.passThroughOnException
77
+ };
78
+
79
+ try {
80
+ // Run middleware chain
81
+ const response = await runMiddleware(flexiRequest, context, middleware, async () => {
82
+ // Match route
83
+ const pathname = flexiRequest.pathname.replace(basePath, '') || '/';
84
+
85
+ // Try exact match first
86
+ let handler = routes.get(pathname);
87
+
88
+ // Try pattern matching
89
+ if (!handler) {
90
+ for (const [pattern, h] of routes) {
91
+ if (matchRoute(pathname, pattern)) {
92
+ handler = h;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+
98
+ if (handler) {
99
+ return await handler(flexiRequest, context);
100
+ }
101
+
102
+ return await notFound(flexiRequest, context);
103
+ });
104
+
105
+ return response;
106
+ } catch (error: any) {
107
+ console.error('Edge handler error:', error);
108
+ return onError(error, flexiRequest);
109
+ }
110
+ }
111
+
112
+ // Run middleware chain
113
+ async function runMiddleware(
114
+ request: FlexiRequest,
115
+ context: EdgeContext,
116
+ middlewares: EdgeMiddleware[],
117
+ finalHandler: () => Promise<FlexiResponse>
118
+ ): Promise<FlexiResponse> {
119
+ let index = 0;
120
+
121
+ async function next(): Promise<FlexiResponse> {
122
+ if (index >= middlewares.length) {
123
+ return finalHandler();
124
+ }
125
+ const mw = middlewares[index++];
126
+ return mw(request, context, next);
127
+ }
128
+
129
+ return next();
130
+ }
131
+
132
+ // Simple route matching
133
+ function matchRoute(pathname: string, pattern: string): boolean {
134
+ // Exact match
135
+ if (pathname === pattern) return true;
136
+
137
+ // Convert pattern to regex
138
+ const regexPattern = pattern
139
+ .replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)') // [...slug] -> catch-all
140
+ .replace(/\[(\w+)\]/g, '(?<$1>[^/]+)'); // [id] -> dynamic segment
141
+
142
+ const regex = new RegExp(`^${regexPattern}$`);
143
+ return regex.test(pathname);
144
+ }
145
+
146
+ // Return platform-specific exports
147
+ return {
148
+ // Web Standard fetch handler (Cloudflare, Vercel Edge, Deno)
149
+ fetch: handleRequest,
150
+
151
+ // Cloudflare Workers
152
+ async scheduled(event: any, env: any, ctx: any) {
153
+ // Handle scheduled events
154
+ },
155
+
156
+ // Add route
157
+ route(path: string, handler: EdgeHandler) {
158
+ routes.set(path, handler);
159
+ return this;
160
+ },
161
+
162
+ // Add middleware
163
+ use(mw: EdgeMiddleware) {
164
+ middleware.push(mw);
165
+ return this;
166
+ },
167
+
168
+ // Node.js adapter
169
+ toNodeHandler() {
170
+ return async (req: any, res: any) => {
171
+ const url = `http://${req.headers.host}${req.url}`;
172
+ const headers = new Headers();
173
+ Object.entries(req.headers).forEach(([key, value]) => {
174
+ if (typeof value === 'string') headers.set(key, value);
175
+ });
176
+
177
+ const body = ['GET', 'HEAD'].includes(req.method) ? undefined : req;
178
+
179
+ const request = new Request(url, {
180
+ method: req.method,
181
+ headers,
182
+ body,
183
+ // @ts-ignore
184
+ duplex: 'half'
185
+ });
186
+
187
+ const response = await handleRequest(request, process.env);
188
+
189
+ res.writeHead(response.status, Object.fromEntries(response.headers));
190
+
191
+ if (response.body) {
192
+ const reader = response.body.getReader();
193
+ while (true) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ res.write(value);
197
+ }
198
+ }
199
+ res.end();
200
+ };
201
+ },
202
+
203
+ // Bun adapter
204
+ toBunHandler() {
205
+ return {
206
+ fetch: handleRequest,
207
+ port: parseInt(process.env.PORT || '3000', 10)
208
+ };
209
+ },
210
+
211
+ // Deno adapter
212
+ toDenoHandler() {
213
+ return handleRequest;
214
+ },
215
+
216
+ // Start server based on runtime
217
+ async listen(port: number = 3000) {
218
+ const rt = detectRuntime();
219
+
220
+ switch (rt) {
221
+ case 'bun':
222
+ console.log(`🚀 FlexiReact Edge running on Bun at http://localhost:${port}`);
223
+ // @ts-ignore - Bun global
224
+ return Bun.serve({
225
+ port,
226
+ fetch: handleRequest
227
+ });
228
+
229
+ case 'deno':
230
+ console.log(`🚀 FlexiReact Edge running on Deno at http://localhost:${port}`);
231
+ // @ts-ignore - Deno global
232
+ return Deno.serve({ port }, handleRequest);
233
+
234
+ case 'node':
235
+ default:
236
+ const http = await import('http');
237
+ const server = http.createServer(this.toNodeHandler());
238
+ server.listen(port, () => {
239
+ console.log(`🚀 FlexiReact Edge running on Node.js at http://localhost:${port}`);
240
+ });
241
+ return server;
242
+ }
243
+ }
244
+ };
245
+ }
246
+
247
+ // Export default app creator
248
+ export default createEdgeApp;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * FlexiReact Edge Runtime
3
+ *
4
+ * Universal edge runtime that works everywhere:
5
+ * - Node.js
6
+ * - Bun
7
+ * - Deno
8
+ * - Cloudflare Workers
9
+ * - Vercel Edge
10
+ * - Netlify Edge
11
+ */
12
+
13
+ // Runtime detection and capabilities
14
+ export {
15
+ detectRuntime,
16
+ getRuntimeCapabilities,
17
+ runtime as edgeRuntimeInfo,
18
+ type RuntimeEnvironment,
19
+ type RuntimeCapabilities
20
+ } from './runtime.js';
21
+
22
+ // Fetch polyfill with helpers
23
+ export {
24
+ FlexiRequest,
25
+ FlexiResponse,
26
+ FlexiHeaders,
27
+ Request,
28
+ Response,
29
+ Headers,
30
+ NativeRequest,
31
+ NativeResponse,
32
+ NativeHeaders
33
+ } from './fetch-polyfill.js';
34
+
35
+ // Universal handler
36
+ export {
37
+ createEdgeApp,
38
+ type EdgeContext,
39
+ type EdgeHandler,
40
+ type EdgeMiddleware,
41
+ type EdgeAppConfig
42
+ } from './handler.js';
43
+
44
+ // Smart caching
45
+ export {
46
+ cache as smartCache,
47
+ initCache,
48
+ cacheFunction,
49
+ unstable_cache,
50
+ revalidateTag,
51
+ revalidatePath,
52
+ getRequestCache,
53
+ reactCache,
54
+ type CacheEntry,
55
+ type CacheOptions,
56
+ type CacheStorage
57
+ } from './cache.js';
58
+
59
+ // Partial Prerendering
60
+ export {
61
+ dynamic,
62
+ staticComponent,
63
+ PPRBoundary,
64
+ PPRShell,
65
+ prerenderWithPPR,
66
+ streamPPR,
67
+ pprFetch,
68
+ PPRLoading,
69
+ getPPRStyles,
70
+ experimental_ppr,
71
+ type PPRConfig,
72
+ type PPRRenderResult,
73
+ type PPRPageConfig,
74
+ type GenerateStaticParams
75
+ } from './ppr.js';
76
+
77
+ // Default export
78
+ export { default as edgeRuntime } from './runtime.js';
79
+ export { default as edgeCache } from './cache.js';
80
+ export { default as edgePPR } from './ppr.js';
81
+ export { default as createApp } from './handler.js';
@@ -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;
package/core/index.ts CHANGED
@@ -70,6 +70,54 @@ export {
70
70
  builtinPlugins
71
71
  } from './plugins/index.js';
72
72
 
73
+ // Edge Runtime
74
+ export {
75
+ // Runtime
76
+ detectRuntime,
77
+ getRuntimeCapabilities,
78
+ edgeRuntimeInfo,
79
+ // Fetch
80
+ FlexiRequest,
81
+ FlexiResponse,
82
+ FlexiHeaders,
83
+ // Handler
84
+ createEdgeApp,
85
+ // Cache
86
+ smartCache,
87
+ initCache,
88
+ cacheFunction,
89
+ unstable_cache,
90
+ revalidateTag,
91
+ revalidatePath,
92
+ reactCache,
93
+ // PPR
94
+ dynamic,
95
+ staticComponent,
96
+ PPRBoundary,
97
+ PPRShell,
98
+ prerenderWithPPR,
99
+ streamPPR,
100
+ pprFetch,
101
+ PPRLoading,
102
+ experimental_ppr,
103
+ // Default exports
104
+ createApp
105
+ } from './edge/index.js';
106
+ export type {
107
+ RuntimeEnvironment,
108
+ RuntimeCapabilities,
109
+ EdgeContext,
110
+ EdgeHandler,
111
+ EdgeMiddleware,
112
+ EdgeAppConfig,
113
+ CacheEntry,
114
+ CacheOptions,
115
+ PPRConfig,
116
+ PPRRenderResult,
117
+ PPRPageConfig,
118
+ GenerateStaticParams
119
+ } from './edge/index.js';
120
+
73
121
  // Font Optimization
74
122
  export {
75
123
  createFont,
@@ -153,7 +201,7 @@ export {
153
201
  export type { CookieOptions } from './helpers.js';
154
202
 
155
203
  // Version
156
- export const VERSION = '2.3.0';
204
+ export const VERSION = '2.4.0';
157
205
 
158
206
  // Default export
159
207
  export default {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexireact/core",
3
- "version": "2.3.0",
3
+ "version": "2.4.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",