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