7
7
FetchAPI ,
8
8
GraphiQLOptions ,
9
9
isAsyncIterable ,
10
+ LandingPageRenderer ,
10
11
useReadinessCheck ,
11
12
YogaServerInstance ,
12
13
type Plugin ,
@@ -18,6 +19,7 @@ import {
18
19
handleFederationSupergraph ,
19
20
isDisposable ,
20
21
OnSubgraphExecuteHook ,
22
+ TransportEntry ,
21
23
UnifiedGraphManager ,
22
24
UnifiedGraphManagerOptions ,
23
25
} from '@graphql-mesh/fusion-runtime' ;
@@ -35,6 +37,7 @@ import { useExecutor } from '@graphql-tools/executor-yoga';
35
37
import { MaybePromise } from '@graphql-tools/utils' ;
36
38
import { getProxyExecutor } from './getProxyExecutor.js' ;
37
39
import { handleUnifiedGraphConfig } from './handleUnifiedGraphConfig.js' ;
40
+ import landingPageHtml from './landing-page-html.js' ;
38
41
import {
39
42
MeshServeConfig ,
40
43
MeshServeConfigContext ,
@@ -81,6 +84,7 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
81
84
let contextBuilder : < T > ( context : T ) => MaybePromise < T > ;
82
85
let readinessChecker : ( ) => MaybePromise < boolean > ;
83
86
let registryPlugin : MeshPlugin < unknown > = { } ;
87
+ let subgraphInformationHTMLRenderer : ( ) => MaybePromise < string > = ( ) => '' ;
84
88
85
89
const disposableStack = new AsyncDisposableStack ( ) ;
86
90
@@ -104,6 +108,10 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
104
108
return mapMaybePromise ( res$ , res => ! isAsyncIterable ( res ) && ! ! res . data ?. __typename ) ;
105
109
} ;
106
110
schemaInvalidator = ( ) => executorPlugin . invalidateUnifiedGraph ( ) ;
111
+ subgraphInformationHTMLRenderer = ( ) => {
112
+ const endpoint = config . proxy . endpoint || '#' ;
113
+ return `<section class="supergraph-information"><h3>Proxy (<a href="${ endpoint } ">${ endpoint } </a>): ${ unifiedGraph ? 'Loaded ✅' : 'Not yet ❌' } </h3></section>` ;
114
+ } ;
107
115
} else {
108
116
let unifiedGraphFetcher : UnifiedGraphManagerOptions < unknown > [ 'getUnifiedGraph' ] ;
109
117
@@ -174,6 +182,51 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
174
182
schemaInvalidator = ( ) => unifiedGraphManager . invalidateUnifiedGraph ( ) ;
175
183
contextBuilder = base => unifiedGraphManager . getContext ( base ) ;
176
184
disposableStack . use ( unifiedGraphManager ) ;
185
+ subgraphInformationHTMLRenderer = async ( ) => {
186
+ const htmlParts : string [ ] = [ ] ;
187
+ let supergraphLoadedPlace : string ;
188
+ if ( 'hive' in config && config . hive . endpoint ) {
189
+ supergraphLoadedPlace = 'Hive CDN <br>' + config . hive . endpoint ;
190
+ } else if ( 'supergraph' in config ) {
191
+ if ( typeof config . supergraph === 'function' ) {
192
+ const fnName = config . supergraph . name || '' ;
193
+ supergraphLoadedPlace = `a custom loader ${ fnName } ` ;
194
+ }
195
+ }
196
+ let loaded = false ;
197
+ let loadError : Error ;
198
+ let transportEntryMap : Record < string , TransportEntry > ;
199
+ try {
200
+ transportEntryMap = await unifiedGraphManager . getTransportEntryMap ( ) ;
201
+ loaded = true ;
202
+ } catch ( e ) {
203
+ loaded = false ;
204
+ loadError = e ;
205
+ }
206
+ if ( loaded ) {
207
+ htmlParts . push ( `<h3>Supergraph Status: Loaded ✅</h3>` ) ;
208
+ htmlParts . push ( `<p><strong>Source: </strong> <i>${ supergraphLoadedPlace } </i></p>` ) ;
209
+ htmlParts . push ( `<table>` ) ;
210
+ htmlParts . push ( `<tr><th>Subgraph</th><th>Transport</th><th>Location</th></tr>` ) ;
211
+ for ( const subgraphName in transportEntryMap ) {
212
+ const transportEntry = transportEntryMap [ subgraphName ] ;
213
+ htmlParts . push ( `<tr>` ) ;
214
+ htmlParts . push ( `<td>${ subgraphName } </td>` ) ;
215
+ htmlParts . push ( `<td>${ transportEntry . kind } </td>` ) ;
216
+ htmlParts . push (
217
+ `<td><a href="${ transportEntry . location } ">${ transportEntry . location } </a></td>` ,
218
+ ) ;
219
+ htmlParts . push ( `</tr>` ) ;
220
+ }
221
+ htmlParts . push ( `</table>` ) ;
222
+ } else {
223
+ htmlParts . push ( `<h3>Status: Failed ❌</h3>` ) ;
224
+ htmlParts . push ( `<p><strong>Source: </strong> <i>${ supergraphLoadedPlace } </i></p>` ) ;
225
+ htmlParts . push ( `<h3>Error:</h3>` ) ;
226
+ htmlParts . push ( `<pre>${ loadError . stack } </pre>` ) ;
227
+ }
228
+ return `<section class="supergraph-information">${ htmlParts . join ( '' ) } </section>` ;
229
+ } ;
177
230
}
178
231
179
232
const readinessCheckPlugin = useReadinessCheck ( {
@@ -244,6 +297,30 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
244
297
} ;
245
298
}
246
299
300
+ let landingPageRenderer : LandingPageRenderer | boolean ;
301
+
302
+ if ( config . landingPage == null || config . landingPage === true ) {
303
+ landingPageRenderer = async function meshLandingPageRenderer ( opts ) {
304
+ return new opts . fetchAPI . Response (
305
+ landingPageHtml
306
+ . replace ( / _ _ G R A P H I Q L _ L I N K _ _ / g, opts . graphqlEndpoint )
307
+ . replace ( / _ _ R E Q U E S T _ P A T H _ _ / g, opts . url . pathname )
308
+ . replace ( / _ _ S U B G R A P H _ H T M L _ _ / g, await subgraphInformationHTMLRenderer ( ) ) ,
309
+ {
310
+ status : 200 ,
311
+ statusText : 'OK' ,
312
+ headers : {
313
+ 'Content-Type' : 'text/html' ,
314
+ } ,
315
+ } ,
316
+ ) ;
317
+ } ;
318
+ } else if ( typeof config . landingPage === 'function' ) {
319
+ landingPageRenderer = config . landingPage ;
320
+ } else if ( config . landingPage === false ) {
321
+ landingPageRenderer = false ;
322
+ }
323
+
247
324
const yoga = createYoga < unknown , MeshServeContext > ( {
248
325
// @ts -expect-error PromiseLike is not compatible with Promise
249
326
schema : schemaFetcher ,
@@ -289,7 +366,7 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
289
366
graphqlEndpoint : config . graphqlEndpoint ,
290
367
maskedErrors : config . maskedErrors ,
291
368
healthCheckEndpoint : config . healthCheckEndpoint || '/healthcheck' ,
292
- landingPage : config . landingPage ,
369
+ landingPage : landingPageRenderer ,
293
370
} ) ;
294
371
295
372
fetchAPI ||= yoga . fetchAPI ;
0 commit comments