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