1import { ExpoConfig, getConfig, getNameFromConfig } from '@expo/config';
2import { getRuntimeVersionNullable } from '@expo/config-plugins/build/utils/Updates';
3import { readFile } from 'fs/promises';
4import path from 'path';
5import resolveFrom from 'resolve-from';
6
7import { disableResponseCache, ExpoMiddleware } from './ExpoMiddleware';
8import {
9  assertMissingRuntimePlatform,
10  assertRuntimePlatform,
11  parsePlatformHeader,
12  RuntimePlatform,
13} from './resolvePlatform';
14import { ServerRequest, ServerResponse } from './server.types';
15
16export const LoadingEndpoint = '/_expo/loading';
17
18function getRuntimeVersion(exp: ExpoConfig, platform: 'android' | 'ios' | null): string {
19  if (!platform) {
20    return 'Undetected';
21  }
22
23  return getRuntimeVersionNullable(exp, platform) ?? 'Undetected';
24}
25
26export class InterstitialPageMiddleware extends ExpoMiddleware {
27  constructor(projectRoot: string) {
28    super(projectRoot, [LoadingEndpoint]);
29  }
30
31  /** Get the template HTML page and inject values. */
32  async _getPageAsync({
33    appName,
34    runtimeVersion,
35  }: {
36    appName: string;
37    runtimeVersion: string | null;
38  }): Promise<string> {
39    const templatePath =
40      // Production: This will resolve when installed in the project.
41      resolveFrom.silent(this.projectRoot, 'expo/static/loading-page/index.html') ??
42      // Development: This will resolve when testing locally.
43      path.resolve(__dirname, '../../../../../static/loading-page/index.html');
44    let content = (await readFile(templatePath)).toString('utf-8');
45
46    content = content.replace(/{{\s*AppName\s*}}/, appName);
47    content = content.replace(/{{\s*RuntimeVersion\s*}}/, runtimeVersion ?? '');
48    content = content.replace(/{{\s*Path\s*}}/, this.projectRoot);
49
50    return content;
51  }
52
53  /** Get settings for the page from the project config. */
54  _getProjectOptions(platform: RuntimePlatform): {
55    appName: string;
56    runtimeVersion: string | null;
57  } {
58    assertRuntimePlatform(platform);
59
60    const { exp } = getConfig(this.projectRoot);
61    const { appName } = getNameFromConfig(exp);
62    const runtimeVersion = getRuntimeVersion(exp, platform);
63
64    return {
65      appName: appName ?? 'App',
66      runtimeVersion,
67    };
68  }
69
70  async handleRequestAsync(req: ServerRequest, res: ServerResponse): Promise<void> {
71    res = disableResponseCache(res);
72    res.setHeader('Content-Type', 'text/html');
73
74    const platform = parsePlatformHeader(req);
75    assertMissingRuntimePlatform(platform);
76    assertRuntimePlatform(platform);
77
78    const { appName, runtimeVersion } = this._getProjectOptions(platform);
79    const content = await this._getPageAsync({ appName, runtimeVersion });
80    res.end(content);
81  }
82}
83