@flightdev/ui 2.0.0 → 4.0.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 +285 -70
- package/dist/{chunk-XTDK7ME5.js → chunk-S4DTUQII.js} +246 -19
- package/dist/chunk-S4DTUQII.js.map +1 -0
- package/dist/core/index.d.ts +423 -3
- package/dist/core/index.js +23 -2
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +29 -5
- package/dist/index.js.map +1 -0
- package/package.json +11 -181
- package/.turbo/turbo-build.log +0 -81
- package/.turbo/turbo-lint.log +0 -40
- package/.turbo/turbo-typecheck.log +0 -4
- package/TESTING.md +0 -124
- package/dist/adapter-MMD-iHNx.d.ts +0 -424
- package/dist/adapters/tier-1/angular.d.ts +0 -60
- package/dist/adapters/tier-1/angular.js +0 -2
- package/dist/adapters/tier-1/index.d.ts +0 -7
- package/dist/adapters/tier-1/index.js +0 -7
- package/dist/adapters/tier-1/qwik.d.ts +0 -55
- package/dist/adapters/tier-1/qwik.js +0 -2
- package/dist/adapters/tier-1/react.d.ts +0 -67
- package/dist/adapters/tier-1/react.js +0 -2
- package/dist/adapters/tier-1/solid.d.ts +0 -45
- package/dist/adapters/tier-1/solid.js +0 -2
- package/dist/adapters/tier-1/svelte.d.ts +0 -48
- package/dist/adapters/tier-1/svelte.js +0 -2
- package/dist/adapters/tier-1/vue.d.ts +0 -47
- package/dist/adapters/tier-1/vue.js +0 -2
- package/dist/adapters/tier-2/index.d.ts +0 -7
- package/dist/adapters/tier-2/index.js +0 -7
- package/dist/adapters/tier-2/inferno.d.ts +0 -31
- package/dist/adapters/tier-2/inferno.js +0 -2
- package/dist/adapters/tier-2/lit.d.ts +0 -34
- package/dist/adapters/tier-2/lit.js +0 -2
- package/dist/adapters/tier-2/marko.d.ts +0 -59
- package/dist/adapters/tier-2/marko.js +0 -2
- package/dist/adapters/tier-2/mithril.d.ts +0 -31
- package/dist/adapters/tier-2/mithril.js +0 -2
- package/dist/adapters/tier-2/preact.d.ts +0 -33
- package/dist/adapters/tier-2/preact.js +0 -2
- package/dist/adapters/tier-2/stencil.d.ts +0 -52
- package/dist/adapters/tier-2/stencil.js +0 -2
- package/dist/adapters/tier-3/alpine.d.ts +0 -73
- package/dist/adapters/tier-3/alpine.js +0 -2
- package/dist/adapters/tier-3/hotwire.d.ts +0 -71
- package/dist/adapters/tier-3/hotwire.js +0 -2
- package/dist/adapters/tier-3/htmx.d.ts +0 -88
- package/dist/adapters/tier-3/htmx.js +0 -2
- package/dist/adapters/tier-3/index.d.ts +0 -7
- package/dist/adapters/tier-3/index.js +0 -7
- package/dist/adapters/tier-3/petite-vue.d.ts +0 -56
- package/dist/adapters/tier-3/petite-vue.js +0 -2
- package/dist/adapters/tier-3/stimulus.d.ts +0 -63
- package/dist/adapters/tier-3/stimulus.js +0 -2
- package/dist/adapters/tier-3/vanilla.d.ts +0 -63
- package/dist/adapters/tier-3/vanilla.js +0 -2
- package/dist/chunk-2SNQ6PTM.js +0 -217
- package/dist/chunk-3D4XMIZI.js +0 -136
- package/dist/chunk-3HU6GSQ4.js +0 -125
- package/dist/chunk-4PZDNFL7.js +0 -148
- package/dist/chunk-5IBLFTYL.js +0 -114
- package/dist/chunk-64JZJ7OK.js +0 -142
- package/dist/chunk-7ZJI3QU2.js +0 -132
- package/dist/chunk-CE4FJHQJ.js +0 -133
- package/dist/chunk-DTCAUBH5.js +0 -87
- package/dist/chunk-NTASPOHG.js +0 -106
- package/dist/chunk-OI2AMQLG.js +0 -152
- package/dist/chunk-Q7HUE44H.js +0 -106
- package/dist/chunk-QH3LOWXU.js +0 -155
- package/dist/chunk-QIVAK6BH.js +0 -103
- package/dist/chunk-V34XPVGK.js +0 -103
- package/dist/chunk-VK7ZPMO7.js +0 -221
- package/dist/chunk-X6CNUW6T.js +0 -136
- package/dist/chunk-YFGSHW5S.js +0 -121
- package/dist/chunk-ZAJVSE7J.js +0 -90
- package/docs/ADAPTERS.md +0 -946
- package/docs/PATTERNS.md +0 -836
- package/src/adapters/tier-1/angular.ts +0 -223
- package/src/adapters/tier-1/index.ts +0 -12
- package/src/adapters/tier-1/qwik.ts +0 -177
- package/src/adapters/tier-1/react.ts +0 -330
- package/src/adapters/tier-1/solid.ts +0 -222
- package/src/adapters/tier-1/svelte.ts +0 -211
- package/src/adapters/tier-1/vue.ts +0 -234
- package/src/adapters/tier-2/index.ts +0 -12
- package/src/adapters/tier-2/inferno.ts +0 -149
- package/src/adapters/tier-2/lit.ts +0 -191
- package/src/adapters/tier-2/marko.ts +0 -199
- package/src/adapters/tier-2/mithril.ts +0 -152
- package/src/adapters/tier-2/preact.ts +0 -133
- package/src/adapters/tier-2/stencil.ts +0 -214
- package/src/adapters/tier-3/alpine.ts +0 -218
- package/src/adapters/tier-3/hotwire.ts +0 -254
- package/src/adapters/tier-3/htmx.ts +0 -263
- package/src/adapters/tier-3/index.ts +0 -12
- package/src/adapters/tier-3/petite-vue.ts +0 -163
- package/src/adapters/tier-3/stimulus.ts +0 -233
- package/src/adapters/tier-3/vanilla.ts +0 -252
- package/src/ambient.d.ts +0 -310
- package/src/core/adapter.ts +0 -366
- package/src/core/index.ts +0 -56
- package/src/core/registry.ts +0 -518
- package/src/core/types.ts +0 -461
- package/src/htmx.ts +0 -134
- package/src/index.ts +0 -263
- package/test/__mocks__/stencil-core.ts +0 -19
- package/test/__mocks__/stencil-hydrate.ts +0 -15
- package/test/adapters/tier-1.test.ts +0 -206
- package/test/adapters/tier-2.test.ts +0 -175
- package/test/adapters/tier-3.test.ts +0 -284
- package/test/contracts/adapter.contract.ts +0 -293
- package/test/core/core.test.ts +0 -310
- package/test/errors/error-handling.test.ts +0 -454
- package/test/integration/htmx.integration.test.ts +0 -246
- package/test/integration/react.integration.test.ts +0 -271
- package/test/integration/registry.integration.test.ts +0 -308
- package/tsconfig.json +0 -22
- package/tsup.config.ts +0 -93
- package/vitest.config.ts +0 -101
package/docs/PATTERNS.md
DELETED
|
@@ -1,836 +0,0 @@
|
|
|
1
|
-
# Enterprise Patterns and Best Practices
|
|
2
|
-
|
|
3
|
-
This document covers production-grade patterns for using the Flight UI adapter system in enterprise applications.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
1. [Architecture Patterns](#architecture-patterns)
|
|
8
|
-
2. [Error Handling](#error-handling)
|
|
9
|
-
3. [Performance Optimization](#performance-optimization)
|
|
10
|
-
4. [Caching Strategies](#caching-strategies)
|
|
11
|
-
5. [Monitoring and Observability](#monitoring-and-observability)
|
|
12
|
-
6. [Security Considerations](#security-considerations)
|
|
13
|
-
7. [Testing Strategies](#testing-strategies)
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Architecture Patterns
|
|
18
|
-
|
|
19
|
-
### Micro-Frontend Architecture
|
|
20
|
-
|
|
21
|
-
Use different adapters for different sections of your application.
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
// flight.config.ts
|
|
25
|
-
import { defineConfig } from '@flight-framework/core';
|
|
26
|
-
|
|
27
|
-
export default defineConfig({
|
|
28
|
-
ui: {
|
|
29
|
-
// Default adapter for main app
|
|
30
|
-
adapter: 'react',
|
|
31
|
-
|
|
32
|
-
// Route-specific adapters
|
|
33
|
-
routes: {
|
|
34
|
-
'/admin/**': 'vue', // Admin uses Vue
|
|
35
|
-
'/docs/**': 'htmx', // Docs are static
|
|
36
|
-
'/dashboard': 'react', // Dashboard needs interactivity
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Implementation**:
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { adapterRegistry, registerBuiltinAdapters } from '@flight-framework/ui';
|
|
46
|
-
|
|
47
|
-
registerBuiltinAdapters();
|
|
48
|
-
|
|
49
|
-
class RouterWithAdapters {
|
|
50
|
-
private adapters = new Map();
|
|
51
|
-
|
|
52
|
-
async getAdapter(path: string): Promise<UIAdapterV2> {
|
|
53
|
-
const adapterName = this.resolveAdapter(path);
|
|
54
|
-
|
|
55
|
-
if (!this.adapters.has(adapterName)) {
|
|
56
|
-
const adapter = await adapterRegistry.get(adapterName);
|
|
57
|
-
this.adapters.set(adapterName, adapter);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return this.adapters.get(adapterName);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private resolveAdapter(path: string): string {
|
|
64
|
-
if (path.startsWith('/admin')) return 'vue';
|
|
65
|
-
if (path.startsWith('/docs')) return 'htmx';
|
|
66
|
-
return 'react';
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Backend for Frontend (BFF) Pattern
|
|
72
|
-
|
|
73
|
-
Each client type gets optimized responses.
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
import { react } from '@flight-framework/ui/react';
|
|
77
|
-
import { htmx } from '@flight-framework/ui/htmx';
|
|
78
|
-
|
|
79
|
-
async function handleRequest(request: Request) {
|
|
80
|
-
const userAgent = request.headers.get('User-Agent') || '';
|
|
81
|
-
const acceptsJS = !request.headers.get('X-No-JS');
|
|
82
|
-
|
|
83
|
-
// Low-bandwidth or no-JS clients get HTMX
|
|
84
|
-
if (!acceptsJS || isLowBandwidth(request)) {
|
|
85
|
-
const adapter = htmx();
|
|
86
|
-
return renderWithAdapter(adapter, request);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Modern clients get React with streaming
|
|
90
|
-
const adapter = react({ streaming: true });
|
|
91
|
-
return renderWithAdapter(adapter, request);
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Islands Architecture for Content Sites
|
|
96
|
-
|
|
97
|
-
Minimize JavaScript while maintaining interactivity.
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
import { react } from '@flight-framework/ui/react';
|
|
101
|
-
|
|
102
|
-
const adapter = react();
|
|
103
|
-
|
|
104
|
-
// Define interactive islands
|
|
105
|
-
const islands = {
|
|
106
|
-
'search': {
|
|
107
|
-
component: SearchWidget,
|
|
108
|
-
hydrate: 'idle',
|
|
109
|
-
priority: 100,
|
|
110
|
-
},
|
|
111
|
-
'comments': {
|
|
112
|
-
component: CommentsSection,
|
|
113
|
-
hydrate: 'visible',
|
|
114
|
-
priority: 50,
|
|
115
|
-
},
|
|
116
|
-
'newsletter': {
|
|
117
|
-
component: NewsletterForm,
|
|
118
|
-
hydrate: 'interaction',
|
|
119
|
-
priority: 10,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Render page with islands
|
|
124
|
-
function renderArticle(article) {
|
|
125
|
-
const islandPlaceholders = {};
|
|
126
|
-
|
|
127
|
-
for (const [name, config] of Object.entries(islands)) {
|
|
128
|
-
islandPlaceholders[name] = adapter.createIsland(
|
|
129
|
-
config.component,
|
|
130
|
-
{ articleId: article.id },
|
|
131
|
-
{ hydrate: config.hydrate, priority: config.priority }
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return `
|
|
136
|
-
<article>
|
|
137
|
-
<header>
|
|
138
|
-
<h1>${article.title}</h1>
|
|
139
|
-
${islandPlaceholders.search.placeholder}
|
|
140
|
-
</header>
|
|
141
|
-
|
|
142
|
-
<main>
|
|
143
|
-
${article.content}
|
|
144
|
-
</main>
|
|
145
|
-
|
|
146
|
-
<section id="comments">
|
|
147
|
-
${islandPlaceholders.comments.placeholder}
|
|
148
|
-
</section>
|
|
149
|
-
|
|
150
|
-
<aside>
|
|
151
|
-
${islandPlaceholders.newsletter.placeholder}
|
|
152
|
-
</aside>
|
|
153
|
-
</article>
|
|
154
|
-
`;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## Error Handling
|
|
161
|
-
|
|
162
|
-
### Streaming Error Boundaries
|
|
163
|
-
|
|
164
|
-
Handle errors at different levels during streaming SSR.
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { react } from '@flight-framework/ui/react';
|
|
168
|
-
|
|
169
|
-
const adapter = react();
|
|
170
|
-
|
|
171
|
-
interface RenderOptions {
|
|
172
|
-
fallbackHtml: string;
|
|
173
|
-
onError: (error: Error, context: string) => void;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function streamWithErrorHandling(
|
|
177
|
-
component: Component,
|
|
178
|
-
response: Response,
|
|
179
|
-
options: RenderOptions
|
|
180
|
-
) {
|
|
181
|
-
const { stream, done, abort } = adapter.renderToStream(
|
|
182
|
-
component,
|
|
183
|
-
{ url: request.url },
|
|
184
|
-
{
|
|
185
|
-
onShellError: (error) => {
|
|
186
|
-
// Critical error - send error page
|
|
187
|
-
options.onError(error, 'shell');
|
|
188
|
-
abort();
|
|
189
|
-
|
|
190
|
-
response.writeHead(500, { 'Content-Type': 'text/html' });
|
|
191
|
-
response.end(options.fallbackHtml);
|
|
192
|
-
},
|
|
193
|
-
onError: (error) => {
|
|
194
|
-
// Non-critical error - log and continue
|
|
195
|
-
options.onError(error, 'content');
|
|
196
|
-
// Error boundary will show fallback UI
|
|
197
|
-
},
|
|
198
|
-
onShellReady: () => {
|
|
199
|
-
// Shell rendered successfully
|
|
200
|
-
response.writeHead(200, {
|
|
201
|
-
'Content-Type': 'text/html',
|
|
202
|
-
'Transfer-Encoding': 'chunked',
|
|
203
|
-
});
|
|
204
|
-
},
|
|
205
|
-
}
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const reader = stream.getReader();
|
|
210
|
-
|
|
211
|
-
while (true) {
|
|
212
|
-
const { done: readerDone, value } = await reader.read();
|
|
213
|
-
if (readerDone) break;
|
|
214
|
-
response.write(value);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
response.end();
|
|
218
|
-
} catch (error) {
|
|
219
|
-
options.onError(error as Error, 'stream');
|
|
220
|
-
response.end(options.fallbackHtml);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Graceful Degradation
|
|
226
|
-
|
|
227
|
-
Fall back to simpler rendering when advanced features fail.
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
async function renderWithFallback(component: Component) {
|
|
231
|
-
const reactAdapter = react({ streaming: true });
|
|
232
|
-
const htmxAdapter = htmx();
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
// Try streaming SSR
|
|
236
|
-
return await streamRender(reactAdapter, component);
|
|
237
|
-
} catch (streamError) {
|
|
238
|
-
console.warn('Streaming failed, trying string render');
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
// Fall back to string render
|
|
242
|
-
return await reactAdapter.renderToString(component);
|
|
243
|
-
} catch (renderError) {
|
|
244
|
-
console.warn('React render failed, falling back to HTMX');
|
|
245
|
-
|
|
246
|
-
// Fall back to HTMX static render
|
|
247
|
-
return await htmxAdapter.renderToString({
|
|
248
|
-
component: () => renderStaticFallback(component),
|
|
249
|
-
props: {},
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Timeout Handling
|
|
257
|
-
|
|
258
|
-
Prevent hanging requests with proper timeouts.
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
function renderWithTimeout(
|
|
262
|
-
adapter: UIAdapterV2,
|
|
263
|
-
component: Component,
|
|
264
|
-
timeoutMs: number
|
|
265
|
-
): Promise<RenderResult> {
|
|
266
|
-
return new Promise((resolve, reject) => {
|
|
267
|
-
const timer = setTimeout(() => {
|
|
268
|
-
reject(new Error(`Render timeout after ${timeoutMs}ms`));
|
|
269
|
-
}, timeoutMs);
|
|
270
|
-
|
|
271
|
-
adapter.renderToString(component)
|
|
272
|
-
.then((result) => {
|
|
273
|
-
clearTimeout(timer);
|
|
274
|
-
resolve(result);
|
|
275
|
-
})
|
|
276
|
-
.catch((error) => {
|
|
277
|
-
clearTimeout(timer);
|
|
278
|
-
reject(error);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Usage
|
|
284
|
-
try {
|
|
285
|
-
const result = await renderWithTimeout(adapter, component, 5000);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
if (error.message.includes('timeout')) {
|
|
288
|
-
return renderCachedFallback();
|
|
289
|
-
}
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## Performance Optimization
|
|
297
|
-
|
|
298
|
-
### Lazy Adapter Loading
|
|
299
|
-
|
|
300
|
-
Load adapters only when needed.
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
const adapterLoaders = {
|
|
304
|
-
react: () => import('@flight-framework/ui/react'),
|
|
305
|
-
vue: () => import('@flight-framework/ui/vue'),
|
|
306
|
-
htmx: () => import('@flight-framework/ui/htmx'),
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
class LazyAdapterRegistry {
|
|
310
|
-
private cache = new Map<string, UIAdapterV2>();
|
|
311
|
-
|
|
312
|
-
async get(name: string): Promise<UIAdapterV2> {
|
|
313
|
-
if (this.cache.has(name)) {
|
|
314
|
-
return this.cache.get(name)!;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const loader = adapterLoaders[name];
|
|
318
|
-
if (!loader) {
|
|
319
|
-
throw new Error(`Unknown adapter: ${name}`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const module = await loader();
|
|
323
|
-
const factory = module.default || module[name];
|
|
324
|
-
const adapter = factory();
|
|
325
|
-
|
|
326
|
-
this.cache.set(name, adapter);
|
|
327
|
-
return adapter;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Component Memoization
|
|
333
|
-
|
|
334
|
-
Cache component render results.
|
|
335
|
-
|
|
336
|
-
```typescript
|
|
337
|
-
class MemoizedRenderer {
|
|
338
|
-
private cache = new Map<string, { result: RenderResult; expiry: number }>();
|
|
339
|
-
|
|
340
|
-
constructor(
|
|
341
|
-
private adapter: UIAdapterV2,
|
|
342
|
-
private defaultTtl: number = 60000
|
|
343
|
-
) {}
|
|
344
|
-
|
|
345
|
-
async render(
|
|
346
|
-
component: Component,
|
|
347
|
-
cacheKey: string,
|
|
348
|
-
ttl?: number
|
|
349
|
-
): Promise<RenderResult> {
|
|
350
|
-
const cached = this.cache.get(cacheKey);
|
|
351
|
-
const now = Date.now();
|
|
352
|
-
|
|
353
|
-
if (cached && cached.expiry > now) {
|
|
354
|
-
return cached.result;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const result = await this.adapter.renderToString(component);
|
|
358
|
-
|
|
359
|
-
this.cache.set(cacheKey, {
|
|
360
|
-
result,
|
|
361
|
-
expiry: now + (ttl ?? this.defaultTtl),
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
return result;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
invalidate(pattern?: string | RegExp) {
|
|
368
|
-
if (!pattern) {
|
|
369
|
-
this.cache.clear();
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for (const key of this.cache.keys()) {
|
|
374
|
-
if (typeof pattern === 'string' ? key.includes(pattern) : pattern.test(key)) {
|
|
375
|
-
this.cache.delete(key);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### Parallel Rendering
|
|
383
|
-
|
|
384
|
-
Render multiple components simultaneously.
|
|
385
|
-
|
|
386
|
-
```typescript
|
|
387
|
-
async function renderPageParallel(sections: Record<string, Component>) {
|
|
388
|
-
const adapter = react();
|
|
389
|
-
|
|
390
|
-
const renderTasks = Object.entries(sections).map(
|
|
391
|
-
async ([name, component]) => {
|
|
392
|
-
const result = await adapter.renderToString(component);
|
|
393
|
-
return [name, result] as const;
|
|
394
|
-
}
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const results = await Promise.all(renderTasks);
|
|
398
|
-
|
|
399
|
-
return Object.fromEntries(results);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Usage
|
|
403
|
-
const { header, main, sidebar, footer } = await renderPageParallel({
|
|
404
|
-
header: { component: Header, props: { user } },
|
|
405
|
-
main: { component: MainContent, props: { data } },
|
|
406
|
-
sidebar: { component: Sidebar, props: { items } },
|
|
407
|
-
footer: { component: Footer, props: {} },
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const html = `
|
|
411
|
-
<!DOCTYPE html>
|
|
412
|
-
<html>
|
|
413
|
-
<body>
|
|
414
|
-
${header.html}
|
|
415
|
-
<div class="layout">
|
|
416
|
-
${main.html}
|
|
417
|
-
${sidebar.html}
|
|
418
|
-
</div>
|
|
419
|
-
${footer.html}
|
|
420
|
-
</body>
|
|
421
|
-
</html>
|
|
422
|
-
`;
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
## Caching Strategies
|
|
428
|
-
|
|
429
|
-
### Multi-Level Cache
|
|
430
|
-
|
|
431
|
-
Implement tiered caching for optimal performance.
|
|
432
|
-
|
|
433
|
-
```typescript
|
|
434
|
-
interface CacheLevel {
|
|
435
|
-
get(key: string): Promise<RenderResult | null>;
|
|
436
|
-
set(key: string, value: RenderResult, ttl: number): Promise<void>;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
class MultiLevelCache {
|
|
440
|
-
constructor(private levels: CacheLevel[]) {}
|
|
441
|
-
|
|
442
|
-
async get(key: string): Promise<RenderResult | null> {
|
|
443
|
-
for (let i = 0; i < this.levels.length; i++) {
|
|
444
|
-
const result = await this.levels[i].get(key);
|
|
445
|
-
|
|
446
|
-
if (result) {
|
|
447
|
-
// Populate higher levels
|
|
448
|
-
for (let j = 0; j < i; j++) {
|
|
449
|
-
await this.levels[j].set(key, result, 60000);
|
|
450
|
-
}
|
|
451
|
-
return result;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async set(key: string, value: RenderResult, ttl: number): Promise<void> {
|
|
459
|
-
await Promise.all(
|
|
460
|
-
this.levels.map(level => level.set(key, value, ttl))
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Usage: Memory -> Redis -> CDN
|
|
466
|
-
const cache = new MultiLevelCache([
|
|
467
|
-
new MemoryCache(),
|
|
468
|
-
new RedisCache(redisClient),
|
|
469
|
-
new CDNCache(cdnClient),
|
|
470
|
-
]);
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
### Stale-While-Revalidate
|
|
474
|
-
|
|
475
|
-
Serve stale content while refreshing in background.
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
class SWRCache {
|
|
479
|
-
private cache = new Map<string, { result: RenderResult; staleAt: number; expireAt: number }>();
|
|
480
|
-
|
|
481
|
-
async getOrRevalidate(
|
|
482
|
-
key: string,
|
|
483
|
-
fetcher: () => Promise<RenderResult>,
|
|
484
|
-
freshTtl: number,
|
|
485
|
-
staleTtl: number
|
|
486
|
-
): Promise<RenderResult> {
|
|
487
|
-
const now = Date.now();
|
|
488
|
-
const cached = this.cache.get(key);
|
|
489
|
-
|
|
490
|
-
if (cached) {
|
|
491
|
-
if (now < cached.staleAt) {
|
|
492
|
-
// Fresh - return immediately
|
|
493
|
-
return cached.result;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (now < cached.expireAt) {
|
|
497
|
-
// Stale - return and revalidate in background
|
|
498
|
-
this.revalidate(key, fetcher, freshTtl, staleTtl);
|
|
499
|
-
return cached.result;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Expired or missing - fetch synchronously
|
|
504
|
-
const result = await fetcher();
|
|
505
|
-
this.cache.set(key, {
|
|
506
|
-
result,
|
|
507
|
-
staleAt: now + freshTtl,
|
|
508
|
-
expireAt: now + staleTtl,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
return result;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private async revalidate(
|
|
515
|
-
key: string,
|
|
516
|
-
fetcher: () => Promise<RenderResult>,
|
|
517
|
-
freshTtl: number,
|
|
518
|
-
staleTtl: number
|
|
519
|
-
): Promise<void> {
|
|
520
|
-
try {
|
|
521
|
-
const result = await fetcher();
|
|
522
|
-
const now = Date.now();
|
|
523
|
-
this.cache.set(key, {
|
|
524
|
-
result,
|
|
525
|
-
staleAt: now + freshTtl,
|
|
526
|
-
expireAt: now + staleTtl,
|
|
527
|
-
});
|
|
528
|
-
} catch (error) {
|
|
529
|
-
console.error(`Revalidation failed for ${key}:`, error);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## Monitoring and Observability
|
|
538
|
-
|
|
539
|
-
### Render Metrics
|
|
540
|
-
|
|
541
|
-
Track SSR performance metrics.
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
interface RenderMetrics {
|
|
545
|
-
adapter: string;
|
|
546
|
-
path: string;
|
|
547
|
-
duration: number;
|
|
548
|
-
success: boolean;
|
|
549
|
-
streaming: boolean;
|
|
550
|
-
ttfb?: number;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
class MetricsCollector {
|
|
554
|
-
private metrics: RenderMetrics[] = [];
|
|
555
|
-
|
|
556
|
-
async trackRender<T>(
|
|
557
|
-
adapter: UIAdapterV2,
|
|
558
|
-
path: string,
|
|
559
|
-
operation: () => Promise<T>
|
|
560
|
-
): Promise<T> {
|
|
561
|
-
const start = performance.now();
|
|
562
|
-
let success = true;
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
return await operation();
|
|
566
|
-
} catch (error) {
|
|
567
|
-
success = false;
|
|
568
|
-
throw error;
|
|
569
|
-
} finally {
|
|
570
|
-
const duration = performance.now() - start;
|
|
571
|
-
|
|
572
|
-
this.metrics.push({
|
|
573
|
-
adapter: adapter.id,
|
|
574
|
-
path,
|
|
575
|
-
duration,
|
|
576
|
-
success,
|
|
577
|
-
streaming: adapter.capabilities.streaming,
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
this.report();
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private report() {
|
|
585
|
-
if (this.metrics.length >= 100) {
|
|
586
|
-
// Send batch to monitoring service
|
|
587
|
-
sendMetrics(this.metrics);
|
|
588
|
-
this.metrics = [];
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
### Structured Logging
|
|
595
|
-
|
|
596
|
-
Log render operations with context.
|
|
597
|
-
|
|
598
|
-
```typescript
|
|
599
|
-
interface RenderLogEntry {
|
|
600
|
-
timestamp: string;
|
|
601
|
-
requestId: string;
|
|
602
|
-
adapter: string;
|
|
603
|
-
path: string;
|
|
604
|
-
component: string;
|
|
605
|
-
duration: number;
|
|
606
|
-
cacheHit: boolean;
|
|
607
|
-
error?: string;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function createRenderLogger(requestId: string) {
|
|
611
|
-
return {
|
|
612
|
-
logRender(entry: Omit<RenderLogEntry, 'timestamp' | 'requestId'>) {
|
|
613
|
-
const log: RenderLogEntry = {
|
|
614
|
-
timestamp: new Date().toISOString(),
|
|
615
|
-
requestId,
|
|
616
|
-
...entry,
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
console.log(JSON.stringify(log));
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Usage
|
|
625
|
-
const logger = createRenderLogger(request.id);
|
|
626
|
-
|
|
627
|
-
const result = await adapter.renderToString(component);
|
|
628
|
-
|
|
629
|
-
logger.logRender({
|
|
630
|
-
adapter: adapter.id,
|
|
631
|
-
path: request.url,
|
|
632
|
-
component: 'ProductPage',
|
|
633
|
-
duration: result.timing?.total || 0,
|
|
634
|
-
cacheHit: false,
|
|
635
|
-
});
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
---
|
|
639
|
-
|
|
640
|
-
## Security Considerations
|
|
641
|
-
|
|
642
|
-
### XSS Prevention
|
|
643
|
-
|
|
644
|
-
Escape user content in rendered HTML.
|
|
645
|
-
|
|
646
|
-
```typescript
|
|
647
|
-
function escapeHtml(unsafe: string): string {
|
|
648
|
-
return unsafe
|
|
649
|
-
.replace(/&/g, '&')
|
|
650
|
-
.replace(/</g, '<')
|
|
651
|
-
.replace(/>/g, '>')
|
|
652
|
-
.replace(/"/g, '"')
|
|
653
|
-
.replace(/'/g, ''');
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// For HTMX templates
|
|
657
|
-
function safeTemplate(user: { name: string }) {
|
|
658
|
-
return `
|
|
659
|
-
<div class="user-profile">
|
|
660
|
-
<h1>${escapeHtml(user.name)}</h1>
|
|
661
|
-
</div>
|
|
662
|
-
`;
|
|
663
|
-
}
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
### CSP Headers
|
|
667
|
-
|
|
668
|
-
Configure Content Security Policy for hydration scripts.
|
|
669
|
-
|
|
670
|
-
```typescript
|
|
671
|
-
function generateCSPHeader(nonce: string): string {
|
|
672
|
-
return [
|
|
673
|
-
`script-src 'self' 'nonce-${nonce}'`,
|
|
674
|
-
`style-src 'self' 'unsafe-inline'`,
|
|
675
|
-
`img-src 'self' data: https:`,
|
|
676
|
-
`connect-src 'self'`,
|
|
677
|
-
].join('; ');
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// In hydration script
|
|
681
|
-
function getHydrationScript(result: RenderResult, nonce: string): string {
|
|
682
|
-
return `
|
|
683
|
-
<script nonce="${nonce}">
|
|
684
|
-
window.__FLIGHT_DATA__ = ${JSON.stringify(result.hydrationData)};
|
|
685
|
-
</script>
|
|
686
|
-
`;
|
|
687
|
-
}
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
### Input Validation
|
|
691
|
-
|
|
692
|
-
Validate props before rendering.
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
import { z } from 'zod';
|
|
696
|
-
|
|
697
|
-
const UserPropsSchema = z.object({
|
|
698
|
-
id: z.string().uuid(),
|
|
699
|
-
name: z.string().max(100),
|
|
700
|
-
role: z.enum(['user', 'admin']),
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
async function safeRender(adapter: UIAdapterV2, props: unknown) {
|
|
704
|
-
const validated = UserPropsSchema.parse(props);
|
|
705
|
-
|
|
706
|
-
return adapter.renderToString({
|
|
707
|
-
component: UserProfile,
|
|
708
|
-
props: validated,
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
## Testing Strategies
|
|
716
|
-
|
|
717
|
-
### Unit Testing Adapters
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
import { describe, it, expect } from 'vitest';
|
|
721
|
-
import { react } from '@flight-framework/ui/react';
|
|
722
|
-
|
|
723
|
-
describe('React Adapter', () => {
|
|
724
|
-
it('renders component to string', async () => {
|
|
725
|
-
const adapter = react();
|
|
726
|
-
|
|
727
|
-
const result = await adapter.renderToString({
|
|
728
|
-
component: () => null,
|
|
729
|
-
props: {},
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
expect(result.html).toBeDefined();
|
|
733
|
-
expect(result.timing).toBeDefined();
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
it('generates hydration script', async () => {
|
|
737
|
-
const adapter = react();
|
|
738
|
-
|
|
739
|
-
const script = adapter.getHydrationScript({
|
|
740
|
-
html: '<div>Test</div>',
|
|
741
|
-
hydrationData: { test: true },
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
expect(script).toContain('<script');
|
|
745
|
-
expect(script).toContain('__FLIGHT_DATA__');
|
|
746
|
-
});
|
|
747
|
-
});
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
### Integration Testing SSR
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
754
|
-
import { adapterRegistry, registerBuiltinAdapters } from '@flight-framework/ui';
|
|
755
|
-
|
|
756
|
-
describe('SSR Integration', () => {
|
|
757
|
-
beforeAll(() => {
|
|
758
|
-
registerBuiltinAdapters();
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it('renders full page with React', async () => {
|
|
762
|
-
const adapter = await adapterRegistry.get('react');
|
|
763
|
-
|
|
764
|
-
const result = await adapter.renderToString({
|
|
765
|
-
component: App,
|
|
766
|
-
props: { user: { name: 'Test' } },
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
expect(result.html).toContain('Test');
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it('streams content correctly', async () => {
|
|
773
|
-
const adapter = await adapterRegistry.get('react');
|
|
774
|
-
|
|
775
|
-
const { stream, done } = adapter.renderToStream({
|
|
776
|
-
component: App,
|
|
777
|
-
props: {},
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
const chunks: Uint8Array[] = [];
|
|
781
|
-
const reader = stream.getReader();
|
|
782
|
-
|
|
783
|
-
while (true) {
|
|
784
|
-
const { done: readerDone, value } = await reader.read();
|
|
785
|
-
if (readerDone) break;
|
|
786
|
-
chunks.push(value);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
await done;
|
|
790
|
-
|
|
791
|
-
expect(chunks.length).toBeGreaterThan(0);
|
|
792
|
-
});
|
|
793
|
-
});
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
### Performance Testing
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
import { describe, it, expect } from 'vitest';
|
|
800
|
-
import { react } from '@flight-framework/ui/react';
|
|
801
|
-
|
|
802
|
-
describe('Performance', () => {
|
|
803
|
-
it('renders within acceptable time', async () => {
|
|
804
|
-
const adapter = react();
|
|
805
|
-
|
|
806
|
-
const start = performance.now();
|
|
807
|
-
|
|
808
|
-
await adapter.renderToString({
|
|
809
|
-
component: LargePage,
|
|
810
|
-
props: {},
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const duration = performance.now() - start;
|
|
814
|
-
|
|
815
|
-
// Should render in under 100ms
|
|
816
|
-
expect(duration).toBeLessThan(100);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('handles concurrent renders', async () => {
|
|
820
|
-
const adapter = react();
|
|
821
|
-
|
|
822
|
-
const renders = Array.from({ length: 10 }, () =>
|
|
823
|
-
adapter.renderToString({
|
|
824
|
-
component: TestComponent,
|
|
825
|
-
props: {},
|
|
826
|
-
})
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
const results = await Promise.all(renders);
|
|
830
|
-
|
|
831
|
-
results.forEach(result => {
|
|
832
|
-
expect(result.html).toBeDefined();
|
|
833
|
-
});
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
```
|