xref: /expo/packages/expo-linking/src/createURL.ts (revision 2f31cd71)
1import Constants from 'expo-constants';
2import qs from 'qs';
3import URL from 'url-parse';
4
5import { CreateURLOptions, ParsedURL } from './Linking.types';
6import { hasCustomScheme, resolveScheme } from './Schemes';
7import { validateURL } from './validateURL';
8
9function getHostUri(): string | null {
10  if (Constants.expoConfig?.hostUri) {
11    return Constants.expoConfig.hostUri;
12  } else if (!hasCustomScheme()) {
13    // we're probably not using up-to-date xdl, so just fake it for now
14    // we have to remove the /--/ on the end since this will be inserted again later
15    return removeScheme(Constants.linkingUri).replace(/\/--($|\/.*$)/, '');
16  } else {
17    return null;
18  }
19}
20
21function isExpoHosted(): boolean {
22  const hostUri = getHostUri();
23  return !!(
24    hostUri &&
25    (/^(.*\.)?(expo\.io|exp\.host|exp\.direct|expo\.test|expo\.dev)(:.*)?(\/.*)?$/.test(hostUri) ||
26      Constants.expoGoConfig?.developer)
27  );
28}
29
30function removeScheme(url: string): string {
31  return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, '');
32}
33
34function removePort(url: string): string {
35  return url.replace(/(?=([a-zA-Z0-9+.-]+:\/\/)?[^/]):\d+/, '');
36}
37
38function removeLeadingSlash(url: string): string {
39  return url.replace(/^\//, '');
40}
41
42function removeTrailingSlashAndQueryString(url: string): string {
43  return url.replace(/\/?\?.*$/, '');
44}
45
46function ensureLeadingSlash(input: string, shouldAppend: boolean): string {
47  const hasSlash = input.startsWith('/');
48  if (hasSlash && !shouldAppend) {
49    return input.substring(1);
50  } else if (!hasSlash && shouldAppend) {
51    return `/${input}`;
52  }
53  return input;
54}
55
56// @needsAudit
57/**
58 * Helper method for constructing a deep link into your app, given an optional path and set of query
59 * parameters. Creates a URI scheme with two slashes by default.
60 *
61 * The scheme in bare and standalone must be defined in the Expo config (`app.config.js` or `app.json`)
62 * under `expo.scheme`.
63 *
64 * # Examples
65 * - Bare: `<scheme>://path` - uses provided scheme or scheme from Expo config `scheme`.
66 * - Standalone, Custom: `yourscheme://path`
67 * - Web (dev): `https://localhost:19006/path`
68 * - Web (prod): `https://myapp.com/path`
69 * - Expo Client (dev): `exp://128.0.0.1:8081/--/path`
70 * - Expo Client (prod): `exp://exp.host/@yourname/your-app/--/path`
71 *
72 * @param path Addition path components to append to the base URL.
73 * @param namedParameters Additional options object.
74 * @return A URL string which points to your app with the given deep link information.
75 */
76export function createURL(
77  path: string,
78  { scheme, queryParams = {}, isTripleSlashed = false }: CreateURLOptions = {}
79): string {
80  const resolvedScheme = resolveScheme({ scheme });
81
82  let hostUri = getHostUri() || '';
83
84  if (hasCustomScheme() && isExpoHosted()) {
85    hostUri = '';
86  }
87
88  if (path) {
89    if (isExpoHosted() && hostUri) {
90      path = `/--/${removeLeadingSlash(path)}`;
91    }
92    if (isTripleSlashed && !path.startsWith('/')) {
93      path = `/${path}`;
94    }
95  } else {
96    path = '';
97  }
98
99  // merge user-provided query params with any that were already in the hostUri
100  // e.g. release-channel
101  let queryString = '';
102  const queryStringMatchResult = hostUri.match(/(.*)\?(.+)/);
103  if (queryStringMatchResult) {
104    hostUri = queryStringMatchResult[1];
105    queryString = queryStringMatchResult[2];
106    let paramsFromHostUri = {};
107    try {
108      const parsedParams = qs.parse(queryString);
109      if (typeof parsedParams === 'object') {
110        paramsFromHostUri = parsedParams;
111      }
112    } catch {}
113    queryParams = {
114      ...queryParams,
115      ...paramsFromHostUri,
116    };
117  }
118  queryString = qs.stringify(queryParams);
119  if (queryString) {
120    queryString = `?${queryString}`;
121  }
122
123  hostUri = ensureLeadingSlash(hostUri, !isTripleSlashed);
124
125  return encodeURI(
126    `${resolvedScheme}:${isTripleSlashed ? '/' : ''}/${hostUri}${path}${queryString}`
127  );
128}
129
130// @needsAudit
131/**
132 * Helper method for parsing out deep link information from a URL.
133 * @param url A URL that points to the currently running experience (e.g. an output of `Linking.createURL()`).
134 * @return A `ParsedURL` object.
135 */
136export function parse(url: string): ParsedURL {
137  validateURL(url);
138
139  const parsed = URL(url, /* parseQueryString */ true);
140
141  for (const param in parsed.query) {
142    parsed.query[param] = decodeURIComponent(parsed.query[param]!);
143  }
144  const queryParams = parsed.query;
145
146  const hostUri = getHostUri() || '';
147  const hostUriStripped = removePort(removeTrailingSlashAndQueryString(hostUri));
148
149  let path = parsed.pathname || null;
150  let hostname = parsed.hostname || null;
151  let scheme = parsed.protocol || null;
152
153  if (scheme) {
154    // Remove colon at end
155    scheme = scheme.substring(0, scheme.length - 1);
156  }
157
158  if (path) {
159    path = removeLeadingSlash(path);
160
161    let expoPrefix: string | null = null;
162    if (hostUriStripped) {
163      const parts = hostUriStripped.split('/');
164      expoPrefix = parts.slice(1).concat(['--/']).join('/');
165    }
166
167    if (isExpoHosted() && !hasCustomScheme() && expoPrefix && path.startsWith(expoPrefix)) {
168      path = path.substring(expoPrefix.length);
169      hostname = null;
170    } else if (path.indexOf('+') > -1) {
171      path = path.substring(path.indexOf('+') + 1);
172    }
173  }
174
175  return {
176    hostname,
177    path,
178    queryParams,
179    scheme,
180  };
181}
182