xref: /expo/tools/src/dynamic-macros/macros.ts (revision 5f54863a)
1import JsonFile from '@expo/json-file';
2import {
3  isMultipartPartWithName,
4  parseMultipartMixedResponseAsync,
5} from '@expo/multipart-body-parser';
6import spawnAsync from '@expo/spawn-async';
7import { Project, UrlUtils } from '@expo/xdl';
8import chalk from 'chalk';
9import crypto from 'crypto';
10import ip from 'ip';
11import fetch, { Response } from 'node-fetch';
12import os from 'os';
13import path from 'path';
14
15import { getExpoRepositoryRootDir } from '../Directories';
16import { getHomeSDKVersionAsync } from '../ProjectVersions';
17
18interface Manifest {
19  id: string;
20  createdAt: string;
21  runtimeVersion: string;
22  metadata: { [key: string]: string };
23  extra: {
24    eas: {
25      projectId: string;
26    };
27    expoClient?: {
28      name: string;
29    };
30  };
31}
32
33// some files are absent on turtle builders and we don't want log errors there
34const isTurtle = !!process.env.TURTLE_WORKING_DIR_PATH;
35
36const EXPO_DIR = getExpoRepositoryRootDir();
37
38type AssetRequestHeaders = { authorization: string };
39
40async function getManifestBodyAsync(response: Response): Promise<{
41  manifest: Manifest;
42  assetRequestHeaders: {
43    [assetKey: string]: AssetRequestHeaders;
44  };
45}> {
46  const contentType = response.headers.get('content-type');
47  if (!contentType) {
48    throw new Error('The multipart manifest response is missing the content-type header');
49  }
50
51  if (contentType === 'application/expo+json' || contentType === 'application/json') {
52    const text = await response.text();
53    return { manifest: JSON.parse(text), assetRequestHeaders: {} };
54  }
55
56  const bodyBuffer = await response.arrayBuffer();
57  const multipartParts = await parseMultipartMixedResponseAsync(
58    contentType,
59    Buffer.from(bodyBuffer)
60  );
61
62  const manifestPart = multipartParts.find((part) => isMultipartPartWithName(part, 'manifest'));
63  if (!manifestPart) {
64    throw new Error('The multipart manifest response is missing the manifest part');
65  }
66
67  const extensionsPart = multipartParts.find((part) => isMultipartPartWithName(part, 'extensions'));
68  const assetRequestHeaders = extensionsPart
69    ? JSON.parse(extensionsPart.body).assetRequestHeaders
70    : {};
71
72  return { manifest: JSON.parse(manifestPart.body), assetRequestHeaders };
73}
74
75async function getManifestAsync(
76  url: string,
77  platform: string
78): Promise<{
79  manifest: Manifest;
80  assetRequestHeaders: {
81    [assetKey: string]: AssetRequestHeaders;
82  };
83}> {
84  const response = await fetch(url.replace('exp://', 'http://').replace('exps://', 'https://'), {
85    method: 'GET',
86    headers: {
87      accept: 'multipart/mixed,application/expo+json,application/json',
88      'expo-platform': platform,
89    },
90  });
91  return await getManifestBodyAsync(response);
92}
93
94async function getSavedDevHomeEASUpdateUrlAsync(): Promise<string> {
95  const devHomeConfig = await new JsonFile(path.join(EXPO_DIR, 'dev-home-config.json')).readAsync();
96  return devHomeConfig.url as string;
97}
98
99function kernelManifestAndAssetRequestHeadersObjectToJson(obj: {
100  manifest: Manifest;
101  assetRequestHeaders: {
102    [assetKey: string]: AssetRequestHeaders;
103  };
104}) {
105  return JSON.stringify(obj);
106}
107
108export default {
109  async TEST_APP_URI() {
110    if (process.env.TEST_SUITE_URI) {
111      return process.env.TEST_SUITE_URI;
112    } else {
113      try {
114        const testSuitePath = path.join(__dirname, '..', '..', '..', 'apps', 'test-suite');
115        const status = await Project.currentStatus(testSuitePath);
116        if (status === 'running') {
117          return await UrlUtils.constructManifestUrlAsync(testSuitePath);
118        } else {
119          return '';
120        }
121      } catch {
122        return '';
123      }
124    }
125  },
126
127  async TEST_CONFIG() {
128    if (process.env.TEST_CONFIG) {
129      return process.env.TEST_CONFIG;
130    } else {
131      return '';
132    }
133  },
134
135  async TEST_SERVER_URL() {
136    let url = 'TODO';
137
138    try {
139      const lanAddress = ip.address();
140      const localServerUrl = `http://${lanAddress}:3013`;
141      const response = await fetch(`${localServerUrl}/expo-test-server-status`, { timeout: 500 });
142      const data = await response.text();
143      if (data === 'running!') {
144        url = localServerUrl;
145      }
146    } catch {}
147
148    return url;
149  },
150
151  async TEST_RUN_ID() {
152    return process.env.UNIVERSE_BUILD_ID || crypto.randomUUID();
153  },
154
155  async BUILD_MACHINE_LOCAL_HOSTNAME() {
156    if (process.env.SHELL_APP_BUILDER) {
157      return '';
158    }
159
160    try {
161      const result = await spawnAsync('scutil', ['--get', 'LocalHostName']);
162      return `${result.stdout.trim()}.local`;
163    } catch (e) {
164      if (e.code !== 'ENOENT') {
165        console.error(e.stack);
166      }
167      return os.hostname();
168    }
169  },
170
171  async DEV_PUBLISHED_KERNEL_MANIFEST(platform) {
172    let manifestAndAssetRequestHeaders: {
173      manifest: Manifest;
174      assetRequestHeaders: {
175        [assetKey: string]: AssetRequestHeaders;
176      };
177    };
178    let savedDevHomeUrl: string | undefined;
179    try {
180      savedDevHomeUrl = await getSavedDevHomeEASUpdateUrlAsync();
181      manifestAndAssetRequestHeaders = await getManifestAsync(savedDevHomeUrl, platform);
182    } catch (e) {
183      const msg = `Unable to download manifest from ${savedDevHomeUrl ?? '(error)'}: ${e.message}`;
184      console[isTurtle ? 'debug' : 'error'](msg);
185      return '';
186    }
187
188    return kernelManifestAndAssetRequestHeadersObjectToJson(manifestAndAssetRequestHeaders);
189  },
190
191  async BUILD_MACHINE_KERNEL_MANIFEST(platform) {
192    if (process.env.SHELL_APP_BUILDER) {
193      return '';
194    }
195
196    if (process.env.CI) {
197      console.log('Skip fetching local manifest on CI.');
198      return '';
199    }
200
201    const pathToHome = 'home';
202    const url = await UrlUtils.constructManifestUrlAsync(path.join(EXPO_DIR, pathToHome));
203
204    try {
205      const manifestAndAssetRequestHeaders = await getManifestAsync(url, platform);
206
207      if (manifestAndAssetRequestHeaders.manifest.extra?.expoClient?.name !== 'expo-home') {
208        console.log(
209          `Manifest at ${url} is not expo-home; using published kernel manifest instead...`
210        );
211        return '';
212      }
213      return kernelManifestAndAssetRequestHeadersObjectToJson(manifestAndAssetRequestHeaders);
214    } catch {
215      console.error(
216        chalk.red(
217          `Unable to generate manifest from ${chalk.cyan(
218            pathToHome
219          )}: Failed to fetch manifest from ${chalk.cyan(url)}`
220        )
221      );
222      return '';
223    }
224  },
225
226  async TEMPORARY_SDK_VERSION(): Promise<string> {
227    return await getHomeSDKVersionAsync();
228  },
229
230  INITIAL_URL() {
231    return null;
232  },
233};
234