apollo-server-core
Version:
Core engine for Apollo GraphQL server
256 lines (241 loc) • 8.12 kB
text/typescript
import type { ImplicitlyInstallablePlugin } from '../../../ApolloServer';
import type {
ApolloServerPluginEmbeddedLandingPageProductionDefaultOptions,
ApolloServerPluginLandingPageLocalDefaultOptions,
ApolloServerPluginLandingPageProductionDefaultOptions,
LandingPageConfig,
} from './types';
export function ApolloServerPluginLandingPageLocalDefault(
options: ApolloServerPluginLandingPageLocalDefaultOptions = {},
): ImplicitlyInstallablePlugin {
const { version, __internal_apolloStudioEnv__, ...rest } = options;
return ApolloServerPluginLandingPageDefault(version, {
isProd: false,
apolloStudioEnv: __internal_apolloStudioEnv__,
...rest,
});
}
export function ApolloServerPluginLandingPageProductionDefault(
options: ApolloServerPluginLandingPageProductionDefaultOptions = {},
): ImplicitlyInstallablePlugin {
const { version, __internal_apolloStudioEnv__, ...rest } = options;
return ApolloServerPluginLandingPageDefault(version, {
isProd: true,
apolloStudioEnv: __internal_apolloStudioEnv__,
...rest,
});
}
// A triple encoding! Wow! First we use JSON.stringify to turn our object into a
// string. Then we encodeURIComponent so we don't have to stress about what
// would happen if the config contained `</script>`. Finally, we JSON.stringify
// it again, which in practice just wraps it in a pair of double quotes (since
// there shouldn't be any backslashes left after encodeURIComponent). The
// consumer of this needs to decodeURIComponent and then JSON.parse; there's
// only one JSON.parse because the outermost JSON string is parsed by the JS
// parser itself.
function encodeConfig(config: LandingPageConfig): string {
return JSON.stringify(encodeURIComponent(JSON.stringify(config)));
}
// This function turns an object into a string and replaces
// <, >, &, ' with their unicode chars to avoid adding html tags to
// the landing page html that might be passed from the config.
// The only place these characters can appear in the output of
// JSON.stringify is within string literals, where they can equally
// well appear \u-escaped. This specifically means that
// `</script>` won't terminate the script block early.
// (Perhaps we should have done this instead of the triple-encoding
// of encodeConfig for the main landing page.)
function getConfigStringForHtml(config: LandingPageConfig) {
return JSON.stringify(config)
.replace('<', '\\u003c')
.replace('>', '\\u003e')
.replace('&', '\\u0026')
.replace("'", '\\u0027');
}
export const getEmbeddedExplorerHTML = (
version: string,
config: ApolloServerPluginEmbeddedLandingPageProductionDefaultOptions,
) => {
interface EmbeddableExplorerOptions {
graphRef: string;
target: string;
initialState?: {
document?: string;
variables?: Record<string, any>;
headers?: Record<string, string>;
displayOptions: {
docsPanelState?: 'open' | 'closed'; // default to 'open',
showHeadersAndEnvVars?: boolean; // default to `false`
theme?: 'dark' | 'light';
};
};
persistExplorerState?: boolean; // defaults to 'false'
endpointUrl: string;
includeCookies?: boolean; // defaults to 'false'
}
const productionLandingPageConfigOrDefault = {
displayOptions: {},
persistExplorerState: false,
...(typeof config.embed === 'boolean' ? {} : config.embed),
};
const embeddedExplorerParams: Omit<EmbeddableExplorerOptions, 'endpointUrl'> =
{
...config,
target: '#embeddableExplorer',
initialState: {
...config,
displayOptions: {
...productionLandingPageConfigOrDefault.displayOptions,
},
},
persistExplorerState:
productionLandingPageConfigOrDefault.persistExplorerState,
};
return `
<div class="fallback">
<h1>Welcome to Apollo Server</h1>
<p>Apollo Explorer cannot be loaded; it appears that you might be offline.</p>
</div>
<style>
iframe {
background-color: white;
}
</style>
<div
style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableExplorer"
></div>
<script src="https://embeddable-explorer.cdn.apollographql.com/${version}/embeddable-explorer.umd.production.min.js"></script>
<script>
var endpointUrl = window.location.href;
var embeddedExplorerConfig = ${getConfigStringForHtml(
embeddedExplorerParams,
)};
new window.EmbeddedExplorer({
...embeddedExplorerConfig,
endpointUrl,
});
</script>
`;
};
export const getEmbeddedSandboxHTML = (
version: string,
config: LandingPageConfig,
) => {
return `
<div class="fallback">
<h1>Welcome to Apollo Server</h1>
<p>Apollo Sandbox cannot be loaded; it appears that you might be offline.</p>
</div>
<style>
iframe {
background-color: white;
}
</style>
<div
style="width: 100vw; height: 100vh; position: absolute; top: 0;"
id="embeddableSandbox"
></div>
<script src="https://embeddable-sandbox.cdn.apollographql.com/${version}/embeddable-sandbox.umd.production.min.js"></script>
<script>
var initialEndpoint = window.location.href;
new window.EmbeddedSandbox({
target: '#embeddableSandbox',
initialEndpoint,
includeCookies: ${config.includeCookies ?? 'false'},
initialState: ${getConfigStringForHtml({
document: config.document ?? undefined,
variables: config.variables ?? undefined,
headers: config.headers ?? undefined,
})},
});
</script>
`;
};
const getNonEmbeddedLandingPageHTML = (
version: string,
config: LandingPageConfig,
) => {
const encodedConfig = encodeConfig(config);
return `
<div class="fallback">
<h1>Welcome to Apollo Server</h1>
<p>The full landing page cannot be loaded; it appears that you might be offline.</p>
</div>
<script>window.landingPage = ${encodedConfig};</script>
<script src="https://apollo-server-landing-page.cdn.apollographql.com/${version}/static/js/main.js"></script>`;
};
// Helper for the two actual plugin functions.
function ApolloServerPluginLandingPageDefault(
maybeVersion: string | undefined,
config: LandingPageConfig & {
isProd: boolean;
apolloStudioEnv: 'staging' | 'prod' | undefined;
},
): ImplicitlyInstallablePlugin {
const version = maybeVersion ?? '_latest';
return {
__internal_installed_implicitly__: false,
async serverWillStart() {
return {
async renderLandingPage() {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="https://apollo-server-landing-page.cdn.apollographql.com/${version}/assets/favicon.png"
/>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap"
rel="stylesheet"
/>
<meta name="theme-color" content="#000000" />
<meta name="description" content="Apollo server landing page" />
<link
rel="apple-touch-icon"
href="https://apollo-server-landing-page.cdn.apollographql.com/${version}/assets/favicon.png"
/>
<link
rel="manifest"
href="https://apollo-server-landing-page.cdn.apollographql.com/${version}/manifest.json"
/>
<title>Apollo Server</title>
</head>
<body style="margin: 0; overflow-x: hidden; overflow-y: hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="react-root">
<style>
.fallback {
opacity: 0;
animation: fadeIn 1s 1s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
padding: 1em;
}
@keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1; }
}
</style>
${
config.embed
? 'graphRef' in config && config.graphRef
? getEmbeddedExplorerHTML(version, config)
: getEmbeddedSandboxHTML(version, config)
: getNonEmbeddedLandingPageHTML(version, config)
}
</div>
</body>
</html>
`;
return { html };
},
};
},
};
}