Skip to content

Commit 1ffb3f1

Browse files
committed
feat(serve-runtime): new landing page (ardatan#7170)
1 parent a673114 commit 1ffb3f1

File tree

10 files changed

+371
-4
lines changed

10 files changed

+371
-4
lines changed

.github/workflows/loadtest.yml

+2
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ jobs:
2222
with:
2323
nodeVersion: ${{matrix.node-version}}
2424
packageManagerVersion: modern
25+
- name: Prepare for build
26+
run: yarn prebuild
2527
- name: loadtest
2628
run: yarn loadtest:e2e

.github/workflows/tests.yml

+11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
name: setup env
3232
with:
3333
packageManagerVersion: modern
34+
35+
- name: Prepare for build
36+
run: yarn prebuild
3437
- name: typecheck
3538
run: yarn typecheck
3639

@@ -67,6 +70,9 @@ jobs:
6770
- name: Generate config schema
6871
run: yarn generate-config-schema
6972

73+
- name: Prepare for build
74+
run: yarn prebuild
75+
7076
- name: Run Tests
7177
uses: nick-fields/retry@v3
7278
with:
@@ -113,6 +119,9 @@ jobs:
113119
- name: Remove node-libcurl
114120
run: rm -rf node_modules/node-libcurl
115121

122+
- name: Prepare for build
123+
run: yarn prebuild
124+
116125
- name: Run Tests
117126
uses: nick-fields/retry@v3
118127
with:
@@ -340,6 +349,8 @@ jobs:
340349
with:
341350
nodeVersion: ${{matrix.node-version}}
342351
packageManagerVersion: modern
352+
- name: Prepare for build
353+
run: yarn prebuild
343354
- name: test
344355
timeout-minutes: 5
345356
run: yarn test:e2e --detectOpenHandles

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"postchangeset": "yarn install ---no-frozen-lockfile",
5252
"postgenerate-config-schema": "node scripts/create-config-schema-ts.js && npx prettier --write ./packages/legacy/types/src",
5353
"postinstall": "husky install",
54-
"prebuild": "yarn clean && yarn generate-config-schema",
54+
"prebuild": "yarn clean && yarn generate-config-schema && yarn workspace @graphql-mesh/serve-runtime generate-landing-page-html",
5555
"prettier": "prettier --write --list-different .",
5656
"prettier:check": "prettier --check .",
5757
"release": "yarn build && changeset publish",

packages/fusion/runtime/src/unifiedGraphManager.ts

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class UnifiedGraphManager<TContext> {
7474
private inContextSDK;
7575
private initialUnifiedGraph$: MaybePromise<void>;
7676
private disposableStack = new AsyncDisposableStack();
77+
private _transportEntryMap: Record<string, TransportEntry>;
7778
constructor(private opts: UnifiedGraphManagerOptions<TContext>) {
7879
this.handleUnifiedGraph = opts.handleUnifiedGraph || handleFederationSupergraph;
7980
this.onSubgraphExecuteHooks = opts?.onSubgraphExecuteHooks || [];
@@ -169,10 +170,15 @@ export class UnifiedGraphManager<TContext> {
169170
);
170171
}
171172
this.continuePolling();
173+
this._transportEntryMap = transportEntryMap;
172174
},
173175
);
174176
}
175177

178+
public getTransportEntryMap() {
179+
return mapMaybePromise(this.ensureUnifiedGraph(), () => this._transportEntryMap);
180+
}
181+
176182
public getUnifiedGraph() {
177183
return mapMaybePromise(this.ensureUnifiedGraph(), () => this.unifiedGraph);
178184
}

packages/serve-runtime/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
landing-page-html.ts

