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