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