@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.
- package/README.md +203 -15
- package/core/edge/cache.ts +344 -0
- package/core/edge/fetch-polyfill.ts +247 -0
- package/core/edge/handler.ts +248 -0
- package/core/edge/index.ts +81 -0
- package/core/edge/ppr.ts +264 -0
- package/core/edge/runtime.ts +161 -0
- package/core/font/index.ts +306 -0
- package/core/image/index.ts +413 -0
- package/core/index.ts +92 -1
- package/core/metadata/index.ts +622 -0
- package/core/server/index.ts +12 -0
- package/package.json +1 -1
package/core/edge/ppr.ts
ADDED
|
@@ -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;
|