1import { ExpoConfig, getConfig, getNameFromConfig } from '@expo/config'; 2import fs from 'fs'; 3import path from 'path'; 4 5import { TEMPLATES } from '../../customize/templates'; 6import { appendLinkToHtml, appendScriptsToHtml } from '../../export/html'; 7import { env } from '../../utils/env'; 8 9/** 10 * Create a static HTML for SPA styled websites. 11 * This method attempts to reuse the same patterns as `@expo/webpack-config`. 12 */ 13export async function createTemplateHtmlFromExpoConfigAsync( 14 projectRoot: string, 15 { 16 scripts, 17 cssLinks, 18 exp = getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp, 19 }: { 20 scripts: string[]; 21 cssLinks?: string[]; 22 exp?: ExpoConfig; 23 } 24) { 25 return createTemplateHtmlAsync(projectRoot, { 26 langIsoCode: exp.web?.lang ?? 'en', 27 scripts, 28 cssLinks, 29 title: getNameFromConfig(exp).webName ?? 'Expo App', 30 description: exp.web?.description, 31 themeColor: exp.web?.themeColor, 32 }); 33} 34 35function getFileFromLocalPublicFolder( 36 projectRoot: string, 37 { publicFolder, filePath }: { publicFolder: string; filePath: string } 38): string | null { 39 const localFilePath = path.resolve(projectRoot, publicFolder, filePath); 40 if (!fs.existsSync(localFilePath)) { 41 return null; 42 } 43 return localFilePath; 44} 45 46/** Attempt to read the `index.html` from the local project before falling back on the template `index.html`. */ 47async function getTemplateIndexHtmlAsync(projectRoot: string): Promise<string> { 48 let filePath = getFileFromLocalPublicFolder(projectRoot, { 49 // TODO: Maybe use the app.json override. 50 publicFolder: env.EXPO_PUBLIC_FOLDER, 51 filePath: 'index.html', 52 }); 53 if (!filePath) { 54 filePath = TEMPLATES.find((value) => value.id === 'index.html')!.file(projectRoot); 55 } 56 return fs.promises.readFile(filePath, 'utf8'); 57} 58 59/** Return an `index.html` string with template values added. */ 60export async function createTemplateHtmlAsync( 61 projectRoot: string, 62 { 63 scripts, 64 cssLinks, 65 description, 66 langIsoCode, 67 title, 68 themeColor, 69 }: { 70 scripts: string[]; 71 cssLinks?: string[]; 72 description?: string; 73 langIsoCode: string; 74 title: string; 75 themeColor?: string; 76 } 77): Promise<string> { 78 // Resolve the best possible index.html template file. 79 let contents = await getTemplateIndexHtmlAsync(projectRoot); 80 81 contents = contents.replace('%LANG_ISO_CODE%', langIsoCode); 82 contents = contents.replace('%WEB_TITLE%', title); 83 84 contents = appendScriptsToHtml(contents, scripts); 85 86 if (cssLinks) { 87 contents = appendLinkToHtml( 88 contents, 89 cssLinks 90 .map((href) => [ 91 // NOTE: We probably don't have to preload the CSS files for SPA-styled websites. 92 { 93 as: 'style', 94 rel: 'preload', 95 href, 96 }, 97 { 98 rel: 'stylesheet', 99 href, 100 }, 101 ]) 102 .flat() 103 ); 104 } 105 106 if (themeColor) { 107 contents = addMeta(contents, `name="theme-color" content="${themeColor}"`); 108 } 109 110 if (description) { 111 contents = addMeta(contents, `name="description" content="${description}"`); 112 } 113 114 return contents; 115} 116 117/** Add a `<meta />` tag to the `<head />` element. */ 118function addMeta(contents: string, meta: string): string { 119 return contents.replace('</head>', `<meta ${meta}>\n</head>`); 120} 121