16d6b81f9SEvan Baconimport { ExpoConfig, getConfig, getNameFromConfig } from '@expo/config'; 26d6b81f9SEvan Baconimport fs from 'fs'; 36d6b81f9SEvan Baconimport path from 'path'; 46d6b81f9SEvan Bacon 56d6b81f9SEvan Baconimport { TEMPLATES } from '../../customize/templates'; 6*9b2597baSEvan Baconimport { appendLinkToHtml, appendScriptsToHtml } from '../../export/html'; 76d6b81f9SEvan Baconimport { env } from '../../utils/env'; 86d6b81f9SEvan Bacon 96d6b81f9SEvan Bacon/** 106d6b81f9SEvan Bacon * Create a static HTML for SPA styled websites. 116d6b81f9SEvan Bacon * This method attempts to reuse the same patterns as `@expo/webpack-config`. 126d6b81f9SEvan Bacon */ 136d6b81f9SEvan Baconexport async function createTemplateHtmlFromExpoConfigAsync( 146d6b81f9SEvan Bacon projectRoot: string, 156d6b81f9SEvan Bacon { 166d6b81f9SEvan Bacon scripts, 17*9b2597baSEvan Bacon cssLinks, 186d6b81f9SEvan Bacon exp = getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp, 196d6b81f9SEvan Bacon }: { 206d6b81f9SEvan Bacon scripts: string[]; 21*9b2597baSEvan Bacon cssLinks?: string[]; 226d6b81f9SEvan Bacon exp?: ExpoConfig; 236d6b81f9SEvan Bacon } 246d6b81f9SEvan Bacon) { 256d6b81f9SEvan Bacon return createTemplateHtmlAsync(projectRoot, { 266d6b81f9SEvan Bacon langIsoCode: exp.web?.lang ?? 'en', 276d6b81f9SEvan Bacon scripts, 28*9b2597baSEvan Bacon cssLinks, 296d6b81f9SEvan Bacon title: getNameFromConfig(exp).webName ?? 'Expo App', 306d6b81f9SEvan Bacon description: exp.web?.description, 316d6b81f9SEvan Bacon themeColor: exp.web?.themeColor, 326d6b81f9SEvan Bacon }); 336d6b81f9SEvan Bacon} 346d6b81f9SEvan Bacon 356d6b81f9SEvan Baconfunction getFileFromLocalPublicFolder( 366d6b81f9SEvan Bacon projectRoot: string, 376d6b81f9SEvan Bacon { publicFolder, filePath }: { publicFolder: string; filePath: string } 386d6b81f9SEvan Bacon): string | null { 396d6b81f9SEvan Bacon const localFilePath = path.resolve(projectRoot, publicFolder, filePath); 406d6b81f9SEvan Bacon if (!fs.existsSync(localFilePath)) { 416d6b81f9SEvan Bacon return null; 426d6b81f9SEvan Bacon } 436d6b81f9SEvan Bacon return localFilePath; 446d6b81f9SEvan Bacon} 456d6b81f9SEvan Bacon 466d6b81f9SEvan Bacon/** Attempt to read the `index.html` from the local project before falling back on the template `index.html`. */ 476d6b81f9SEvan Baconasync function getTemplateIndexHtmlAsync(projectRoot: string): Promise<string> { 486d6b81f9SEvan Bacon let filePath = getFileFromLocalPublicFolder(projectRoot, { 496d6b81f9SEvan Bacon // TODO: Maybe use the app.json override. 506d6b81f9SEvan Bacon publicFolder: env.EXPO_PUBLIC_FOLDER, 516d6b81f9SEvan Bacon filePath: 'index.html', 526d6b81f9SEvan Bacon }); 536d6b81f9SEvan Bacon if (!filePath) { 546d6b81f9SEvan Bacon filePath = TEMPLATES.find((value) => value.id === 'index.html')!.file(projectRoot); 556d6b81f9SEvan Bacon } 566d6b81f9SEvan Bacon return fs.promises.readFile(filePath, 'utf8'); 576d6b81f9SEvan Bacon} 586d6b81f9SEvan Bacon 596d6b81f9SEvan Bacon/** Return an `index.html` string with template values added. */ 606d6b81f9SEvan Baconexport async function createTemplateHtmlAsync( 616d6b81f9SEvan Bacon projectRoot: string, 626d6b81f9SEvan Bacon { 636d6b81f9SEvan Bacon scripts, 64*9b2597baSEvan Bacon cssLinks, 656d6b81f9SEvan Bacon description, 666d6b81f9SEvan Bacon langIsoCode, 676d6b81f9SEvan Bacon title, 686d6b81f9SEvan Bacon themeColor, 696d6b81f9SEvan Bacon }: { 706d6b81f9SEvan Bacon scripts: string[]; 71*9b2597baSEvan Bacon cssLinks?: string[]; 726d6b81f9SEvan Bacon description?: string; 736d6b81f9SEvan Bacon langIsoCode: string; 746d6b81f9SEvan Bacon title: string; 756d6b81f9SEvan Bacon themeColor?: string; 766d6b81f9SEvan Bacon } 776d6b81f9SEvan Bacon): Promise<string> { 786d6b81f9SEvan Bacon // Resolve the best possible index.html template file. 796d6b81f9SEvan Bacon let contents = await getTemplateIndexHtmlAsync(projectRoot); 806d6b81f9SEvan Bacon 816d6b81f9SEvan Bacon contents = contents.replace('%LANG_ISO_CODE%', langIsoCode); 826d6b81f9SEvan Bacon contents = contents.replace('%WEB_TITLE%', title); 83*9b2597baSEvan Bacon 84*9b2597baSEvan Bacon contents = appendScriptsToHtml(contents, scripts); 85*9b2597baSEvan Bacon 86*9b2597baSEvan Bacon if (cssLinks) { 87*9b2597baSEvan Bacon contents = appendLinkToHtml( 88*9b2597baSEvan Bacon contents, 89*9b2597baSEvan Bacon cssLinks 90*9b2597baSEvan Bacon .map((href) => [ 91*9b2597baSEvan Bacon // NOTE: We probably don't have to preload the CSS files for SPA-styled websites. 92*9b2597baSEvan Bacon { 93*9b2597baSEvan Bacon as: 'style', 94*9b2597baSEvan Bacon rel: 'preload', 95*9b2597baSEvan Bacon href, 96*9b2597baSEvan Bacon }, 97*9b2597baSEvan Bacon { 98*9b2597baSEvan Bacon rel: 'stylesheet', 99*9b2597baSEvan Bacon href, 100*9b2597baSEvan Bacon }, 101*9b2597baSEvan Bacon ]) 102*9b2597baSEvan Bacon .flat() 1036d6b81f9SEvan Bacon ); 104*9b2597baSEvan Bacon } 1056d6b81f9SEvan Bacon 1066d6b81f9SEvan Bacon if (themeColor) { 1076d6b81f9SEvan Bacon contents = addMeta(contents, `name="theme-color" content="${themeColor}"`); 1086d6b81f9SEvan Bacon } 1096d6b81f9SEvan Bacon 1106d6b81f9SEvan Bacon if (description) { 1116d6b81f9SEvan Bacon contents = addMeta(contents, `name="description" content="${description}"`); 1126d6b81f9SEvan Bacon } 1136d6b81f9SEvan Bacon 1146d6b81f9SEvan Bacon return contents; 1156d6b81f9SEvan Bacon} 1166d6b81f9SEvan Bacon 1176d6b81f9SEvan Bacon/** Add a `<meta />` tag to the `<head />` element. */ 1186d6b81f9SEvan Baconfunction addMeta(contents: string, meta: string): string { 1196d6b81f9SEvan Bacon return contents.replace('</head>', `<meta ${meta}>\n</head>`); 1206d6b81f9SEvan Bacon} 121