1212e3a1aSEric Samelsonimport { getConfig, getNameFromConfig } from '@expo/config';
2*f0d67e12SMateus Craveiroimport { getRuntimeVersionNullableAsync } from '@expo/config-plugins/build/utils/Updates';
38d307f52SEvan Baconimport { readFile } from 'fs/promises';
48d307f52SEvan Baconimport path from 'path';
58d307f52SEvan Baconimport resolveFrom from 'resolve-from';
68d307f52SEvan Bacon
78d307f52SEvan Baconimport { disableResponseCache, ExpoMiddleware } from './ExpoMiddleware';
829975bfdSEvan Baconimport {
929975bfdSEvan Bacon  assertMissingRuntimePlatform,
1029975bfdSEvan Bacon  assertRuntimePlatform,
1129975bfdSEvan Bacon  parsePlatformHeader,
12212e3a1aSEric Samelson  resolvePlatformFromUserAgentHeader,
1329975bfdSEvan Bacon  RuntimePlatform,
1429975bfdSEvan Bacon} from './resolvePlatform';
158d307f52SEvan Baconimport { ServerRequest, ServerResponse } from './server.types';
168d307f52SEvan Bacon
17212e3a1aSEric Samelsontype ProjectVersion = {
18212e3a1aSEric Samelson  type: 'sdk' | 'runtime';
19212e3a1aSEric Samelson  version: string | null;
20212e3a1aSEric Samelson};
21212e3a1aSEric Samelson
22474a7a4bSEvan Baconconst debug = require('debug')(
23474a7a4bSEvan Bacon  'expo:start:server:middleware:interstitialPage'
24474a7a4bSEvan Bacon) as typeof console.log;
25474a7a4bSEvan Bacon
268d307f52SEvan Baconexport const LoadingEndpoint = '/_expo/loading';
278d307f52SEvan Bacon
288d307f52SEvan Baconexport class InterstitialPageMiddleware extends ExpoMiddleware {
29212e3a1aSEric Samelson  constructor(
30212e3a1aSEric Samelson    projectRoot: string,
31212e3a1aSEric Samelson    protected options: { scheme: string | null } = { scheme: null }
32212e3a1aSEric Samelson  ) {
338d307f52SEvan Bacon    super(projectRoot, [LoadingEndpoint]);
348d307f52SEvan Bacon  }
358d307f52SEvan Bacon
368d307f52SEvan Bacon  /** Get the template HTML page and inject values. */
378d307f52SEvan Bacon  async _getPageAsync({
388d307f52SEvan Bacon    appName,
39212e3a1aSEric Samelson    projectVersion,
408d307f52SEvan Bacon  }: {
418d307f52SEvan Bacon    appName: string;
42212e3a1aSEric Samelson    projectVersion: ProjectVersion;
438d307f52SEvan Bacon  }): Promise<string> {
448d307f52SEvan Bacon    const templatePath =
458d307f52SEvan Bacon      // Production: This will resolve when installed in the project.
468d307f52SEvan Bacon      resolveFrom.silent(this.projectRoot, 'expo/static/loading-page/index.html') ??
478d307f52SEvan Bacon      // Development: This will resolve when testing locally.
488d307f52SEvan Bacon      path.resolve(__dirname, '../../../../../static/loading-page/index.html');
498d307f52SEvan Bacon    let content = (await readFile(templatePath)).toString('utf-8');
508d307f52SEvan Bacon
5129975bfdSEvan Bacon    content = content.replace(/{{\s*AppName\s*}}/, appName);
528d307f52SEvan Bacon    content = content.replace(/{{\s*Path\s*}}/, this.projectRoot);
53212e3a1aSEric Samelson    content = content.replace(/{{\s*Scheme\s*}}/, this.options.scheme ?? 'Unknown');
54212e3a1aSEric Samelson    content = content.replace(
55212e3a1aSEric Samelson      /{{\s*ProjectVersionType\s*}}/,
56212e3a1aSEric Samelson      `${projectVersion.type === 'sdk' ? 'SDK' : 'Runtime'} version`
57212e3a1aSEric Samelson    );
58212e3a1aSEric Samelson    content = content.replace(/{{\s*ProjectVersion\s*}}/, projectVersion.version ?? 'Undetected');
598d307f52SEvan Bacon
608d307f52SEvan Bacon    return content;
618d307f52SEvan Bacon  }
628d307f52SEvan Bacon
638d307f52SEvan Bacon  /** Get settings for the page from the project config. */
64*f0d67e12SMateus Craveiro  async _getProjectOptionsAsync(platform: RuntimePlatform): Promise<{
658d307f52SEvan Bacon    appName: string;
66212e3a1aSEric Samelson    projectVersion: ProjectVersion;
67*f0d67e12SMateus Craveiro  }> {
688d307f52SEvan Bacon    assertRuntimePlatform(platform);
698d307f52SEvan Bacon
708d307f52SEvan Bacon    const { exp } = getConfig(this.projectRoot);
718d307f52SEvan Bacon    const { appName } = getNameFromConfig(exp);
72*f0d67e12SMateus Craveiro    const runtimeVersion = await getRuntimeVersionNullableAsync(this.projectRoot, exp, platform);
73212e3a1aSEric Samelson    const sdkVersion = exp.sdkVersion ?? null;
748d307f52SEvan Bacon
758d307f52SEvan Bacon    return {
7629975bfdSEvan Bacon      appName: appName ?? 'App',
77212e3a1aSEric Samelson      projectVersion:
78212e3a1aSEric Samelson        sdkVersion && !runtimeVersion
79212e3a1aSEric Samelson          ? { type: 'sdk', version: sdkVersion }
80212e3a1aSEric Samelson          : { type: 'runtime', version: runtimeVersion },
818d307f52SEvan Bacon    };
828d307f52SEvan Bacon  }
838d307f52SEvan Bacon
848d307f52SEvan Bacon  async handleRequestAsync(req: ServerRequest, res: ServerResponse): Promise<void> {
858d307f52SEvan Bacon    res = disableResponseCache(res);
868d307f52SEvan Bacon    res.setHeader('Content-Type', 'text/html');
878d307f52SEvan Bacon
88212e3a1aSEric Samelson    const platform = parsePlatformHeader(req) ?? resolvePlatformFromUserAgentHeader(req);
8929975bfdSEvan Bacon    assertMissingRuntimePlatform(platform);
908d307f52SEvan Bacon    assertRuntimePlatform(platform);
918d307f52SEvan Bacon
92*f0d67e12SMateus Craveiro    const { appName, projectVersion } = await this._getProjectOptionsAsync(platform);
93474a7a4bSEvan Bacon    debug(
94212e3a1aSEric Samelson      `Create loading page. (platform: ${platform}, appName: ${appName}, projectVersion: ${projectVersion.version}, type: ${projectVersion.type})`
95474a7a4bSEvan Bacon    );
96212e3a1aSEric Samelson    const content = await this._getPageAsync({ appName, projectVersion });
978d307f52SEvan Bacon    res.end(content);
988d307f52SEvan Bacon  }
998d307f52SEvan Bacon}
100