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