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