1// This MUST be first to ensure that `fetch` is defined in the React Native environment.
2import 'react-native/Libraries/Core/InitializeCore';
3
4import Constants from 'expo-constants';
5import URL from 'url-parse';
6
7import { install, setLocationHref } from './Location';
8import getDevServer from '../getDevServer';
9
10let hasWarned = false;
11
12const manifest = Constants.expoConfig as Record<string, any> | null;
13
14// Add a development warning for fetch requests with relative paths
15// to ensure developers are aware of the need to configure a production
16// base URL in the Expo config (app.json) under `expo.extra.router.origin`.
17function warnProductionOriginNotConfigured(requestUrl: string) {
18  if (hasWarned) {
19    return;
20  }
21  hasWarned = true;
22  if (!manifest?.extra?.router?.origin) {
23    console.warn(
24      `The relative fetch request "${requestUrl}" will not work in production until the Expo Router Config Plugin (app.json) is configured with the \`origin\` prop set to the base URL of your web server, e.g. \`{ plugins: [["expo-router", { origin: "..." }]] }\`. [Learn more](https://expo.github.io/router/docs/lab/runtime-location)`
25    );
26  }
27}
28
29// TODO: This would be better if native and tied as close to the JS engine as possible, i.e. it should
30// reflect the exact location of the JS file that was executed.
31function getBaseUrl() {
32  if (process.env.NODE_ENV !== 'production') {
33    // e.g. http://localhost:19006
34    return getDevServer().url?.replace(/\/$/, '');
35  }
36
37  // TODO: Make it official by moving out of `extra`
38  const productionBaseUrl = manifest?.extra?.router?.origin;
39
40  if (!productionBaseUrl) {
41    return null;
42  }
43
44  // Ensure no trailing slash
45  return productionBaseUrl?.replace(/\/$/, '');
46}
47
48function wrapFetchWithWindowLocation(fetch: Function & { __EXPO_BASE_URL_POLYFILLED?: boolean }) {
49  if (fetch.__EXPO_BASE_URL_POLYFILLED) {
50    return fetch;
51  }
52
53  const _fetch = (...props: any[]) => {
54    if (props[0] && typeof props[0] === 'string' && props[0].startsWith('/')) {
55      if (process.env.NODE_ENV !== 'production') {
56        warnProductionOriginNotConfigured(props[0]);
57      }
58
59      props[0] = new URL(props[0], window.location?.origin).toString();
60    } else if (props[0] && typeof props[0] === 'object') {
61      if (props[0].url && typeof props[0].url === 'string' && props[0].url.startsWith('/')) {
62        if (process.env.NODE_ENV !== 'production') {
63          warnProductionOriginNotConfigured(props[0]);
64        }
65
66        props[0].url = new URL(props[0].url, window.location?.origin).toString();
67      }
68    }
69    return fetch(...props);
70  };
71
72  _fetch.__EXPO_BASE_URL_POLYFILLED = true;
73
74  return _fetch;
75}
76
77if (manifest?.extra?.router?.origin !== false) {
78  // Polyfill window.location in native runtimes.
79  if (typeof window !== 'undefined' && !window.location) {
80    const url = getBaseUrl();
81    if (url) {
82      setLocationHref(url);
83      install();
84    }
85  }
86  // Polyfill native fetch to support relative URLs
87  Object.defineProperty(global, 'fetch', {
88    value: wrapFetchWithWindowLocation(fetch),
89  });
90}
91