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