xref: /expo/packages/@expo/config/src/Config.ts (revision 8c301ce0)
1082815dcSEvan Baconimport { ModConfig } from '@expo/config-plugins';
2082815dcSEvan Baconimport JsonFile, { JSONObject } from '@expo/json-file';
3082815dcSEvan Baconimport fs from 'fs';
4082815dcSEvan Baconimport { sync as globSync } from 'glob';
5082815dcSEvan Baconimport path from 'path';
6082815dcSEvan Baconimport resolveFrom from 'resolve-from';
7082815dcSEvan Baconimport semver from 'semver';
8082815dcSEvan Baconimport slugify from 'slugify';
9082815dcSEvan Bacon
10082815dcSEvan Baconimport {
11082815dcSEvan Bacon  AppJSONConfig,
12082815dcSEvan Bacon  ConfigFilePaths,
13082815dcSEvan Bacon  ExpoConfig,
14082815dcSEvan Bacon  GetConfigOptions,
15082815dcSEvan Bacon  PackageJSONConfig,
16082815dcSEvan Bacon  Platform,
17082815dcSEvan Bacon  ProjectConfig,
18082815dcSEvan Bacon  ProjectTarget,
19082815dcSEvan Bacon  WriteConfigOptions,
20082815dcSEvan Bacon} from './Config.types';
21082815dcSEvan Baconimport { getDynamicConfig, getStaticConfig } from './getConfig';
22*97e1898cSEvan Baconimport { getExpoSDKVersion } from './getExpoSDKVersion';
23082815dcSEvan Baconimport { withConfigPlugins } from './plugins/withConfigPlugins';
24082815dcSEvan Baconimport { withInternal } from './plugins/withInternal';
25082815dcSEvan Baconimport { getRootPackageJsonPath } from './resolvePackageJson';
26082815dcSEvan Bacon
27082815dcSEvan Bacontype SplitConfigs = { expo: ExpoConfig; mods: ModConfig };
28082815dcSEvan Bacon
29082815dcSEvan Bacon/**
30082815dcSEvan Bacon * If a config has an `expo` object then that will be used as the config.
31082815dcSEvan Bacon * This method reduces out other top level values if an `expo` object exists.
32082815dcSEvan Bacon *
33082815dcSEvan Bacon * @param config Input config object to reduce
34082815dcSEvan Bacon */
35082815dcSEvan Baconfunction reduceExpoObject(config?: any): SplitConfigs {
36082815dcSEvan Bacon  if (!config) return config === undefined ? null : config;
37082815dcSEvan Bacon
38082815dcSEvan Bacon  const { mods, ...expo } = config.expo ?? config;
39082815dcSEvan Bacon
40082815dcSEvan Bacon  return {
41082815dcSEvan Bacon    expo,
42082815dcSEvan Bacon    mods,
43082815dcSEvan Bacon  };
44082815dcSEvan Bacon}
45082815dcSEvan Bacon
46082815dcSEvan Bacon/**
47082815dcSEvan Bacon * Get all platforms that a project is currently capable of running.
48082815dcSEvan Bacon *
49082815dcSEvan Bacon * @param projectRoot
50082815dcSEvan Bacon * @param exp
51082815dcSEvan Bacon */
52082815dcSEvan Baconfunction getSupportedPlatforms(projectRoot: string): Platform[] {
53082815dcSEvan Bacon  const platforms: Platform[] = [];
54082815dcSEvan Bacon  if (resolveFrom.silent(projectRoot, 'react-native')) {
55082815dcSEvan Bacon    platforms.push('ios', 'android');
56082815dcSEvan Bacon  }
57082815dcSEvan Bacon  if (resolveFrom.silent(projectRoot, 'react-native-web')) {
58082815dcSEvan Bacon    platforms.push('web');
59082815dcSEvan Bacon  }
60082815dcSEvan Bacon  return platforms;
61082815dcSEvan Bacon}
62082815dcSEvan Bacon
63082815dcSEvan Bacon/**
64082815dcSEvan Bacon * Evaluate the config for an Expo project.
65082815dcSEvan Bacon * If a function is exported from the `app.config.js` then a partial config will be passed as an argument.
66082815dcSEvan Bacon * The partial config is composed from any existing app.json, and certain fields from the `package.json` like name and description.
67082815dcSEvan Bacon *
68082815dcSEvan Bacon * If options.isPublicConfig is true, the Expo config will include only public-facing options (omitting private keys).
69082815dcSEvan Bacon * The resulting config should be suitable for hosting or embedding in a publicly readable location.
70082815dcSEvan Bacon *
71082815dcSEvan Bacon * **Example**
72082815dcSEvan Bacon * ```js
73082815dcSEvan Bacon * module.exports = function({ config }) {
74082815dcSEvan Bacon *   // mutate the config before returning it.
75082815dcSEvan Bacon *   config.slug = 'new slug'
76082815dcSEvan Bacon *   return { expo: config };
77082815dcSEvan Bacon * }
78082815dcSEvan Bacon * ```
79082815dcSEvan Bacon *
80082815dcSEvan Bacon * **Supports**
81082815dcSEvan Bacon * - `app.config.ts`
82082815dcSEvan Bacon * - `app.config.js`
83082815dcSEvan Bacon * - `app.config.json`
84082815dcSEvan Bacon * - `app.json`
85082815dcSEvan Bacon *
86082815dcSEvan Bacon * @param projectRoot the root folder containing all of your application code
87082815dcSEvan Bacon * @param options enforce criteria for a project config
88082815dcSEvan Bacon */
89082815dcSEvan Baconexport function getConfig(projectRoot: string, options: GetConfigOptions = {}): ProjectConfig {
90082815dcSEvan Bacon  const paths = getConfigFilePaths(projectRoot);
91082815dcSEvan Bacon
92082815dcSEvan Bacon  const rawStaticConfig = paths.staticConfigPath ? getStaticConfig(paths.staticConfigPath) : null;
93082815dcSEvan Bacon  // For legacy reasons, always return an object.
94082815dcSEvan Bacon  const rootConfig = (rawStaticConfig || {}) as AppJSONConfig;
95082815dcSEvan Bacon  const staticConfig = reduceExpoObject(rawStaticConfig) || {};
96082815dcSEvan Bacon
97082815dcSEvan Bacon  // Can only change the package.json location if an app.json or app.config.json exists
98082815dcSEvan Bacon  const [packageJson, packageJsonPath] = getPackageJsonAndPath(projectRoot);
99082815dcSEvan Bacon
100082815dcSEvan Bacon  function fillAndReturnConfig(config: SplitConfigs, dynamicConfigObjectType: string | null) {
101082815dcSEvan Bacon    const configWithDefaultValues = {
102082815dcSEvan Bacon      ...ensureConfigHasDefaultValues({
103082815dcSEvan Bacon        projectRoot,
104082815dcSEvan Bacon        exp: config.expo,
105082815dcSEvan Bacon        pkg: packageJson,
106082815dcSEvan Bacon        skipSDKVersionRequirement: options.skipSDKVersionRequirement,
107082815dcSEvan Bacon        paths,
108082815dcSEvan Bacon        packageJsonPath,
109082815dcSEvan Bacon      }),
110082815dcSEvan Bacon      mods: config.mods,
111082815dcSEvan Bacon      dynamicConfigObjectType,
112082815dcSEvan Bacon      rootConfig,
113082815dcSEvan Bacon      dynamicConfigPath: paths.dynamicConfigPath,
114082815dcSEvan Bacon      staticConfigPath: paths.staticConfigPath,
115082815dcSEvan Bacon    };
116082815dcSEvan Bacon
117082815dcSEvan Bacon    if (options.isModdedConfig) {
118082815dcSEvan Bacon      // @ts-ignore: Add the mods back to the object.
119082815dcSEvan Bacon      configWithDefaultValues.exp.mods = config.mods ?? null;
120082815dcSEvan Bacon    }
121082815dcSEvan Bacon
122082815dcSEvan Bacon    // Apply static json plugins, should be done after _internal
123082815dcSEvan Bacon    configWithDefaultValues.exp = withConfigPlugins(
124082815dcSEvan Bacon      configWithDefaultValues.exp,
125082815dcSEvan Bacon      !!options.skipPlugins
126082815dcSEvan Bacon    );
127082815dcSEvan Bacon
128082815dcSEvan Bacon    if (!options.isModdedConfig) {
129082815dcSEvan Bacon      // @ts-ignore: Delete mods added by static plugins when they won't have a chance to be evaluated
130082815dcSEvan Bacon      delete configWithDefaultValues.exp.mods;
131082815dcSEvan Bacon    }
132082815dcSEvan Bacon
133082815dcSEvan Bacon    if (options.isPublicConfig) {
134*97e1898cSEvan Bacon      // TODD(EvanBacon): Drop plugins array after it's been resolved.
135*97e1898cSEvan Bacon
136082815dcSEvan Bacon      // Remove internal values with references to user's file paths from the public config.
137082815dcSEvan Bacon      delete configWithDefaultValues.exp._internal;
138082815dcSEvan Bacon
139082815dcSEvan Bacon      if (configWithDefaultValues.exp.hooks) {
140082815dcSEvan Bacon        delete configWithDefaultValues.exp.hooks;
141082815dcSEvan Bacon      }
142082815dcSEvan Bacon      if (configWithDefaultValues.exp.ios?.config) {
143082815dcSEvan Bacon        delete configWithDefaultValues.exp.ios.config;
144082815dcSEvan Bacon      }
145082815dcSEvan Bacon      if (configWithDefaultValues.exp.android?.config) {
146082815dcSEvan Bacon        delete configWithDefaultValues.exp.android.config;
147082815dcSEvan Bacon      }
148082815dcSEvan Bacon
149082815dcSEvan Bacon      delete configWithDefaultValues.exp.updates?.codeSigningCertificate;
150082815dcSEvan Bacon      delete configWithDefaultValues.exp.updates?.codeSigningMetadata;
151082815dcSEvan Bacon    }
152082815dcSEvan Bacon
153082815dcSEvan Bacon    return configWithDefaultValues;
154082815dcSEvan Bacon  }
155082815dcSEvan Bacon
156082815dcSEvan Bacon  // Fill in the static config
157082815dcSEvan Bacon  function getContextConfig(config: SplitConfigs) {
158082815dcSEvan Bacon    return ensureConfigHasDefaultValues({
159082815dcSEvan Bacon      projectRoot,
160082815dcSEvan Bacon      exp: config.expo,
161082815dcSEvan Bacon      pkg: packageJson,
162082815dcSEvan Bacon      skipSDKVersionRequirement: true,
163082815dcSEvan Bacon      paths,
164082815dcSEvan Bacon      packageJsonPath,
165082815dcSEvan Bacon    }).exp;
166082815dcSEvan Bacon  }
167082815dcSEvan Bacon
168082815dcSEvan Bacon  if (paths.dynamicConfigPath) {
169082815dcSEvan Bacon    // No app.config.json or app.json but app.config.js
170082815dcSEvan Bacon    const { exportedObjectType, config: rawDynamicConfig } = getDynamicConfig(
171082815dcSEvan Bacon      paths.dynamicConfigPath,
172082815dcSEvan Bacon      {
173082815dcSEvan Bacon        projectRoot,
174082815dcSEvan Bacon        staticConfigPath: paths.staticConfigPath,
175082815dcSEvan Bacon        packageJsonPath,
176082815dcSEvan Bacon        config: getContextConfig(staticConfig),
177082815dcSEvan Bacon      }
178082815dcSEvan Bacon    );
179082815dcSEvan Bacon    // Allow for the app.config.js to `export default null;`
180082815dcSEvan Bacon    // Use `dynamicConfigPath` to detect if a dynamic config exists.
181082815dcSEvan Bacon    const dynamicConfig = reduceExpoObject(rawDynamicConfig) || {};
182082815dcSEvan Bacon    return fillAndReturnConfig(dynamicConfig, exportedObjectType);
183082815dcSEvan Bacon  }
184082815dcSEvan Bacon
185082815dcSEvan Bacon  // No app.config.js but json or no config
186082815dcSEvan Bacon  return fillAndReturnConfig(staticConfig || {}, null);
187082815dcSEvan Bacon}
188082815dcSEvan Bacon
189082815dcSEvan Baconexport function getPackageJson(projectRoot: string): PackageJSONConfig {
190082815dcSEvan Bacon  const [pkg] = getPackageJsonAndPath(projectRoot);
191082815dcSEvan Bacon  return pkg;
192082815dcSEvan Bacon}
193082815dcSEvan Bacon
194082815dcSEvan Baconfunction getPackageJsonAndPath(projectRoot: string): [PackageJSONConfig, string] {
195082815dcSEvan Bacon  const packageJsonPath = getRootPackageJsonPath(projectRoot);
196082815dcSEvan Bacon  return [JsonFile.read(packageJsonPath), packageJsonPath];
197082815dcSEvan Bacon}
198082815dcSEvan Bacon
199082815dcSEvan Bacon/**
200082815dcSEvan Bacon * Get the static and dynamic config paths for a project. Also accounts for custom paths.
201082815dcSEvan Bacon *
202082815dcSEvan Bacon * @param projectRoot
203082815dcSEvan Bacon */
204082815dcSEvan Baconexport function getConfigFilePaths(projectRoot: string): ConfigFilePaths {
205082815dcSEvan Bacon  return {
206082815dcSEvan Bacon    dynamicConfigPath: getDynamicConfigFilePath(projectRoot),
207082815dcSEvan Bacon    staticConfigPath: getStaticConfigFilePath(projectRoot),
208082815dcSEvan Bacon  };
209082815dcSEvan Bacon}
210082815dcSEvan Bacon
211082815dcSEvan Baconfunction getDynamicConfigFilePath(projectRoot: string): string | null {
212082815dcSEvan Bacon  for (const fileName of ['app.config.ts', 'app.config.js']) {
213082815dcSEvan Bacon    const configPath = path.join(projectRoot, fileName);
214082815dcSEvan Bacon    if (fs.existsSync(configPath)) {
215082815dcSEvan Bacon      return configPath;
216082815dcSEvan Bacon    }
217082815dcSEvan Bacon  }
218082815dcSEvan Bacon  return null;
219082815dcSEvan Bacon}
220082815dcSEvan Bacon
221082815dcSEvan Baconfunction getStaticConfigFilePath(projectRoot: string): string | null {
222082815dcSEvan Bacon  for (const fileName of ['app.config.json', 'app.json']) {
223082815dcSEvan Bacon    const configPath = path.join(projectRoot, fileName);
224082815dcSEvan Bacon    if (fs.existsSync(configPath)) {
225082815dcSEvan Bacon      return configPath;
226082815dcSEvan Bacon    }
227082815dcSEvan Bacon  }
228082815dcSEvan Bacon  return null;
229082815dcSEvan Bacon}
230082815dcSEvan Bacon
231082815dcSEvan Bacon/**
232082815dcSEvan Bacon * Attempt to modify an Expo project config.
233082815dcSEvan Bacon * This will only fully work if the project is using static configs only.
234082815dcSEvan Bacon * Otherwise 'warn' | 'fail' will return with a message about why the config couldn't be updated.
235082815dcSEvan Bacon * The potentially modified config object will be returned for testing purposes.
236082815dcSEvan Bacon *
237082815dcSEvan Bacon * @param projectRoot
238082815dcSEvan Bacon * @param modifications modifications to make to an existing config
239082815dcSEvan Bacon * @param readOptions options for reading the current config file
240082815dcSEvan Bacon * @param writeOptions If true, the static config file will not be rewritten
241082815dcSEvan Bacon */
242082815dcSEvan Baconexport async function modifyConfigAsync(
243082815dcSEvan Bacon  projectRoot: string,
244082815dcSEvan Bacon  modifications: Partial<ExpoConfig>,
245082815dcSEvan Bacon  readOptions: GetConfigOptions = {},
246082815dcSEvan Bacon  writeOptions: WriteConfigOptions = {}
247082815dcSEvan Bacon): Promise<{
248082815dcSEvan Bacon  type: 'success' | 'warn' | 'fail';
249082815dcSEvan Bacon  message?: string;
250082815dcSEvan Bacon  config: AppJSONConfig | null;
251082815dcSEvan Bacon}> {
252082815dcSEvan Bacon  const config = getConfig(projectRoot, readOptions);
253082815dcSEvan Bacon  if (config.dynamicConfigPath) {
254082815dcSEvan Bacon    // We cannot automatically write to a dynamic config.
255082815dcSEvan Bacon    /* Currently we should just use the safest approach possible, informing the user that they'll need to manually modify their dynamic config.
256082815dcSEvan Bacon
257082815dcSEvan Bacon    if (config.staticConfigPath) {
258082815dcSEvan Bacon      // Both a dynamic and a static config exist.
259082815dcSEvan Bacon      if (config.dynamicConfigObjectType === 'function') {
260082815dcSEvan Bacon        // The dynamic config exports a function, this means it possibly extends the static config.
261082815dcSEvan Bacon      } else {
262082815dcSEvan Bacon        // Dynamic config ignores the static config, there isn't a reason to automatically write to it.
263082815dcSEvan Bacon        // Instead we should warn the user to add values to their dynamic config.
264082815dcSEvan Bacon      }
265082815dcSEvan Bacon    }
266082815dcSEvan Bacon    */
267082815dcSEvan Bacon    return {
268082815dcSEvan Bacon      type: 'warn',
269082815dcSEvan Bacon      message: `Cannot automatically write to dynamic config at: ${path.relative(
270082815dcSEvan Bacon        projectRoot,
271082815dcSEvan Bacon        config.dynamicConfigPath
272082815dcSEvan Bacon      )}`,
273082815dcSEvan Bacon      config: null,
274082815dcSEvan Bacon    };
275082815dcSEvan Bacon  } else if (config.staticConfigPath) {
276082815dcSEvan Bacon    // Static with no dynamic config, this means we can append to the config automatically.
277082815dcSEvan Bacon    let outputConfig: AppJSONConfig;
278082815dcSEvan Bacon    // If the config has an expo object (app.json) then append the options to that object.
279082815dcSEvan Bacon    if (config.rootConfig.expo) {
280082815dcSEvan Bacon      outputConfig = {
281082815dcSEvan Bacon        ...config.rootConfig,
282082815dcSEvan Bacon        expo: { ...config.rootConfig.expo, ...modifications },
283082815dcSEvan Bacon      };
284082815dcSEvan Bacon    } else {
285082815dcSEvan Bacon      // Otherwise (app.config.json) just add the config modification to the top most level.
286082815dcSEvan Bacon      outputConfig = { ...config.rootConfig, ...modifications };
287082815dcSEvan Bacon    }
288082815dcSEvan Bacon    if (!writeOptions.dryRun) {
289082815dcSEvan Bacon      await JsonFile.writeAsync(config.staticConfigPath, outputConfig, { json5: false });
290082815dcSEvan Bacon    }
291082815dcSEvan Bacon    return { type: 'success', config: outputConfig };
292082815dcSEvan Bacon  }
293082815dcSEvan Bacon
294082815dcSEvan Bacon  return { type: 'fail', message: 'No config exists', config: null };
295082815dcSEvan Bacon}
296082815dcSEvan Bacon
297082815dcSEvan Baconfunction ensureConfigHasDefaultValues({
298082815dcSEvan Bacon  projectRoot,
299082815dcSEvan Bacon  exp,
300082815dcSEvan Bacon  pkg,
301082815dcSEvan Bacon  paths,
302082815dcSEvan Bacon  packageJsonPath,
303082815dcSEvan Bacon  skipSDKVersionRequirement = false,
304082815dcSEvan Bacon}: {
305082815dcSEvan Bacon  projectRoot: string;
306082815dcSEvan Bacon  exp: Partial<ExpoConfig> | null;
307082815dcSEvan Bacon  pkg: JSONObject;
308082815dcSEvan Bacon  skipSDKVersionRequirement?: boolean;
309082815dcSEvan Bacon  paths?: ConfigFilePaths;
310082815dcSEvan Bacon  packageJsonPath?: string;
311082815dcSEvan Bacon}): { exp: ExpoConfig; pkg: PackageJSONConfig } {
312082815dcSEvan Bacon  if (!exp) {
313082815dcSEvan Bacon    exp = {};
314082815dcSEvan Bacon  }
315082815dcSEvan Bacon  exp = withInternal(exp as any, {
316082815dcSEvan Bacon    projectRoot,
317082815dcSEvan Bacon    ...(paths ?? {}),
318082815dcSEvan Bacon    packageJsonPath,
319082815dcSEvan Bacon  });
320082815dcSEvan Bacon  // Defaults for package.json fields
321082815dcSEvan Bacon  const pkgName = typeof pkg.name === 'string' ? pkg.name : path.basename(projectRoot);
322082815dcSEvan Bacon  const pkgVersion = typeof pkg.version === 'string' ? pkg.version : '1.0.0';
323082815dcSEvan Bacon
324082815dcSEvan Bacon  const pkgWithDefaults = { ...pkg, name: pkgName, version: pkgVersion };
325082815dcSEvan Bacon
326082815dcSEvan Bacon  // Defaults for app.json/app.config.js fields
327082815dcSEvan Bacon  const name = exp.name ?? pkgName;
328082815dcSEvan Bacon  const slug = exp.slug ?? slugify(name.toLowerCase());
329082815dcSEvan Bacon  const version = exp.version ?? pkgVersion;
330082815dcSEvan Bacon  let description = exp.description;
331082815dcSEvan Bacon  if (!description && typeof pkg.description === 'string') {
332082815dcSEvan Bacon    description = pkg.description;
333082815dcSEvan Bacon  }
334082815dcSEvan Bacon
335082815dcSEvan Bacon  const expWithDefaults = { ...exp, name, slug, version, description };
336082815dcSEvan Bacon
337082815dcSEvan Bacon  let sdkVersion;
338082815dcSEvan Bacon  try {
339082815dcSEvan Bacon    sdkVersion = getExpoSDKVersion(projectRoot, expWithDefaults);
340082815dcSEvan Bacon  } catch (error) {
341082815dcSEvan Bacon    if (!skipSDKVersionRequirement) throw error;
342082815dcSEvan Bacon  }
343082815dcSEvan Bacon
344082815dcSEvan Bacon  let platforms = exp.platforms;
345082815dcSEvan Bacon  if (!platforms) {
346082815dcSEvan Bacon    platforms = getSupportedPlatforms(projectRoot);
347082815dcSEvan Bacon  }
348082815dcSEvan Bacon
349082815dcSEvan Bacon  return {
350082815dcSEvan Bacon    exp: { ...expWithDefaults, sdkVersion, platforms },
351082815dcSEvan Bacon    pkg: pkgWithDefaults,
352082815dcSEvan Bacon  };
353082815dcSEvan Bacon}
354082815dcSEvan Bacon
355082815dcSEvan Baconconst DEFAULT_BUILD_PATH = `web-build`;
356082815dcSEvan Bacon
357082815dcSEvan Baconexport function getWebOutputPath(config: { [key: string]: any } = {}): string {
358082815dcSEvan Bacon  if (process.env.WEBPACK_BUILD_OUTPUT_PATH) {
359082815dcSEvan Bacon    return process.env.WEBPACK_BUILD_OUTPUT_PATH;
360082815dcSEvan Bacon  }
361082815dcSEvan Bacon  const expo = config.expo || config || {};
362082815dcSEvan Bacon  return expo?.web?.build?.output || DEFAULT_BUILD_PATH;
363082815dcSEvan Bacon}
364082815dcSEvan Bacon
365082815dcSEvan Baconexport function getNameFromConfig(exp: Record<string, any> = {}): {
366082815dcSEvan Bacon  appName?: string;
367082815dcSEvan Bacon  webName?: string;
368082815dcSEvan Bacon} {
369082815dcSEvan Bacon  // For RN CLI support
370082815dcSEvan Bacon  const appManifest = exp.expo || exp;
371082815dcSEvan Bacon  const { web = {} } = appManifest;
372082815dcSEvan Bacon
373082815dcSEvan Bacon  // rn-cli apps use a displayName value as well.
374082815dcSEvan Bacon  const appName = exp.displayName || appManifest.displayName || appManifest.name;
375082815dcSEvan Bacon  const webName = web.name || appName;
376082815dcSEvan Bacon
377082815dcSEvan Bacon  return {
378082815dcSEvan Bacon    appName,
379082815dcSEvan Bacon    webName,
380082815dcSEvan Bacon  };
381082815dcSEvan Bacon}
382082815dcSEvan Bacon
383082815dcSEvan Baconexport function getDefaultTarget(
384082815dcSEvan Bacon  projectRoot: string,
385082815dcSEvan Bacon  exp?: Pick<ExpoConfig, 'sdkVersion'>
386082815dcSEvan Bacon): ProjectTarget {
387082815dcSEvan Bacon  exp ??= getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp;
388082815dcSEvan Bacon
389082815dcSEvan Bacon  // before SDK 37, always default to managed to preserve previous behavior
390082815dcSEvan Bacon  if (exp.sdkVersion && exp.sdkVersion !== 'UNVERSIONED' && semver.lt(exp.sdkVersion, '37.0.0')) {
391082815dcSEvan Bacon    return 'managed';
392082815dcSEvan Bacon  }
393082815dcSEvan Bacon  return isBareWorkflowProject(projectRoot) ? 'bare' : 'managed';
394082815dcSEvan Bacon}
395082815dcSEvan Bacon
396082815dcSEvan Baconfunction isBareWorkflowProject(projectRoot: string): boolean {
397082815dcSEvan Bacon  const [pkg] = getPackageJsonAndPath(projectRoot);
398082815dcSEvan Bacon
399*97e1898cSEvan Bacon  // TODO: Drop this
400082815dcSEvan Bacon  if (pkg.dependencies && pkg.dependencies.expokit) {
401082815dcSEvan Bacon    return false;
402082815dcSEvan Bacon  }
403082815dcSEvan Bacon
404082815dcSEvan Bacon  const xcodeprojFiles = globSync('ios/**/*.xcodeproj', {
405082815dcSEvan Bacon    absolute: true,
406082815dcSEvan Bacon    cwd: projectRoot,
407082815dcSEvan Bacon  });
408082815dcSEvan Bacon  if (xcodeprojFiles.length) {
409082815dcSEvan Bacon    return true;
410082815dcSEvan Bacon  }
411082815dcSEvan Bacon  const gradleFiles = globSync('android/**/*.gradle', {
412082815dcSEvan Bacon    absolute: true,
413082815dcSEvan Bacon    cwd: projectRoot,
414082815dcSEvan Bacon  });
415082815dcSEvan Bacon  if (gradleFiles.length) {
416082815dcSEvan Bacon    return true;
417082815dcSEvan Bacon  }
418082815dcSEvan Bacon
419082815dcSEvan Bacon  return false;
420082815dcSEvan Bacon}
421082815dcSEvan Bacon
422082815dcSEvan Bacon/**
423082815dcSEvan Bacon * Return a useful name describing the project config.
424082815dcSEvan Bacon * - dynamic: app.config.js
425082815dcSEvan Bacon * - static: app.json
426082815dcSEvan Bacon * - custom path app config relative to root folder
427082815dcSEvan Bacon * - both: app.config.js or app.json
428082815dcSEvan Bacon */
429082815dcSEvan Baconexport function getProjectConfigDescription(projectRoot: string): string {
430082815dcSEvan Bacon  const paths = getConfigFilePaths(projectRoot);
431082815dcSEvan Bacon  return getProjectConfigDescriptionWithPaths(projectRoot, paths);
432082815dcSEvan Bacon}
433082815dcSEvan Bacon
434082815dcSEvan Bacon/**
435082815dcSEvan Bacon * Returns a string describing the configurations used for the given project root.
436082815dcSEvan Bacon * Will return null if no config is found.
437082815dcSEvan Bacon *
438082815dcSEvan Bacon * @param projectRoot
439082815dcSEvan Bacon * @param projectConfig
440082815dcSEvan Bacon */
441082815dcSEvan Baconexport function getProjectConfigDescriptionWithPaths(
442082815dcSEvan Bacon  projectRoot: string,
443082815dcSEvan Bacon  projectConfig: ConfigFilePaths
444082815dcSEvan Bacon): string {
445082815dcSEvan Bacon  if (projectConfig.dynamicConfigPath) {
446082815dcSEvan Bacon    const relativeDynamicConfigPath = path.relative(projectRoot, projectConfig.dynamicConfigPath);
447082815dcSEvan Bacon    if (projectConfig.staticConfigPath) {
448082815dcSEvan Bacon      return `${relativeDynamicConfigPath} or ${path.relative(
449082815dcSEvan Bacon        projectRoot,
450082815dcSEvan Bacon        projectConfig.staticConfigPath
451082815dcSEvan Bacon      )}`;
452082815dcSEvan Bacon    }
453082815dcSEvan Bacon    return relativeDynamicConfigPath;
454082815dcSEvan Bacon  } else if (projectConfig.staticConfigPath) {
455082815dcSEvan Bacon    return path.relative(projectRoot, projectConfig.staticConfigPath);
456082815dcSEvan Bacon  }
457082815dcSEvan Bacon  // If a config doesn't exist, our tooling will generate a static app.json
458082815dcSEvan Bacon  return 'app.json';
459082815dcSEvan Bacon}
460082815dcSEvan Bacon
461082815dcSEvan Baconexport * from './Config.types';
462