@flightdev/ui 2.0.1 → 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.
Files changed (120) hide show
  1. package/README.md +283 -68
  2. package/dist/{chunk-XTDK7ME5.js → chunk-S4DTUQII.js} +246 -19
  3. package/dist/chunk-S4DTUQII.js.map +1 -0
  4. package/dist/core/index.d.ts +423 -3
  5. package/dist/core/index.js +23 -2
  6. package/dist/core/index.js.map +1 -0
  7. package/dist/index.d.ts +2 -3
  8. package/dist/index.js +29 -5
  9. package/dist/index.js.map +1 -0
  10. package/package.json +7 -183
  11. package/.turbo/turbo-build.log +0 -81
  12. package/.turbo/turbo-lint.log +0 -40
  13. package/.turbo/turbo-typecheck.log +0 -4
  14. package/TESTING.md +0 -124
  15. package/dist/adapter-MMD-iHNx.d.ts +0 -424
  16. package/dist/adapters/tier-1/angular.d.ts +0 -60
  17. package/dist/adapters/tier-1/angular.js +0 -2
  18. package/dist/adapters/tier-1/index.d.ts +0 -7
  19. package/dist/adapters/tier-1/index.js +0 -7
  20. package/dist/adapters/tier-1/qwik.d.ts +0 -55
  21. package/dist/adapters/tier-1/qwik.js +0 -2
  22. package/dist/adapters/tier-1/react.d.ts +0 -67
  23. package/dist/adapters/tier-1/react.js +0 -2
  24. package/dist/adapters/tier-1/solid.d.ts +0 -45
  25. package/dist/adapters/tier-1/solid.js +0 -2
  26. package/dist/adapters/tier-1/svelte.d.ts +0 -48
  27. package/dist/adapters/tier-1/svelte.js +0 -2
  28. package/dist/adapters/tier-1/vue.d.ts +0 -47
  29. package/dist/adapters/tier-1/vue.js +0 -2
  30. package/dist/adapters/tier-2/index.d.ts +0 -7
  31. package/dist/adapters/tier-2/index.js +0 -7
  32. package/dist/adapters/tier-2/inferno.d.ts +0 -31
  33. package/dist/adapters/tier-2/inferno.js +0 -2
  34. package/dist/adapters/tier-2/lit.d.ts +0 -34
  35. package/dist/adapters/tier-2/lit.js +0 -2
  36. package/dist/adapters/tier-2/marko.d.ts +0 -59
  37. package/dist/adapters/tier-2/marko.js +0 -2
  38. package/dist/adapters/tier-2/mithril.d.ts +0 -31
  39. package/dist/adapters/tier-2/mithril.js +0 -2
  40. package/dist/adapters/tier-2/preact.d.ts +0 -33
  41. package/dist/adapters/tier-2/preact.js +0 -2
  42. package/dist/adapters/tier-2/stencil.d.ts +0 -52
  43. package/dist/adapters/tier-2/stencil.js +0 -2
  44. package/dist/adapters/tier-3/alpine.d.ts +0 -73
  45. package/dist/adapters/tier-3/alpine.js +0 -2
  46. package/dist/adapters/tier-3/hotwire.d.ts +0 -71
  47. package/dist/adapters/tier-3/hotwire.js +0 -2
  48. package/dist/adapters/tier-3/htmx.d.ts +0 -88
  49. package/dist/adapters/tier-3/htmx.js +0 -2
  50. package/dist/adapters/tier-3/index.d.ts +0 -7
  51. package/dist/adapters/tier-3/index.js +0 -7
  52. package/dist/adapters/tier-3/petite-vue.d.ts +0 -56
  53. package/dist/adapters/tier-3/petite-vue.js +0 -2
  54. package/dist/adapters/tier-3/stimulus.d.ts +0 -63
  55. package/dist/adapters/tier-3/stimulus.js +0 -2
  56. package/dist/adapters/tier-3/vanilla.d.ts +0 -63
  57. package/dist/adapters/tier-3/vanilla.js +0 -2
  58. package/dist/chunk-2SNQ6PTM.js +0 -217
  59. package/dist/chunk-3D4XMIZI.js +0 -136
  60. package/dist/chunk-3HU6GSQ4.js +0 -125
  61. package/dist/chunk-4PZDNFL7.js +0 -148
  62. package/dist/chunk-5IBLFTYL.js +0 -114
  63. package/dist/chunk-64JZJ7OK.js +0 -142
  64. package/dist/chunk-7ZJI3QU2.js +0 -132
  65. package/dist/chunk-CE4FJHQJ.js +0 -133
  66. package/dist/chunk-DTCAUBH5.js +0 -87
  67. package/dist/chunk-NTASPOHG.js +0 -106
  68. package/dist/chunk-OI2AMQLG.js +0 -152
  69. package/dist/chunk-Q7HUE44H.js +0 -106
  70. package/dist/chunk-QH3LOWXU.js +0 -155
  71. package/dist/chunk-QIVAK6BH.js +0 -103
  72. package/dist/chunk-V34XPVGK.js +0 -103
  73. package/dist/chunk-VK7ZPMO7.js +0 -221
  74. package/dist/chunk-X6CNUW6T.js +0 -136
  75. package/dist/chunk-YFGSHW5S.js +0 -121
  76. package/dist/chunk-ZAJVSE7J.js +0 -90
  77. package/docs/ADAPTERS.md +0 -946
  78. package/docs/PATTERNS.md +0 -836
  79. package/src/adapters/tier-1/angular.ts +0 -223
  80. package/src/adapters/tier-1/index.ts +0 -12
  81. package/src/adapters/tier-1/qwik.ts +0 -177
  82. package/src/adapters/tier-1/react.ts +0 -330
  83. package/src/adapters/tier-1/solid.ts +0 -222
  84. package/src/adapters/tier-1/svelte.ts +0 -211
  85. package/src/adapters/tier-1/vue.ts +0 -234
  86. package/src/adapters/tier-2/index.ts +0 -12
  87. package/src/adapters/tier-2/inferno.ts +0 -149
  88. package/src/adapters/tier-2/lit.ts +0 -191
  89. package/src/adapters/tier-2/marko.ts +0 -199
  90. package/src/adapters/tier-2/mithril.ts +0 -152
  91. package/src/adapters/tier-2/preact.ts +0 -133
  92. package/src/adapters/tier-2/stencil.ts +0 -214
  93. package/src/adapters/tier-3/alpine.ts +0 -218
  94. package/src/adapters/tier-3/hotwire.ts +0 -254
  95. package/src/adapters/tier-3/htmx.ts +0 -263
  96. package/src/adapters/tier-3/index.ts +0 -12
  97. package/src/adapters/tier-3/petite-vue.ts +0 -163
  98. package/src/adapters/tier-3/stimulus.ts +0 -233
  99. package/src/adapters/tier-3/vanilla.ts +0 -252
  100. package/src/ambient.d.ts +0 -310
  101. package/src/core/adapter.ts +0 -366
  102. package/src/core/index.ts +0 -56
  103. package/src/core/registry.ts +0 -518
  104. package/src/core/types.ts +0 -461
  105. package/src/htmx.ts +0 -134
  106. package/src/index.ts +0 -263
  107. package/test/__mocks__/stencil-core.ts +0 -19
  108. package/test/__mocks__/stencil-hydrate.ts +0 -15
  109. package/test/adapters/tier-1.test.ts +0 -206
  110. package/test/adapters/tier-2.test.ts +0 -175
  111. package/test/adapters/tier-3.test.ts +0 -284
  112. package/test/contracts/adapter.contract.ts +0 -293
  113. package/test/core/core.test.ts +0 -310
  114. package/test/errors/error-handling.test.ts +0 -454
  115. package/test/integration/htmx.integration.test.ts +0 -246
  116. package/test/integration/react.integration.test.ts +0 -271
  117. package/test/integration/registry.integration.test.ts +0 -308
  118. package/tsconfig.json +0 -22
  119. package/tsup.config.ts +0 -93
  120. 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 '@flightdev/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 '@flightdev/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 '@flightdev/ui/react';
77
- import { htmx } from '@flightdev/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 '@flightdev/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 '@flightdev/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('@flightdev/ui/react'),
305
- vue: () => import('@flightdev/ui/vue'),
306
- htmx: () => import('@flightdev/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, '&amp;')
650
- .replace(/</g, '&lt;')
651
- .replace(/>/g, '&gt;')
652
- .replace(/"/g, '&quot;')
653
- .replace(/'/g, '&#039;');
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 '@flightdev/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 '@flightdev/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 '@flightdev/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
- ```