@flight-framework/core 0.0.3 → 0.1.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/dist/{chunk-CLMFEKYM.js → chunk-54HPVE7N.js} +67 -2
- package/dist/chunk-54HPVE7N.js.map +1 -0
- package/dist/{chunk-ABNCAPQB.js → chunk-6WSPUG5L.js} +2 -2
- package/dist/chunk-6WSPUG5L.js.map +1 -0
- package/dist/chunk-I2B4WSHC.js +126 -0
- package/dist/chunk-I2B4WSHC.js.map +1 -0
- package/dist/chunk-MQQLYWZZ.js +288 -0
- package/dist/chunk-MQQLYWZZ.js.map +1 -0
- package/dist/chunk-OBNYNJB5.js +353 -0
- package/dist/chunk-OBNYNJB5.js.map +1 -0
- package/dist/chunk-WFAWAHJH.js +267 -0
- package/dist/chunk-WFAWAHJH.js.map +1 -0
- package/dist/chunk-XOIYNY4I.js +164 -0
- package/dist/chunk-XOIYNY4I.js.map +1 -0
- package/dist/chunk-YIOQC3DC.js +282 -0
- package/dist/chunk-YIOQC3DC.js.map +1 -0
- package/dist/file-router/index.d.ts +9 -0
- package/dist/file-router/index.js +1 -1
- package/dist/file-router/streaming-hints.d.ts +129 -0
- package/dist/file-router/streaming-hints.js +3 -0
- package/dist/file-router/streaming-hints.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/islands/index.d.ts +234 -0
- package/dist/islands/index.js +3 -0
- package/dist/islands/index.js.map +1 -0
- package/dist/streaming/adapters/index.d.ts +223 -0
- package/dist/streaming/adapters/index.js +3 -0
- package/dist/streaming/adapters/index.js.map +1 -0
- package/dist/streaming/conditional.d.ts +130 -0
- package/dist/streaming/conditional.js +3 -0
- package/dist/streaming/conditional.js.map +1 -0
- package/dist/streaming/index.d.ts +8 -0
- package/dist/streaming/index.js +1 -1
- package/dist/streaming/observability.d.ts +201 -0
- package/dist/streaming/observability.js +4 -0
- package/dist/streaming/observability.js.map +1 -0
- package/dist/streaming/priority.d.ts +103 -0
- package/dist/streaming/priority.js +3 -0
- package/dist/streaming/priority.js.map +1 -0
- package/package.json +25 -1
- package/dist/chunk-ABNCAPQB.js.map +0 -1
- package/dist/chunk-CLMFEKYM.js.map +0 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { SuspenseBoundaryConfig, StreamingRenderOptions } from './index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @flight-framework/core - Streaming Observability
|
|
5
|
+
*
|
|
6
|
+
* Zero-telemetry metrics and instrumentation for streaming SSR.
|
|
7
|
+
* All data stays on YOUR infrastructure - never sent anywhere.
|
|
8
|
+
*
|
|
9
|
+
* Best Practices 2026:
|
|
10
|
+
* - Comprehensive timing metrics for each boundary
|
|
11
|
+
* - Hook-based observability for custom integrations
|
|
12
|
+
* - Performance insights without vendor lock-in
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { createInstrumentedStream } from '@flight-framework/core/streaming';
|
|
17
|
+
*
|
|
18
|
+
* const { stream, metrics } = await createInstrumentedStream({
|
|
19
|
+
* shell: '<html>...',
|
|
20
|
+
* boundaries: [...],
|
|
21
|
+
* onMetrics: (m) => myAnalytics.track(m), // YOUR system
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Timing information for a single boundary
|
|
28
|
+
*/
|
|
29
|
+
interface BoundaryTiming {
|
|
30
|
+
/** Boundary identifier */
|
|
31
|
+
id: string;
|
|
32
|
+
/** When the boundary started resolving (ms since stream start) */
|
|
33
|
+
startTime: number;
|
|
34
|
+
/** When the boundary finished resolving */
|
|
35
|
+
endTime: number;
|
|
36
|
+
/** Total duration in milliseconds */
|
|
37
|
+
duration: number;
|
|
38
|
+
/** Whether it resolved successfully */
|
|
39
|
+
success: boolean;
|
|
40
|
+
/** Size of content in bytes (if successful) */
|
|
41
|
+
contentSize?: number;
|
|
42
|
+
/** Error message if failed */
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Overall streaming metrics
|
|
47
|
+
*/
|
|
48
|
+
interface StreamingMetrics {
|
|
49
|
+
/** Unique ID for this stream session */
|
|
50
|
+
streamId: string;
|
|
51
|
+
/** When streaming started */
|
|
52
|
+
startTime: number;
|
|
53
|
+
/** Time to shell ready (TTFB improvement) */
|
|
54
|
+
shellTime: number;
|
|
55
|
+
/** Time until all boundaries resolved */
|
|
56
|
+
totalStreamTime: number;
|
|
57
|
+
/** Individual boundary timings */
|
|
58
|
+
boundaries: BoundaryTiming[];
|
|
59
|
+
/** Number of boundaries that resolved successfully */
|
|
60
|
+
successCount: number;
|
|
61
|
+
/** Number of boundaries that failed */
|
|
62
|
+
errorCount: number;
|
|
63
|
+
/** Total bytes streamed */
|
|
64
|
+
totalBytes: number;
|
|
65
|
+
/** Strategy used (if priority streaming) */
|
|
66
|
+
strategy?: string;
|
|
67
|
+
/** Custom metadata you can add */
|
|
68
|
+
meta?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Observability hooks for streaming events
|
|
72
|
+
*/
|
|
73
|
+
interface StreamingObserver {
|
|
74
|
+
/** Called when streaming starts */
|
|
75
|
+
onStreamStart?: (streamId: string) => void;
|
|
76
|
+
/** Called when shell is sent */
|
|
77
|
+
onShellReady?: (timing: {
|
|
78
|
+
streamId: string;
|
|
79
|
+
duration: number;
|
|
80
|
+
}) => void;
|
|
81
|
+
/** Called when a boundary starts resolving */
|
|
82
|
+
onBoundaryStart?: (info: {
|
|
83
|
+
streamId: string;
|
|
84
|
+
boundaryId: string;
|
|
85
|
+
startTime: number;
|
|
86
|
+
}) => void;
|
|
87
|
+
/** Called when a boundary finishes */
|
|
88
|
+
onBoundaryEnd?: (timing: BoundaryTiming & {
|
|
89
|
+
streamId: string;
|
|
90
|
+
}) => void;
|
|
91
|
+
/** Called when streaming completes */
|
|
92
|
+
onStreamEnd?: (metrics: StreamingMetrics) => void;
|
|
93
|
+
/** Called on any error */
|
|
94
|
+
onError?: (error: {
|
|
95
|
+
streamId: string;
|
|
96
|
+
boundaryId?: string;
|
|
97
|
+
error: Error;
|
|
98
|
+
}) => void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Configuration for instrumented streaming
|
|
102
|
+
*/
|
|
103
|
+
interface InstrumentedStreamConfig {
|
|
104
|
+
/** Initial HTML shell */
|
|
105
|
+
shell: string;
|
|
106
|
+
/** Closing HTML */
|
|
107
|
+
shellEnd: string;
|
|
108
|
+
/** Suspense boundaries */
|
|
109
|
+
suspenseBoundaries: SuspenseBoundaryConfig[];
|
|
110
|
+
/** Streaming options */
|
|
111
|
+
options?: StreamingRenderOptions;
|
|
112
|
+
/** Observer hooks */
|
|
113
|
+
observer?: StreamingObserver;
|
|
114
|
+
/** Callback when all metrics are ready */
|
|
115
|
+
onMetrics?: (metrics: StreamingMetrics) => void;
|
|
116
|
+
/** Custom metadata to include in metrics */
|
|
117
|
+
meta?: Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Result of instrumented streaming
|
|
121
|
+
*/
|
|
122
|
+
interface InstrumentedStreamResult {
|
|
123
|
+
/** The readable stream */
|
|
124
|
+
stream: ReadableStream<Uint8Array>;
|
|
125
|
+
/** Abort the stream */
|
|
126
|
+
abort: () => void;
|
|
127
|
+
/** Shell ready promise */
|
|
128
|
+
shellReady: Promise<void>;
|
|
129
|
+
/** All content ready promise */
|
|
130
|
+
allReady: Promise<void>;
|
|
131
|
+
/** Final metrics (resolves when stream completes) */
|
|
132
|
+
metrics: Promise<StreamingMetrics>;
|
|
133
|
+
/** Stream ID for correlation */
|
|
134
|
+
streamId: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Create an instrumented streaming SSR response with full observability
|
|
138
|
+
*/
|
|
139
|
+
declare function createInstrumentedStream(config: InstrumentedStreamConfig): Promise<InstrumentedStreamResult>;
|
|
140
|
+
/**
|
|
141
|
+
* Aggregate metrics from multiple streams for analysis
|
|
142
|
+
*/
|
|
143
|
+
declare class MetricsAggregator {
|
|
144
|
+
private metrics;
|
|
145
|
+
private maxSamples;
|
|
146
|
+
constructor(maxSamples?: number);
|
|
147
|
+
/**
|
|
148
|
+
* Add metrics from a stream
|
|
149
|
+
*/
|
|
150
|
+
add(metrics: StreamingMetrics): void;
|
|
151
|
+
/**
|
|
152
|
+
* Get average shell time
|
|
153
|
+
*/
|
|
154
|
+
getAverageShellTime(): number;
|
|
155
|
+
/**
|
|
156
|
+
* Get average total stream time
|
|
157
|
+
*/
|
|
158
|
+
getAverageTotalTime(): number;
|
|
159
|
+
/**
|
|
160
|
+
* Get slowest boundaries (by average duration)
|
|
161
|
+
*/
|
|
162
|
+
getSlowestBoundaries(limit?: number): {
|
|
163
|
+
id: string;
|
|
164
|
+
avgDuration: number;
|
|
165
|
+
errorRate: number;
|
|
166
|
+
}[];
|
|
167
|
+
/**
|
|
168
|
+
* Get overall error rate
|
|
169
|
+
*/
|
|
170
|
+
getErrorRate(): number;
|
|
171
|
+
/**
|
|
172
|
+
* Get percentiles for shell time
|
|
173
|
+
*/
|
|
174
|
+
getShellTimePercentiles(): {
|
|
175
|
+
p50: number;
|
|
176
|
+
p90: number;
|
|
177
|
+
p99: number;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Export all metrics for external analysis
|
|
181
|
+
*/
|
|
182
|
+
export(): StreamingMetrics[];
|
|
183
|
+
/**
|
|
184
|
+
* Clear all metrics
|
|
185
|
+
*/
|
|
186
|
+
clear(): void;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a streaming observer from a simple logger function
|
|
190
|
+
*/
|
|
191
|
+
declare function createLoggerObserver(log: (message: string, data?: Record<string, unknown>) => void): StreamingObserver;
|
|
192
|
+
/**
|
|
193
|
+
* Create observer that sends metrics to a custom endpoint (YOUR infrastructure)
|
|
194
|
+
*/
|
|
195
|
+
declare function createHttpObserver(endpoint: string, options?: {
|
|
196
|
+
headers?: Record<string, string>;
|
|
197
|
+
batchSize?: number;
|
|
198
|
+
flushInterval?: number;
|
|
199
|
+
}): StreamingObserver;
|
|
200
|
+
|
|
201
|
+
export { type BoundaryTiming, type InstrumentedStreamConfig, type InstrumentedStreamResult, MetricsAggregator, type StreamingMetrics, type StreamingObserver, createHttpObserver, createInstrumentedStream, createLoggerObserver };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"observability.js"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { StreamingRenderOptions, StreamingRenderResult } from './index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @flight-framework/core - Priority-Based Streaming SSR
|
|
5
|
+
*
|
|
6
|
+
* Out-of-order streaming with priority control, deadlines, and dependencies.
|
|
7
|
+
* Gives developers full control over streaming order and timing.
|
|
8
|
+
*
|
|
9
|
+
* Best Practices 2026:
|
|
10
|
+
* - Priority-based content delivery for optimal UX
|
|
11
|
+
* - Deadline-aware streaming to prevent slow boundaries from blocking
|
|
12
|
+
* - Dependency graphs for complex data relationships
|
|
13
|
+
* - Strategy-based streaming modes for different use cases
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { streamWithPriority } from '@flight-framework/core/streaming';
|
|
18
|
+
*
|
|
19
|
+
* const result = await streamWithPriority({
|
|
20
|
+
* shell: '<html>...',
|
|
21
|
+
* shellEnd: '</html>',
|
|
22
|
+
* boundaries: [
|
|
23
|
+
* { id: 'hero', priority: 1, content: fetchHero() },
|
|
24
|
+
* { id: 'nav', priority: 2, content: fetchNav() },
|
|
25
|
+
* { id: 'comments', priority: 10, content: fetchComments() },
|
|
26
|
+
* ],
|
|
27
|
+
* strategy: 'priority-first',
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Boundary with priority and advanced options
|
|
34
|
+
*/
|
|
35
|
+
interface PriorityBoundary {
|
|
36
|
+
/** Unique identifier for this boundary */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Fallback HTML while loading */
|
|
39
|
+
fallback: string;
|
|
40
|
+
/** Content promise or factory function */
|
|
41
|
+
content: Promise<string> | (() => Promise<string>);
|
|
42
|
+
/** Priority level (lower number = higher priority, rendered first) */
|
|
43
|
+
priority: number;
|
|
44
|
+
/** Maximum time to wait before skipping (ms). Boundary shows fallback if exceeded. */
|
|
45
|
+
deadline?: number;
|
|
46
|
+
/** IDs of boundaries that must resolve before this one starts */
|
|
47
|
+
dependsOn?: string[];
|
|
48
|
+
/** Optional metadata for observability */
|
|
49
|
+
meta?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Streaming strategy - how to order content delivery
|
|
53
|
+
*/
|
|
54
|
+
type StreamingStrategy = 'priority-first' | 'first-ready' | 'dependency-aware' | 'round-robin' | 'deadline-strict';
|
|
55
|
+
/**
|
|
56
|
+
* Result of a priority boundary resolution
|
|
57
|
+
*/
|
|
58
|
+
interface BoundaryResolution {
|
|
59
|
+
id: string;
|
|
60
|
+
content: string | null;
|
|
61
|
+
duration: number;
|
|
62
|
+
status: 'resolved' | 'timeout' | 'error' | 'skipped';
|
|
63
|
+
error?: Error;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Priority streaming configuration
|
|
67
|
+
*/
|
|
68
|
+
interface PriorityStreamConfig {
|
|
69
|
+
/** Initial HTML shell */
|
|
70
|
+
shell: string;
|
|
71
|
+
/** Closing HTML */
|
|
72
|
+
shellEnd: string;
|
|
73
|
+
/** Boundaries with priorities */
|
|
74
|
+
boundaries: PriorityBoundary[];
|
|
75
|
+
/** Streaming strategy */
|
|
76
|
+
strategy: StreamingStrategy;
|
|
77
|
+
/** Base streaming options */
|
|
78
|
+
options?: StreamingRenderOptions;
|
|
79
|
+
/** Callback when a boundary resolves */
|
|
80
|
+
onBoundaryResolved?: (resolution: BoundaryResolution) => void;
|
|
81
|
+
/** Global timeout for all boundaries */
|
|
82
|
+
globalTimeout?: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Priority streaming result with additional metrics
|
|
86
|
+
*/
|
|
87
|
+
interface PriorityStreamResult extends StreamingRenderResult {
|
|
88
|
+
/** Resolution details for each boundary */
|
|
89
|
+
resolutions: Promise<BoundaryResolution[]>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create a priority-ordered streaming SSR response
|
|
93
|
+
*/
|
|
94
|
+
declare function streamWithPriority(config: PriorityStreamConfig): Promise<PriorityStreamResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Validate that boundaries don't have circular dependencies
|
|
97
|
+
*/
|
|
98
|
+
declare function validateDependencies(boundaries: PriorityBoundary[]): {
|
|
99
|
+
valid: boolean;
|
|
100
|
+
cycles?: string[][];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export { type BoundaryResolution, type PriorityBoundary, type PriorityStreamConfig, type PriorityStreamResult, type StreamingStrategy, streamWithPriority, validateDependencies };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"priority.js"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flight-framework/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Core primitives for Flight Framework - routing, rendering, caching",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flight",
|
|
@@ -61,6 +61,30 @@
|
|
|
61
61
|
"types": "./dist/streaming/index.d.ts",
|
|
62
62
|
"import": "./dist/streaming/index.js"
|
|
63
63
|
},
|
|
64
|
+
"./streaming/priority": {
|
|
65
|
+
"types": "./dist/streaming/priority.d.ts",
|
|
66
|
+
"import": "./dist/streaming/priority.js"
|
|
67
|
+
},
|
|
68
|
+
"./streaming/observability": {
|
|
69
|
+
"types": "./dist/streaming/observability.d.ts",
|
|
70
|
+
"import": "./dist/streaming/observability.js"
|
|
71
|
+
},
|
|
72
|
+
"./streaming/conditional": {
|
|
73
|
+
"types": "./dist/streaming/conditional.d.ts",
|
|
74
|
+
"import": "./dist/streaming/conditional.js"
|
|
75
|
+
},
|
|
76
|
+
"./streaming/adapters": {
|
|
77
|
+
"types": "./dist/streaming/adapters/index.d.ts",
|
|
78
|
+
"import": "./dist/streaming/adapters/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./islands": {
|
|
81
|
+
"types": "./dist/islands/index.d.ts",
|
|
82
|
+
"import": "./dist/islands/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./file-router/streaming-hints": {
|
|
85
|
+
"types": "./dist/file-router/streaming-hints.d.ts",
|
|
86
|
+
"import": "./dist/file-router/streaming-hints.js"
|
|
87
|
+
},
|
|
64
88
|
"./rsc": {
|
|
65
89
|
"types": "./dist/rsc/index.d.ts",
|
|
66
90
|
"import": "./dist/rsc/index.js"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/streaming/index.ts"],"names":[],"mappings":"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","file":"chunk-ABNCAPQB.js","sourcesContent":["/**\r\n * @flight-framework/core - Streaming SSR\r\n * \r\n * Full streaming server-side rendering implementation following React 18+/19 patterns.\r\n * Supports both Node.js (renderToPipeableStream) and Edge (renderToReadableStream) environments.\r\n * \r\n * Best Practices 2025/2026:\r\n * - Progressive HTML streaming with Suspense boundaries\r\n * - Shell-first rendering for fast TTFB\r\n * - Nested Suspense for granular loading states\r\n * - Error boundaries for graceful degradation\r\n * - Web Streams API for Edge runtime compatibility\r\n */\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Streaming render options following React's conventions\r\n */\r\nexport interface StreamingRenderOptions {\r\n /** Bootstrap scripts to load on client */\r\n bootstrapScripts?: string[];\r\n /** Bootstrap ES modules */\r\n bootstrapModules?: string[];\r\n /** Inline script content */\r\n bootstrapScriptContent?: string;\r\n /** Prefix for React IDs (useId) */\r\n identifierPrefix?: string;\r\n /** Nonce for CSP */\r\n nonce?: string;\r\n /** Callback when shell is ready (main content before Suspense) */\r\n onShellReady?: () => void;\r\n /** Callback when shell errors */\r\n onShellError?: (error: Error) => void;\r\n /** Callback when all content is ready */\r\n onAllReady?: () => void;\r\n /** Callback for any error */\r\n onError?: (error: Error) => void;\r\n /** Timeout before aborting stream */\r\n timeoutMs?: number;\r\n /** Progressive hydration enabled */\r\n progressiveHydration?: boolean;\r\n}\r\n\r\n/**\r\n * Streaming render result\r\n */\r\nexport interface StreamingRenderResult {\r\n /** The readable stream to pipe to response */\r\n stream: ReadableStream<Uint8Array>;\r\n /** Abort the stream */\r\n abort: () => void;\r\n /** Promise that resolves when shell is ready */\r\n shellReady: Promise<void>;\r\n /** Promise that resolves when all content is ready */\r\n allReady: Promise<void>;\r\n}\r\n\r\n/**\r\n * Suspense boundary configuration\r\n */\r\nexport interface SuspenseBoundaryConfig {\r\n /** Unique ID for this boundary */\r\n id: string;\r\n /** Fallback HTML to show while loading */\r\n fallback: string;\r\n /** Content resolver promise */\r\n contentPromise: Promise<string>;\r\n}\r\n\r\n// ============================================================================\r\n// Streaming Renderer\r\n// ============================================================================\r\n\r\n/**\r\n * Create a streaming SSR response using Web Streams API.\r\n * Compatible with Edge Runtime (Cloudflare, Vercel Edge, Deno).\r\n * \r\n * @example\r\n * ```typescript\r\n * const result = await createStreamingSSR({\r\n * shell: '<html><body><div id=\"root\">',\r\n * shellEnd: '</div></body></html>',\r\n * suspenseBoundaries: [\r\n * {\r\n * id: 'posts',\r\n * fallback: '<div>Loading posts...</div>',\r\n * contentPromise: fetchAndRenderPosts(),\r\n * }\r\n * ],\r\n * bootstrapScripts: ['/client.js'],\r\n * });\r\n * \r\n * return new Response(result.stream, {\r\n * headers: { 'Content-Type': 'text/html' },\r\n * });\r\n * ```\r\n */\r\nexport async function createStreamingSSR(config: {\r\n /** Initial HTML shell (before suspense content) */\r\n shell: string;\r\n /** Closing HTML shell */\r\n shellEnd: string;\r\n /** Suspense boundaries with async content */\r\n suspenseBoundaries?: SuspenseBoundaryConfig[];\r\n /** Streaming options */\r\n options?: StreamingRenderOptions;\r\n}): Promise<StreamingRenderResult> {\r\n const { shell, shellEnd, suspenseBoundaries = [], options = {} } = config;\r\n\r\n let shellResolved = false;\r\n let allResolved = false;\r\n let _aborted = false;\r\n let abortController: AbortController | null = null;\r\n\r\n // Shell ready promise\r\n let resolveShell: () => void;\r\n const shellReady = new Promise<void>((resolve) => {\r\n resolveShell = resolve;\r\n });\r\n\r\n // All ready promise\r\n let resolveAll: () => void;\r\n const allReady = new Promise<void>((resolve) => {\r\n resolveAll = resolve;\r\n });\r\n\r\n const encoder = new TextEncoder();\r\n\r\n const stream = new ReadableStream<Uint8Array>({\r\n async start(controller) {\r\n try {\r\n // 1. Send the shell immediately (fast TTFB)\r\n const shellWithPlaceholders = buildShellWithPlaceholders(\r\n shell,\r\n suspenseBoundaries,\r\n shellEnd,\r\n options\r\n );\r\n\r\n controller.enqueue(encoder.encode(shellWithPlaceholders));\r\n\r\n shellResolved = true;\r\n options.onShellReady?.();\r\n resolveShell!();\r\n\r\n // 2. Stream Suspense boundary contents as they resolve\r\n if (suspenseBoundaries.length > 0) {\r\n await streamSuspenseContent(controller, encoder, suspenseBoundaries, options);\r\n }\r\n\r\n // 3. Send hydration script if needed\r\n if (options.bootstrapScripts?.length || options.bootstrapModules?.length) {\r\n const hydrationScript = buildHydrationScript(options);\r\n controller.enqueue(encoder.encode(hydrationScript));\r\n }\r\n\r\n allResolved = true;\r\n options.onAllReady?.();\r\n resolveAll!();\r\n\r\n controller.close();\r\n } catch (error) {\r\n if (!shellResolved) {\r\n options.onShellError?.(error as Error);\r\n }\r\n options.onError?.(error as Error);\r\n controller.error(error);\r\n }\r\n },\r\n\r\n cancel() {\r\n _aborted = true;\r\n abortController?.abort();\r\n },\r\n });\r\n\r\n // Abort function\r\n const abort = () => {\r\n _aborted = true;\r\n abortController?.abort();\r\n };\r\n\r\n // Timeout handling\r\n if (options.timeoutMs) {\r\n abortController = new AbortController();\r\n setTimeout(() => {\r\n if (!allResolved) {\r\n abort();\r\n }\r\n }, options.timeoutMs);\r\n }\r\n\r\n return {\r\n stream,\r\n abort,\r\n shellReady,\r\n allReady,\r\n };\r\n}\r\n\r\n/**\r\n * Build shell HTML with Suspense placeholders\r\n */\r\nfunction buildShellWithPlaceholders(\r\n shell: string,\r\n boundaries: SuspenseBoundaryConfig[],\r\n shellEnd: string,\r\n options: StreamingRenderOptions\r\n): string {\r\n let html = shell;\r\n\r\n // Add inline bootstrap script if provided\r\n if (options.bootstrapScriptContent) {\r\n html += `<script>${options.bootstrapScriptContent}</script>`;\r\n }\r\n\r\n // Add Suspense fallbacks with placeholder markers\r\n for (const boundary of boundaries) {\r\n html += `\r\n<!--$?--><template id=\"B:${boundary.id}\"></template>\r\n${boundary.fallback}\r\n<!--/$-->`;\r\n }\r\n\r\n html += shellEnd;\r\n\r\n return html;\r\n}\r\n\r\n/**\r\n * Stream Suspense content as promises resolve\r\n */\r\nasync function streamSuspenseContent(\r\n controller: ReadableStreamDefaultController<Uint8Array>,\r\n encoder: TextEncoder,\r\n boundaries: SuspenseBoundaryConfig[],\r\n options: StreamingRenderOptions\r\n): Promise<void> {\r\n // Race all boundaries - stream each as it completes\r\n const pending = boundaries.map(async (boundary) => {\r\n try {\r\n const content = await boundary.contentPromise;\r\n return { boundary, content, error: null };\r\n } catch (error) {\r\n return { boundary, content: null, error: error as Error };\r\n }\r\n });\r\n\r\n // Process as each resolves\r\n const results = await Promise.allSettled(pending);\r\n\r\n for (const result of results) {\r\n if (result.status === 'fulfilled') {\r\n const { boundary, content, error } = result.value;\r\n\r\n if (error) {\r\n // Send error replacement\r\n const errorScript = buildErrorReplacement(boundary.id, error.message);\r\n controller.enqueue(encoder.encode(errorScript));\r\n options.onError?.(error);\r\n } else if (content) {\r\n // Send content replacement script\r\n const replacementScript = buildContentReplacement(boundary.id, content);\r\n controller.enqueue(encoder.encode(replacementScript));\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Build script to replace Suspense placeholder with actual content\r\n * This follows React's streaming hydration pattern\r\n */\r\nfunction buildContentReplacement(id: string, content: string): string {\r\n // Escape script-breaking characters\r\n const escaped = content\r\n .replace(/\\\\/g, '\\\\\\\\')\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/'/g, \"\\\\'\")\r\n .replace(/\"/g, '\\\\\"')\r\n .replace(/\\n/g, '\\\\n')\r\n .replace(/\\r/g, '\\\\r');\r\n\r\n return `\r\n<script>\r\n(function(){\r\n var b=document.getElementById(\"B:${id}\");\r\n if(b){\r\n var p=b.previousSibling;\r\n while(p&&p.nodeType===8&&p.data===\"$?\")p=p.previousSibling;\r\n var n=b.nextSibling;\r\n var f=document.createDocumentFragment();\r\n var t=document.createElement(\"template\");\r\n t.innerHTML=\"${escaped}\";\r\n while(t.content.firstChild)f.appendChild(t.content.firstChild);\r\n if(n&&n.nodeType===8&&n.data===\"/$\"){\r\n var s=n.nextSibling;\r\n while(s&&s!==b){var x=s.nextSibling;s.parentNode.removeChild(s);s=x;}\r\n }\r\n b.parentNode.replaceChild(f,b);\r\n }\r\n})();\r\n</script>`;\r\n}\r\n\r\n/**\r\n * Build script to show error in place of Suspense content\r\n */\r\nfunction buildErrorReplacement(id: string, message: string): string {\r\n const escaped = message.replace(/'/g, \"\\\\'\").replace(/\"/g, '\\\\\"');\r\n\r\n return `\r\n<script>\r\n(function(){\r\n var b=document.getElementById(\"B:${id}\");\r\n if(b){\r\n var t=document.createElement(\"div\");\r\n t.className=\"streaming-error\";\r\n t.textContent=\"Error: ${escaped}\";\r\n b.parentNode.replaceChild(t,b);\r\n }\r\n})();\r\n</script>`;\r\n}\r\n\r\n/**\r\n * Build hydration bootstrap script\r\n */\r\nfunction buildHydrationScript(options: StreamingRenderOptions): string {\r\n let scripts = '';\r\n\r\n if (options.bootstrapScripts?.length) {\r\n for (const src of options.bootstrapScripts) {\r\n const nonceAttr = options.nonce ? ` nonce=\"${options.nonce}\"` : '';\r\n scripts += `<script src=\"${src}\"${nonceAttr} async></script>`;\r\n }\r\n }\r\n\r\n if (options.bootstrapModules?.length) {\r\n for (const src of options.bootstrapModules) {\r\n const nonceAttr = options.nonce ? ` nonce=\"${options.nonce}\"` : '';\r\n scripts += `<script type=\"module\" src=\"${src}\"${nonceAttr}></script>`;\r\n }\r\n }\r\n\r\n return scripts;\r\n}\r\n\r\n// ============================================================================\r\n// Streaming Response Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Create a streaming HTML Response object\r\n */\r\nexport function createStreamingResponse(\r\n stream: ReadableStream<Uint8Array>,\r\n options: {\r\n status?: number;\r\n headers?: Record<string, string>;\r\n } = {}\r\n): Response {\r\n return new Response(stream, {\r\n status: options.status || 200,\r\n headers: {\r\n 'Content-Type': 'text/html; charset=utf-8',\r\n 'Transfer-Encoding': 'chunked',\r\n 'X-Content-Type-Options': 'nosniff',\r\n ...options.headers,\r\n },\r\n });\r\n}\r\n\r\n/**\r\n * Higher-level API: Render page with Suspense boundaries\r\n * \r\n * @example\r\n * ```typescript\r\n * return renderWithStreaming({\r\n * layout: ({ children }) => `\r\n * <html>\r\n * <head><title>My App</title></head>\r\n * <body><div id=\"root\">${children}</div></body>\r\n * </html>\r\n * `,\r\n * page: async () => '<h1>Welcome</h1>',\r\n * suspense: {\r\n * posts: {\r\n * fallback: '<div class=\"skeleton\">Loading...</div>',\r\n * content: fetchPosts().then(renderPosts),\r\n * },\r\n * },\r\n * });\r\n * ```\r\n */\r\nexport async function renderWithStreaming(config: {\r\n /** Layout wrapper */\r\n layout: (props: { children: string }) => string;\r\n /** Main page content (sync part) */\r\n page: () => string | Promise<string>;\r\n /** Suspense boundaries keyed by ID */\r\n suspense?: Record<string, { fallback: string; content: Promise<string> }>;\r\n /** Bootstrap scripts */\r\n bootstrapScripts?: string[];\r\n /** Timeout in ms */\r\n timeoutMs?: number;\r\n}): Promise<Response> {\r\n const { layout, page, suspense = {}, bootstrapScripts, timeoutMs } = config;\r\n\r\n // Get synchronous page content\r\n const pageContent = await page();\r\n\r\n // Build suspense boundaries\r\n const boundaries: SuspenseBoundaryConfig[] = Object.entries(suspense).map(\r\n ([id, { fallback, content }]) => ({\r\n id,\r\n fallback,\r\n contentPromise: content,\r\n })\r\n );\r\n\r\n // Create streaming result\r\n const result = await createStreamingSSR({\r\n shell: layout({ children: pageContent }),\r\n shellEnd: '',\r\n suspenseBoundaries: boundaries,\r\n options: {\r\n bootstrapScripts,\r\n timeoutMs,\r\n },\r\n });\r\n\r\n return createStreamingResponse(result.stream);\r\n}\r\n\r\n// ============================================================================\r\n// Progressive Rendering Utilities\r\n// ============================================================================\r\n\r\n/**\r\n * Create a lazy component that streams its content\r\n */\r\nexport function createLazyContent<T>(\r\n fetcher: () => Promise<T>,\r\n renderer: (data: T) => string,\r\n fallback: string\r\n): { fallback: string; content: Promise<string> } {\r\n return {\r\n fallback,\r\n content: fetcher().then(renderer),\r\n };\r\n}\r\n\r\n/**\r\n * Parallel streaming: resolve multiple boundaries simultaneously\r\n */\r\nexport async function streamParallel(\r\n boundaries: Array<{\r\n id: string;\r\n fallback: string;\r\n content: () => Promise<string>;\r\n }>\r\n): Promise<SuspenseBoundaryConfig[]> {\r\n return boundaries.map((b) => ({\r\n id: b.id,\r\n fallback: b.fallback,\r\n contentPromise: b.content(),\r\n }));\r\n}\r\n\r\n/**\r\n * Sequential streaming: resolve boundaries in order\r\n */\r\nexport async function streamSequential(\r\n boundaries: Array<{\r\n id: string;\r\n fallback: string;\r\n content: () => Promise<string>;\r\n }>\r\n): Promise<SuspenseBoundaryConfig[]> {\r\n const result: SuspenseBoundaryConfig[] = [];\r\n\r\n for (const b of boundaries) {\r\n result.push({\r\n id: b.id,\r\n fallback: b.fallback,\r\n contentPromise: b.content(),\r\n });\r\n }\r\n\r\n return result;\r\n}\r\n\r\n// ============================================================================\r\n// Export for use with React\r\n// ============================================================================\r\n\r\n/**\r\n * NOTE: For React-specific streaming with renderToPipeableStream or \r\n * renderToReadableStream, use the dedicated React adapter:\r\n * \r\n * ```typescript\r\n * import { renderToReadableStream } from 'react-dom/server';\r\n * import { createStreamingResponse } from '@flight-framework/core/streaming';\r\n * \r\n * const stream = await renderToReadableStream(<App />, {\r\n * bootstrapScripts: ['/client.js'],\r\n * });\r\n * \r\n * return createStreamingResponse(stream);\r\n * ```\r\n * \r\n * This module provides framework-agnostic streaming primitives that work\r\n * with any UI library or custom HTML generation.\r\n */\r\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/file-router/index.ts"],"names":[],"mappings":"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","file":"chunk-CLMFEKYM.js","sourcesContent":["/**\r\n * @flight-framework/core - File Router\r\n * \r\n * Auto-discovery of routes from file system.\r\n * Similar to Next.js App Router and Nuxt server/api patterns.\r\n */\r\n\r\nimport { readdir } from 'node:fs/promises';\r\nimport { join, basename, extname } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';\r\n\r\nexport type Handler = (context: unknown) => Response | Promise<Response>;\r\nexport type Middleware = (context: unknown, next: () => Promise<Response>) => Response | Promise<Response>;\r\n\r\nexport interface FileRoute {\r\n /** HTTP method (GET, POST, etc) or 'ALL' */\r\n method: HttpMethod | 'ALL';\r\n /** Route path with params (e.g., /users/:id) */\r\n path: string;\r\n /** Original file path */\r\n filePath: string;\r\n /** Handler function (for APIs) */\r\n handler?: Handler;\r\n /** Route-specific middleware */\r\n middleware?: Middleware[];\r\n /** Route type: 'page' for SSR pages, 'api' for API endpoints */\r\n type: 'page' | 'api';\r\n /** Component function (for pages) */\r\n component?: () => unknown;\r\n /** Page metadata (title, description, etc) */\r\n meta?: Record<string, unknown>;\r\n /** Parallel route slot name (for @folder convention) */\r\n slot?: string;\r\n}\r\n\r\nexport interface FileRouterOptions {\r\n /** Root directory to scan (default: src/routes) */\r\n directory: string;\r\n /** File extensions to consider (default: ['.ts', '.js']) */\r\n extensions?: string[];\r\n /** Whether to watch for changes (default: false in prod) */\r\n watch?: boolean;\r\n /** \r\n * Custom module loader for development with Vite.\r\n * Pass vite.ssrLoadModule to load TSX files correctly.\r\n * Falls back to native import() if not provided.\r\n */\r\n moduleLoader?: (filePath: string) => Promise<any>;\r\n}\r\n\r\nexport interface ScanResult {\r\n routes: FileRoute[];\r\n errors: string[];\r\n}\r\n\r\n// ============================================================================\r\n// File Scanner\r\n// ============================================================================\r\n\r\n/**\r\n * Scan a directory for route files\r\n */\r\nexport async function scanRoutes(options: FileRouterOptions): Promise<ScanResult> {\r\n const {\r\n directory,\r\n extensions = ['.ts', '.js'],\r\n } = options;\r\n\r\n const routes: FileRoute[] = [];\r\n const errors: string[] = [];\r\n\r\n async function scanDir(dir: string, basePath: string = ''): Promise<void> {\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n // Skip hidden directories and node_modules\r\n if (entry.name.startsWith('.') || entry.name === 'node_modules') {\r\n continue;\r\n }\r\n\r\n // Detect parallel route slots (@folder convention)\r\n if (entry.name.startsWith('@')) {\r\n const slotName = entry.name.slice(1);\r\n // Scan slot directory and mark routes with slot name\r\n const slotRoutes = await scanSlotDir(fullPath, basePath, slotName, extensions);\r\n routes.push(...slotRoutes);\r\n continue;\r\n }\r\n\r\n // Recurse into subdirectory\r\n await scanDir(fullPath, `${basePath}/${entry.name}`);\r\n } else if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n // Parse route from filename\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n errors.push(`Failed to scan ${dir}: ${error}`);\r\n }\r\n }\r\n\r\n await scanDir(directory);\r\n\r\n return { routes, errors };\r\n}\r\n\r\n/**\r\n * Scan a parallel route slot directory\r\n */\r\nasync function scanSlotDir(\r\n dir: string,\r\n basePath: string,\r\n slotName: string,\r\n extensions: string[] = ['.ts', '.js', '.tsx', '.jsx']\r\n): Promise<FileRoute[]> {\r\n const routes: FileRoute[] = [];\r\n\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n slot: slotName,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to scan slot @${slotName}:`, error);\r\n }\r\n\r\n return routes;\r\n}\r\n\r\n// ============================================================================\r\n// Route Parser\r\n// ============================================================================\r\n\r\ninterface ParsedRoute {\r\n method: HttpMethod | 'ALL';\r\n path: string;\r\n type: 'page' | 'api';\r\n}\r\n\r\n/**\r\n * Parse route information from filename and path\r\n * \r\n * Patterns:\r\n * - index.ts → /\r\n * - users.ts → /users\r\n * - users.get.ts → GET /users (API)\r\n * - users.post.ts → POST /users (API)\r\n * - about.page.tsx → GET /about (Page)\r\n * - blog/[slug].page.tsx → GET /blog/:slug (Page)\r\n * - [id].ts → /:id\r\n * - [...slug].ts → /* (catch-all)\r\n * - [[...slug]].ts → /* (optional catch-all)\r\n */\r\nfunction parseRouteFile(filename: string, basePath: string): ParsedRoute | null {\r\n const ext = extname(filename);\r\n const nameWithoutExt = basename(filename, ext);\r\n\r\n // Skip files starting with underscore (private)\r\n if (nameWithoutExt.startsWith('_')) {\r\n return null;\r\n }\r\n\r\n // Detect route type\r\n let type: 'page' | 'api' = 'api';\r\n let routeName = nameWithoutExt;\r\n let method: HttpMethod | 'ALL' = 'ALL';\r\n\r\n // Check for page suffix (e.g., about.page.tsx, index.page.tsx)\r\n const pageMatch = nameWithoutExt.match(/^(.+)?\\.page$/i);\r\n if (pageMatch) {\r\n type = 'page';\r\n method = 'GET'; // Pages are always GET\r\n routeName = pageMatch[1] || 'index';\r\n } else {\r\n // Check for method suffix (e.g., users.get.ts)\r\n const methodMatch = nameWithoutExt.match(/^(.+)\\.(get|post|put|delete|patch|head|options)$/i);\r\n if (methodMatch) {\r\n routeName = methodMatch[1] || nameWithoutExt;\r\n method = (methodMatch[2] || 'ALL').toUpperCase() as HttpMethod;\r\n }\r\n }\r\n\r\n // Also check if file is in /api/ directory\r\n if (basePath.startsWith('/api') || basePath.includes('/api/')) {\r\n type = 'api';\r\n }\r\n\r\n // Build route path\r\n let path = basePath;\r\n\r\n if (routeName !== 'index') {\r\n path = `${basePath}/${convertToRoutePath(routeName)}`;\r\n }\r\n\r\n // Ensure path starts with /\r\n if (!path.startsWith('/')) {\r\n path = '/' + path;\r\n }\r\n\r\n // Remove trailing slash (except for root)\r\n if (path !== '/' && path.endsWith('/')) {\r\n path = path.slice(0, -1);\r\n }\r\n\r\n return { method, path, type };\r\n}\r\n\r\n/**\r\n * Convert filename segment to route path segment\r\n * \r\n * - [id] → :id\r\n * - [...slug] → *\r\n * - [[...slug]] → *?\r\n */\r\nfunction convertToRoutePath(name: string): string {\r\n // Optional catch-all: [[...slug]]\r\n if (name.startsWith('[[...') && name.endsWith(']]')) {\r\n const paramName = name.slice(5, -2);\r\n return `:${paramName}*`;\r\n }\r\n\r\n // Catch-all: [...slug]\r\n if (name.startsWith('[...') && name.endsWith(']')) {\r\n const paramName = name.slice(4, -1);\r\n return `:${paramName}+`;\r\n }\r\n\r\n // Dynamic param: [id]\r\n if (name.startsWith('[') && name.endsWith(']')) {\r\n const paramName = name.slice(1, -1);\r\n return `:${paramName}`;\r\n }\r\n\r\n return name;\r\n}\r\n\r\n// ============================================================================\r\n// Route Loader\r\n// ============================================================================\r\n\r\n/**\r\n * Load routes with their handlers or components\r\n * @param scanResult - Result from scanRoutes\r\n * @param moduleLoader - Optional custom loader (use vite.ssrLoadModule for dev)\r\n */\r\nexport async function loadRoutes(\r\n scanResult: ScanResult,\r\n moduleLoader?: (filePath: string) => Promise<any>\r\n): Promise<FileRoute[]> {\r\n const loadedRoutes: FileRoute[] = [];\r\n\r\n for (const route of scanResult.routes) {\r\n try {\r\n // Use custom loader if provided (Vite ssrLoadModule for TSX)\r\n // Otherwise fall back to native import() for production\r\n let module: any;\r\n if (moduleLoader) {\r\n module = await moduleLoader(route.filePath);\r\n } else {\r\n // Convert to file:// URL for Windows ESM compatibility\r\n const fileUrl = pathToFileURL(route.filePath).href;\r\n module = await import(fileUrl);\r\n }\r\n\r\n // Handle PAGE routes\r\n if (route.type === 'page') {\r\n // Pages export default component\r\n const component = module.default;\r\n const meta = module.meta || module.metadata || {};\r\n\r\n if (component) {\r\n loadedRoutes.push({\r\n ...route,\r\n component,\r\n meta,\r\n });\r\n }\r\n continue;\r\n }\r\n\r\n // Handle API routes\r\n if (route.method === 'ALL') {\r\n // Look for named exports: GET, POST, PUT, DELETE, etc\r\n const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];\r\n\r\n for (const method of methods) {\r\n if (typeof module[method] === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method,\r\n handler: module[method],\r\n });\r\n }\r\n }\r\n\r\n // Also check for default export as handler\r\n if (typeof module.default === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method: 'ALL',\r\n handler: module.default,\r\n });\r\n }\r\n } else {\r\n // Specific method from filename suffix\r\n const handler = module[route.method] || module.default;\r\n\r\n if (typeof handler === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n handler,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to load route ${route.filePath}:`, error);\r\n }\r\n }\r\n\r\n return loadedRoutes;\r\n}\r\n\r\n// ============================================================================\r\n// File Router Factory\r\n// ============================================================================\r\n\r\nexport interface FileRouter {\r\n routes: FileRoute[];\r\n refresh: () => Promise<void>;\r\n}\r\n\r\n/**\r\n * Create a file-based router\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createFileRouter } from '@flight-framework/core/file-router';\r\n * import { createServer } from '@flight-framework/http';\r\n * \r\n * const router = await createFileRouter({ directory: './src/routes' });\r\n * const app = createServer();\r\n * \r\n * // Register all discovered routes\r\n * for (const route of router.routes) {\r\n * app[route.method.toLowerCase()](route.path, route.handler);\r\n * }\r\n * ```\r\n */\r\nexport async function createFileRouter(options: FileRouterOptions): Promise<FileRouter> {\r\n let routes: FileRoute[] = [];\r\n const { moduleLoader } = options;\r\n\r\n async function refresh(): Promise<void> {\r\n const scanResult = await scanRoutes(options);\r\n\r\n if (scanResult.errors.length > 0) {\r\n console.warn('[Flight] Route scan errors:', scanResult.errors);\r\n }\r\n\r\n routes = await loadRoutes(scanResult, moduleLoader);\r\n\r\n console.log(`[Flight] Loaded ${routes.length} routes from ${options.directory}`);\r\n }\r\n\r\n // Initial load\r\n await refresh();\r\n\r\n return {\r\n get routes() {\r\n return routes;\r\n },\r\n refresh,\r\n };\r\n}\r\n"]}
|