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