packages/serve-runtime/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"./package.json": "./package.json"
3232
},
3333
"typings": "dist/typings/index.d.ts",
34+
"scripts": {
35+
"generate-landing-page-html": "node scripts/generate-landing-page-html.js"
36+
},
3437
"peerDependencies": {
3538
"graphql": "*"
3639
},
@@ -53,6 +56,9 @@
5356
"disposablestack": "^1.1.6",
5457
"graphql-yoga": "^5.6.0"
5558
},
59+
"devDependencies": {
60+
"html-minifier-terser": "7.2.0"
61+
},
5662
"publishConfig": {
5763
"access": "public",
5864
"directory": "dist"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { minify as minifyT } from 'html-minifier-terser';
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
8+
async function minify(str) {
9+
return (
10+
await minifyT(str, {
11+
minifyJS: true,
12+
useShortDoctype: false,
13+
removeAttributeQuotes: true,
14+
collapseWhitespace: true,
15+
minifyCSS: true,
16+
})
17+
).toString('utf-8');
18+
}
19+
20+
async function minifyLandingPageHTML() {
21+
const minified = await minify(
22+
fs.readFileSync(path.join(__dirname, '..', 'src', 'landing-page.html'), 'utf-8'),
23+
);
24+
25+
await fs.promises.writeFile(
26+
path.join(__dirname, '../src/landing-page-html.ts'),
27+
`export default ${JSON.stringify(minified)}`,
28+
);
29+
}
30+
31+
minifyLandingPageHTML().catch(err => {
32+
console.error(err);
33+
process.exit(1);
34+
});

packages/serve-runtime/src/createServeRuntime.ts

+78-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
FetchAPI,
88
GraphiQLOptions,
99
isAsyncIterable,
10+
LandingPageRenderer,
1011
useReadinessCheck,
1112
YogaServerInstance,
1213
type Plugin,
@@ -18,6 +19,7 @@ import {
1819
handleFederationSupergraph,
1920
isDisposable,
2021
OnSubgraphExecuteHook,
22+
TransportEntry,
2123
UnifiedGraphManager,
2224
UnifiedGraphManagerOptions,
2325
} from '@graphql-mesh/fusion-runtime';
@@ -35,6 +37,7 @@ import { useExecutor } from '@graphql-tools/executor-yoga';
3537
import { MaybePromise } from '@graphql-tools/utils';
3638
import { getProxyExecutor } from './getProxyExecutor.js';
3739
import { handleUnifiedGraphConfig } from './handleUnifiedGraphConfig.js';
40+
import landingPageHtml from './landing-page-html.js';
3841
import {
3942
MeshServeConfig,
4043
MeshServeConfigContext,
@@ -81,6 +84,7 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
8184
let contextBuilder: <T>(context: T) => MaybePromise<T>;
8285
let readinessChecker: () => MaybePromise<boolean>;
8386
let registryPlugin: MeshPlugin<unknown> = {};
87+
let subgraphInformationHTMLRenderer: () => MaybePromise<string> = () => '';
8488

8589
const disposableStack = new AsyncDisposableStack();
8690

@@ -104,6 +108,10 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
104108
return mapMaybePromise(res$, res => !isAsyncIterable(res) && !!res.data?.__typename);
105109
};
106110
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+
};
107115
} else {
108116
let unifiedGraphFetcher: UnifiedGraphManagerOptions<unknown>['getUnifiedGraph'];
109117

@@ -174,6 +182,51 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
174182
schemaInvalidator = () => unifiedGraphManager.invalidateUnifiedGraph();
175183
contextBuilder = base => unifiedGraphManager.getContext(base);
176184
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+
};
177230
}
178231

179232
const readinessCheckPlugin = useReadinessCheck({
@@ -244,6 +297,30 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
244297
};
245298
}
246299

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(/__GRAPHIQL_LINK__/g, opts.graphqlEndpoint)
307+
.replace(/__REQUEST_PATH__/g, opts.url.pathname)
308+
.replace(/__SUBGRAPH_HTML__/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+
247324
const yoga = createYoga<unknown, MeshServeContext>({
248325
// @ts-expect-error PromiseLike is not compatible with Promise
249326
schema: schemaFetcher,
@@ -289,7 +366,7 @@ export function createServeRuntime<TContext extends Record<string, any> = Record
289366
graphqlEndpoint: config.graphqlEndpoint,
290367
maskedErrors: config.maskedErrors,
291368
healthCheckEndpoint: config.healthCheckEndpoint || '/healthcheck',
292-
landingPage: config.landingPage,
369+
landingPage: landingPageRenderer,
293370
});
294371

295372
fetchAPI ||= yoga.fetchAPI;

0 commit comments

Comments
 (0)