xref: /expo/packages/@expo/config/build/Config.js (revision b422da63)
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4  value: true
5});
6var _exportNames = {
7  getConfig: true,
8  getPackageJson: true,
9  getConfigFilePaths: true,
10  modifyConfigAsync: true,
11  getWebOutputPath: true,
12  getNameFromConfig: true,
13  getDefaultTarget: true,
14  getProjectConfigDescription: true,
15  getProjectConfigDescriptionWithPaths: true
16};
17exports.getConfig = getConfig;
18exports.getConfigFilePaths = getConfigFilePaths;
19exports.getDefaultTarget = getDefaultTarget;
20exports.getNameFromConfig = getNameFromConfig;
21exports.getPackageJson = getPackageJson;
22exports.getProjectConfigDescription = getProjectConfigDescription;
23exports.getProjectConfigDescriptionWithPaths = getProjectConfigDescriptionWithPaths;
24exports.getWebOutputPath = getWebOutputPath;
25exports.modifyConfigAsync = modifyConfigAsync;
26function _jsonFile() {
27  const data = _interopRequireDefault(require("@expo/json-file"));
28  _jsonFile = function () {
29    return data;
30  };
31  return data;
32}
33function _fs() {
34  const data = _interopRequireDefault(require("fs"));
35  _fs = function () {
36    return data;
37  };
38  return data;
39}
40function _glob() {
41  const data = require("glob");
42  _glob = function () {
43    return data;
44  };
45  return data;
46}
47function _path() {
48  const data = _interopRequireDefault(require("path"));
49  _path = function () {
50    return data;
51  };
52  return data;
53}
54function _resolveFrom() {
55  const data = _interopRequireDefault(require("resolve-from"));
56  _resolveFrom = function () {
57    return data;
58  };
59  return data;
60}
61function _semver() {
62  const data = _interopRequireDefault(require("semver"));
63  _semver = function () {
64    return data;
65  };
66  return data;
67}
68function _slugify() {
69  const data = _interopRequireDefault(require("slugify"));
70  _slugify = function () {
71    return data;
72  };
73  return data;
74}
75function _getConfig() {
76  const data = require("./getConfig");
77  _getConfig = function () {
78    return data;
79  };
80  return data;
81}
82function _getExpoSDKVersion() {
83  const data = require("./getExpoSDKVersion");
84  _getExpoSDKVersion = function () {
85    return data;
86  };
87  return data;
88}
89function _getFullName() {
90  const data = require("./getFullName");
91  _getFullName = function () {
92    return data;
93  };
94  return data;
95}
96function _withConfigPlugins() {
97  const data = require("./plugins/withConfigPlugins");
98  _withConfigPlugins = function () {
99    return data;
100  };
101  return data;
102}
103function _withInternal() {
104  const data = require("./plugins/withInternal");
105  _withInternal = function () {
106    return data;
107  };
108  return data;
109}
110function _resolvePackageJson() {
111  const data = require("./resolvePackageJson");
112  _resolvePackageJson = function () {
113    return data;
114  };
115  return data;
116}
117var _Config = require("./Config.types");
118Object.keys(_Config).forEach(function (key) {
119  if (key === "default" || key === "__esModule") return;
120  if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
121  if (key in exports && exports[key] === _Config[key]) return;
122  Object.defineProperty(exports, key, {
123    enumerable: true,
124    get: function () {
125      return _Config[key];
126    }
127  });
128});
129function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
130/**
131 * If a config has an `expo` object then that will be used as the config.
132 * This method reduces out other top level values if an `expo` object exists.
133 *
134 * @param config Input config object to reduce
135 */
136function reduceExpoObject(config) {
137  var _config$expo;
138  if (!config) return config === undefined ? null : config;
139  const {
140    mods,
141    ...expo
142  } = (_config$expo = config.expo) !== null && _config$expo !== void 0 ? _config$expo : config;
143  return {
144    expo,
145    mods
146  };
147}
148
149/**
150 * Get all platforms that a project is currently capable of running.
151 *
152 * @param projectRoot
153 * @param exp
154 */
155function getSupportedPlatforms(projectRoot) {
156  const platforms = [];
157  if (_resolveFrom().default.silent(projectRoot, 'react-native')) {
158    platforms.push('ios', 'android');
159  }
160  if (_resolveFrom().default.silent(projectRoot, 'react-native-web')) {
161    platforms.push('web');
162  }
163  return platforms;
164}
165
166/**
167 * Evaluate the config for an Expo project.
168 * If a function is exported from the `app.config.js` then a partial config will be passed as an argument.
169 * The partial config is composed from any existing app.json, and certain fields from the `package.json` like name and description.
170 *
171 * If options.isPublicConfig is true, the Expo config will include only public-facing options (omitting private keys).
172 * The resulting config should be suitable for hosting or embedding in a publicly readable location.
173 *
174 * **Example**
175 * ```js
176 * module.exports = function({ config }) {
177 *   // mutate the config before returning it.
178 *   config.slug = 'new slug'
179 *   return { expo: config };
180 * }
181 * ```
182 *
183 * **Supports**
184 * - `app.config.ts`
185 * - `app.config.js`
186 * - `app.config.json`
187 * - `app.json`
188 *
189 * @param projectRoot the root folder containing all of your application code
190 * @param options enforce criteria for a project config
191 */
192function getConfig(projectRoot, options = {}) {
193  const paths = getConfigFilePaths(projectRoot);
194  const rawStaticConfig = paths.staticConfigPath ? (0, _getConfig().getStaticConfig)(paths.staticConfigPath) : null;
195  // For legacy reasons, always return an object.
196  const rootConfig = rawStaticConfig || {};
197  const staticConfig = reduceExpoObject(rawStaticConfig) || {};
198
199  // Can only change the package.json location if an app.json or app.config.json exists
200  const [packageJson, packageJsonPath] = getPackageJsonAndPath(projectRoot);
201  function fillAndReturnConfig(config, dynamicConfigObjectType) {
202    const configWithDefaultValues = {
203      ...ensureConfigHasDefaultValues({
204        projectRoot,
205        exp: config.expo,
206        pkg: packageJson,
207        skipSDKVersionRequirement: options.skipSDKVersionRequirement,
208        paths,
209        packageJsonPath
210      }),
211      mods: config.mods,
212      dynamicConfigObjectType,
213      rootConfig,
214      dynamicConfigPath: paths.dynamicConfigPath,
215      staticConfigPath: paths.staticConfigPath
216    };
217    if (options.isModdedConfig) {
218      var _config$mods;
219      // @ts-ignore: Add the mods back to the object.
220      configWithDefaultValues.exp.mods = (_config$mods = config.mods) !== null && _config$mods !== void 0 ? _config$mods : null;
221    }
222
223    // Apply static json plugins, should be done after _internal
224    configWithDefaultValues.exp = (0, _withConfigPlugins().withConfigPlugins)(configWithDefaultValues.exp, !!options.skipPlugins);
225    if (!options.isModdedConfig) {
226      // @ts-ignore: Delete mods added by static plugins when they won't have a chance to be evaluated
227      delete configWithDefaultValues.exp.mods;
228    }
229    if (options.isPublicConfig) {
230      var _configWithDefaultVal, _configWithDefaultVal2, _configWithDefaultVal3, _configWithDefaultVal4;
231      // TODD(EvanBacon): Drop plugins array after it's been resolved.
232
233      // Remove internal values with references to user's file paths from the public config.
234      delete configWithDefaultValues.exp._internal;
235      if (configWithDefaultValues.exp.hooks) {
236        delete configWithDefaultValues.exp.hooks;
237      }
238      if ((_configWithDefaultVal = configWithDefaultValues.exp.ios) !== null && _configWithDefaultVal !== void 0 && _configWithDefaultVal.config) {
239        delete configWithDefaultValues.exp.ios.config;
240      }
241      if ((_configWithDefaultVal2 = configWithDefaultValues.exp.android) !== null && _configWithDefaultVal2 !== void 0 && _configWithDefaultVal2.config) {
242        delete configWithDefaultValues.exp.android.config;
243      }
244
245      // These value will be overwritten when the manifest is being served from the host (i.e. not completely accurate).
246      // @ts-ignore: currentFullName not on type yet.
247      configWithDefaultValues.exp.currentFullName = (0, _getFullName().getFullName)(configWithDefaultValues.exp);
248      // @ts-ignore: originalFullName not on type yet.
249      configWithDefaultValues.exp.originalFullName = (0, _getFullName().getFullName)(configWithDefaultValues.exp);
250      (_configWithDefaultVal3 = configWithDefaultValues.exp.updates) === null || _configWithDefaultVal3 === void 0 ? true : delete _configWithDefaultVal3.codeSigningCertificate;
251      (_configWithDefaultVal4 = configWithDefaultValues.exp.updates) === null || _configWithDefaultVal4 === void 0 ? true : delete _configWithDefaultVal4.codeSigningMetadata;
252    }
253    return configWithDefaultValues;
254  }
255
256  // Fill in the static config
257  function getContextConfig(config) {
258    return ensureConfigHasDefaultValues({
259      projectRoot,
260      exp: config.expo,
261      pkg: packageJson,
262      skipSDKVersionRequirement: true,
263      paths,
264      packageJsonPath
265    }).exp;
266  }
267  if (paths.dynamicConfigPath) {
268    // No app.config.json or app.json but app.config.js
269    const {
270      exportedObjectType,
271      config: rawDynamicConfig
272    } = (0, _getConfig().getDynamicConfig)(paths.dynamicConfigPath, {
273      projectRoot,
274      staticConfigPath: paths.staticConfigPath,
275      packageJsonPath,
276      config: getContextConfig(staticConfig)
277    });
278    // Allow for the app.config.js to `export default null;`
279    // Use `dynamicConfigPath` to detect if a dynamic config exists.
280    const dynamicConfig = reduceExpoObject(rawDynamicConfig) || {};
281    return fillAndReturnConfig(dynamicConfig, exportedObjectType);
282  }
283
284  // No app.config.js but json or no config
285  return fillAndReturnConfig(staticConfig || {}, null);
286}
287function getPackageJson(projectRoot) {
288  const [pkg] = getPackageJsonAndPath(projectRoot);
289  return pkg;
290}
291function getPackageJsonAndPath(projectRoot) {
292  const packageJsonPath = (0, _resolvePackageJson().getRootPackageJsonPath)(projectRoot);
293  return [_jsonFile().default.read(packageJsonPath), packageJsonPath];
294}
295
296/**
297 * Get the static and dynamic config paths for a project. Also accounts for custom paths.
298 *
299 * @param projectRoot
300 */
301function getConfigFilePaths(projectRoot) {
302  return {
303    dynamicConfigPath: getDynamicConfigFilePath(projectRoot),
304    staticConfigPath: getStaticConfigFilePath(projectRoot)
305  };
306}
307function getDynamicConfigFilePath(projectRoot) {
308  for (const fileName of ['app.config.ts', 'app.config.js']) {
309    const configPath = _path().default.join(projectRoot, fileName);
310    if (_fs().default.existsSync(configPath)) {
311      return configPath;
312    }
313  }
314  return null;
315}
316function getStaticConfigFilePath(projectRoot) {
317  for (const fileName of ['app.config.json', 'app.json']) {
318    const configPath = _path().default.join(projectRoot, fileName);
319    if (_fs().default.existsSync(configPath)) {
320      return configPath;
321    }
322  }
323  return null;
324}
325
326/**
327 * Attempt to modify an Expo project config.
328 * This will only fully work if the project is using static configs only.
329 * Otherwise 'warn' | 'fail' will return with a message about why the config couldn't be updated.
330 * The potentially modified config object will be returned for testing purposes.
331 *
332 * @param projectRoot
333 * @param modifications modifications to make to an existing config
334 * @param readOptions options for reading the current config file
335 * @param writeOptions If true, the static config file will not be rewritten
336 */
337async function modifyConfigAsync(projectRoot, modifications, readOptions = {}, writeOptions = {}) {
338  const config = getConfig(projectRoot, readOptions);
339  if (config.dynamicConfigPath) {
340    // We cannot automatically write to a dynamic config.
341    /* Currently we should just use the safest approach possible, informing the user that they'll need to manually modify their dynamic config.
342     if (config.staticConfigPath) {
343      // Both a dynamic and a static config exist.
344      if (config.dynamicConfigObjectType === 'function') {
345        // The dynamic config exports a function, this means it possibly extends the static config.
346      } else {
347        // Dynamic config ignores the static config, there isn't a reason to automatically write to it.
348        // Instead we should warn the user to add values to their dynamic config.
349      }
350    }
351    */
352    return {
353      type: 'warn',
354      message: `Cannot automatically write to dynamic config at: ${_path().default.relative(projectRoot, config.dynamicConfigPath)}`,
355      config: null
356    };
357  } else if (config.staticConfigPath) {
358    // Static with no dynamic config, this means we can append to the config automatically.
359    let outputConfig;
360    // If the config has an expo object (app.json) then append the options to that object.
361    if (config.rootConfig.expo) {
362      outputConfig = {
363        ...config.rootConfig,
364        expo: {
365          ...config.rootConfig.expo,
366          ...modifications
367        }
368      };
369    } else {
370      // Otherwise (app.config.json) just add the config modification to the top most level.
371      outputConfig = {
372        ...config.rootConfig,
373        ...modifications
374      };
375    }
376    if (!writeOptions.dryRun) {
377      await _jsonFile().default.writeAsync(config.staticConfigPath, outputConfig, {
378        json5: false
379      });
380    }
381    return {
382      type: 'success',
383      config: outputConfig
384    };
385  }
386  return {
387    type: 'fail',
388    message: 'No config exists',
389    config: null
390  };
391}
392function ensureConfigHasDefaultValues({
393  projectRoot,
394  exp,
395  pkg,
396  paths,
397  packageJsonPath,
398  skipSDKVersionRequirement = false
399}) {
400  var _exp$name, _exp$slug, _exp$version;
401  if (!exp) {
402    exp = {};
403  }
404  exp = (0, _withInternal().withInternal)(exp, {
405    projectRoot,
406    ...(paths !== null && paths !== void 0 ? paths : {}),
407    packageJsonPath
408  });
409  // Defaults for package.json fields
410  const pkgName = typeof pkg.name === 'string' ? pkg.name : _path().default.basename(projectRoot);
411  const pkgVersion = typeof pkg.version === 'string' ? pkg.version : '1.0.0';
412  const pkgWithDefaults = {
413    ...pkg,
414    name: pkgName,
415    version: pkgVersion
416  };
417
418  // Defaults for app.json/app.config.js fields
419  const name = (_exp$name = exp.name) !== null && _exp$name !== void 0 ? _exp$name : pkgName;
420  const slug = (_exp$slug = exp.slug) !== null && _exp$slug !== void 0 ? _exp$slug : (0, _slugify().default)(name.toLowerCase());
421  const version = (_exp$version = exp.version) !== null && _exp$version !== void 0 ? _exp$version : pkgVersion;
422  let description = exp.description;
423  if (!description && typeof pkg.description === 'string') {
424    description = pkg.description;
425  }
426  const expWithDefaults = {
427    ...exp,
428    name,
429    slug,
430    version,
431    description
432  };
433  let sdkVersion;
434  try {
435    sdkVersion = (0, _getExpoSDKVersion().getExpoSDKVersion)(projectRoot, expWithDefaults);
436  } catch (error) {
437    if (!skipSDKVersionRequirement) throw error;
438  }
439  let platforms = exp.platforms;
440  if (!platforms) {
441    platforms = getSupportedPlatforms(projectRoot);
442  }
443  return {
444    exp: {
445      ...expWithDefaults,
446      sdkVersion,
447      platforms
448    },
449    pkg: pkgWithDefaults
450  };
451}
452const DEFAULT_BUILD_PATH = `web-build`;
453function getWebOutputPath(config = {}) {
454  var _expo$web, _expo$web$build;
455  if (process.env.WEBPACK_BUILD_OUTPUT_PATH) {
456    return process.env.WEBPACK_BUILD_OUTPUT_PATH;
457  }
458  const expo = config.expo || config || {};
459  return (expo === null || expo === void 0 ? void 0 : (_expo$web = expo.web) === null || _expo$web === void 0 ? void 0 : (_expo$web$build = _expo$web.build) === null || _expo$web$build === void 0 ? void 0 : _expo$web$build.output) || DEFAULT_BUILD_PATH;
460}
461function getNameFromConfig(exp = {}) {
462  // For RN CLI support
463  const appManifest = exp.expo || exp;
464  const {
465    web = {}
466  } = appManifest;
467
468  // rn-cli apps use a displayName value as well.
469  const appName = exp.displayName || appManifest.displayName || appManifest.name;
470  const webName = web.name || appName;
471  return {
472    appName,
473    webName
474  };
475}
476function getDefaultTarget(projectRoot, exp) {
477  var _exp;
478  (_exp = exp) !== null && _exp !== void 0 ? _exp : exp = getConfig(projectRoot, {
479    skipSDKVersionRequirement: true
480  }).exp;
481
482  // before SDK 37, always default to managed to preserve previous behavior
483  if (exp.sdkVersion && exp.sdkVersion !== 'UNVERSIONED' && _semver().default.lt(exp.sdkVersion, '37.0.0')) {
484    return 'managed';
485  }
486  return isBareWorkflowProject(projectRoot) ? 'bare' : 'managed';
487}
488function isBareWorkflowProject(projectRoot) {
489  const [pkg] = getPackageJsonAndPath(projectRoot);
490
491  // TODO: Drop this
492  if (pkg.dependencies && pkg.dependencies.expokit) {
493    return false;
494  }
495  const xcodeprojFiles = (0, _glob().sync)('ios/**/*.xcodeproj', {
496    absolute: true,
497    cwd: projectRoot
498  });
499  if (xcodeprojFiles.length) {
500    return true;
501  }
502  const gradleFiles = (0, _glob().sync)('android/**/*.gradle', {
503    absolute: true,
504    cwd: projectRoot
505  });
506  if (gradleFiles.length) {
507    return true;
508  }
509  return false;
510}
511
512/**
513 * Return a useful name describing the project config.
514 * - dynamic: app.config.js
515 * - static: app.json
516 * - custom path app config relative to root folder
517 * - both: app.config.js or app.json
518 */
519function getProjectConfigDescription(projectRoot) {
520  const paths = getConfigFilePaths(projectRoot);
521  return getProjectConfigDescriptionWithPaths(projectRoot, paths);
522}
523
524/**
525 * Returns a string describing the configurations used for the given project root.
526 * Will return null if no config is found.
527 *
528 * @param projectRoot
529 * @param projectConfig
530 */
531function getProjectConfigDescriptionWithPaths(projectRoot, projectConfig) {
532  if (projectConfig.dynamicConfigPath) {
533    const relativeDynamicConfigPath = _path().default.relative(projectRoot, projectConfig.dynamicConfigPath);
534    if (projectConfig.staticConfigPath) {
535      return `${relativeDynamicConfigPath} or ${_path().default.relative(projectRoot, projectConfig.staticConfigPath)}`;
536    }
537    return relativeDynamicConfigPath;
538  } else if (projectConfig.staticConfigPath) {
539    return _path().default.relative(projectRoot, projectConfig.staticConfigPath);
540  }
541  // If a config doesn't exist, our tooling will generate a static app.json
542  return 'app.json';
543}
544//# sourceMappingURL=Config.js.map