Source: config.js

/**
 * #### Import members from **@edx/frontend-platform**
 *
 * The configuration module provides utilities for working with an application's configuration
 * document (ConfigDocument).  Configuration variables can be supplied to the
 * application in four different ways.  They are applied in the following order:
 *
 * - Build-time Configuration
 *   - Environment Variables
 *   - JavaScript File
 * - Runtime Configuration
 *
 * Last one in wins.  Variables with the same name defined via the later methods will override any
 * defined using an earlier method.  i.e., if a variable is defined in Runtime Configuration, that
 * will override the same variable defined in either Build-time Configuration method (environment
 * variables or JS file).  Configuration defined in a JS file will override environment variables.
 *
 * ##### Build-time Configuration
 *
 * Build-time configuration methods add config variables into the app when it is built by webpack.
 * This saves the app an API call and means it has all the information it needs to initialize right
 * away.  There are two methods of supplying build-time configuration: environment variables and a
 * JavaScript file.
 *
 * ###### Environment Variables
 *
 * A set list of required config variables can be supplied as
 * command-line environment variables during the build process.
 *
 * As a simple example, these are supplied on the command-line before invoking `npm run build`:
 *
 * ```
 * LMS_BASE_URL=http://localhost:18000 npm run build
 * ```
 *
 * Note that additional variables _cannot_ be supplied via this method without using the `config`
 * initialization handler.  The app won't pick them up and they'll appear `undefined`.
 *
 * This configuration method is being deprecated in favor of JavaScript File Configuration.
 *
 * ###### JavaScript File Configuration
 *
 * Configuration variables can be supplied in an optional file named env.config.js.  This file must
 * export either an Object containing configuration variables or a function.  The function must
 * return an Object containing configuration variables or, alternately, a promise which resolves to
 * an Object.
 *
 * Using a function or async function allows the configuration to be resolved at runtime (because
 * the function will be executed at runtime).  This is not common, and the capability is included
 * for the sake of flexibility.
 *
 * JavaScript File Configuration is well-suited to extensibility use cases or component overrides,
 * in that the configuration file can depend on any installed JavaScript module.  It is also the
 * preferred way of doing build-time configuration if runtime configuration isn't used by your
 * deployment of the platform.
 *
 * Exporting a config object:
 * ```
 * const config = {
 *   LMS_BASE_URL: 'http://localhost:18000'
 * };
 *
 * export default config;
 * ```
 *
 * Exporting a function that returns an object:
 * ```
 * function getConfig() {
 *   return {
 *     LMS_BASE_URL: 'http://localhost:18000'
 *   };
 * }
 * ```
 *
 * Exporting a function that returns a promise that resolves to an object:
 * ```
 * function getAsyncConfig() {
 *   return new Promise((resolve, reject) => {
 *     resolve({
 *       LMS_BASE_URL: 'http://localhost:18000'
 *     });
 *   });
 * }
 *
 * export default getAsyncConfig;
 * ```
 *
 * ##### Runtime Configuration
 *
 * Configuration variables can also be supplied using the "runtime configuration" method, taking
 * advantage of the Micro-frontend Config API in edx-platform. More information on this API can be
 * found in the ADR which introduced it:
 *
 * https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst
 *
 * The runtime configuration method can be enabled by supplying a MFE_CONFIG_API_URL via one of the other
 * two configuration methods above.
 *
 * Runtime configuration is particularly useful if you need to supply different configurations to
 * a single deployment of a micro-frontend, for instance.  It is also a perfectly valid alternative
 * to build-time configuration, though it introduces an additional API call to edx-platform on MFE
 * initialization.
 *
 * ##### Initialization Config Handler
 *
 * The configuration document can be extended by
 * applications at run-time using a `config` initialization handler.  Please see the Initialization
 * documentation for more information on handlers and initialization phases.
 *
 * ```
 * initialize({
 *   handlers: {
 *     config: () => {
 *       mergeConfig({
 *         CUSTOM_VARIABLE: 'custom value',
 *         LMS_BASE_URL: 'http://localhost:18001' // You can override variables, but this is uncommon.
  *       }, 'App config override handler');
 *     },
 *   },
 * });
 * ```
 *
 * @module Config
 */

import { APP_CONFIG_INITIALIZED, CONFIG_CHANGED } from './constants';

import { publish, subscribe } from './pubSub';
import { ensureDefinedConfig } from './utils';

function extractRegex(envVar) {
  // Convert the environment variable string to a regex, while guarding
  // against a non-string and an empty/whitespace-only string.
  if (typeof envVar === 'string' && envVar.trim() !== '') {
    return new RegExp(envVar);
  }
  return undefined;
}

const ENVIRONMENT = process.env.NODE_ENV;
let config = {
  ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
  ACCOUNT_PROFILE_URL: process.env.ACCOUNT_PROFILE_URL,
  ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL,
  BASE_URL: process.env.BASE_URL,
  PUBLIC_PATH: process.env.PUBLIC_PATH || '/',
  CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL,
  CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
  DISCOVERY_API_BASE_URL: process.env.DISCOVERY_API_BASE_URL,
  PUBLISHER_BASE_URL: process.env.PUBLISHER_BASE_URL,
  ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
  ENVIRONMENT,
  IGNORED_ERROR_REGEX: extractRegex(process.env.IGNORED_ERROR_REGEX),
  LANGUAGE_PREFERENCE_COOKIE_NAME: process.env.LANGUAGE_PREFERENCE_COOKIE_NAME,
  LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
  LMS_BASE_URL: process.env.LMS_BASE_URL,
  LOGIN_URL: process.env.LOGIN_URL,
  LOGOUT_URL: process.env.LOGOUT_URL,
  STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
  MARKETING_SITE_BASE_URL: process.env.MARKETING_SITE_BASE_URL,
  ORDER_HISTORY_URL: process.env.ORDER_HISTORY_URL,
  REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
  SECURE_COOKIES: ENVIRONMENT !== 'development',
  SEGMENT_KEY: process.env.SEGMENT_KEY,
  SITE_NAME: process.env.SITE_NAME,
  USER_INFO_COOKIE_NAME: process.env.USER_INFO_COOKIE_NAME,
  LOGO_URL: process.env.LOGO_URL,
  LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
  LOGO_WHITE_URL: process.env.LOGO_WHITE_URL,
  FAVICON_URL: process.env.FAVICON_URL,
  MFE_CONFIG_API_URL: process.env.MFE_CONFIG_API_URL,
  APP_ID: process.env.APP_ID,
  SUPPORT_URL: process.env.SUPPORT_URL,
};

/**
 * Getter for the application configuration document.  This is synchronous and merely returns a
 * reference to an existing object, and is thus safe to call as often as desired.
 *
 * Example:
 *
 * ```
 * import { getConfig } from '@edx/frontend-platform';
 *
 * const {
 *   LMS_BASE_URL,
 * } = getConfig();
 * ```
 *
 * @returns {ConfigDocument}
  */
export function getConfig() {
  return config;
}

/**
 * Replaces the existing ConfigDocument.  This is not commonly used, but can be helpful for tests.
 *
 * The supplied config document will be tested with `ensureDefinedConfig` to ensure it does not
 * have any `undefined` keys.
 *
 * Example:
 *
 * ```
 * import { setConfig } from '@edx/frontend-platform';
 *
 * setConfig({
 *   LMS_BASE_URL, // This is overriding the ENTIRE document - this is not merged in!
 * });
 * ```
 *
 * @param {ConfigDocument} newConfig
 */
export function setConfig(newConfig) {
  ensureDefinedConfig(config, 'config');
  config = newConfig;
  publish(CONFIG_CHANGED);
}

/**
 * Merges additional configuration values into the ConfigDocument returned by `getConfig`.  Will
 * override any values that exist with the same keys.
 *
 * ```
 * mergeConfig({
 *   NEW_KEY: 'new value',
 *   OTHER_NEW_KEY: 'other new value',
 * });
 *
 * If any of the key values are `undefined`, an error will be logged to 'warn'.
 *
 * @param {Object} newConfig
 */
export function mergeConfig(newConfig) {
  ensureDefinedConfig(newConfig, 'ProcessEnvConfigService');
  config = Object.assign(config, newConfig);
  publish(CONFIG_CHANGED);
}

/**
 * A method allowing application code to indicate that particular ConfigDocument keys are required
 * for them to function.  This is useful for diagnosing development/deployment issues, primarily,
 * by surfacing misconfigurations early.  For instance, if the build process fails to supply an
 * environment variable on the command-line, it's possible that one of the `process.env` variables
 * will be undefined.  Should be used in conjunction with `mergeConfig` for custom `ConfigDocument`
 * properties.  Requester is for informational/error reporting purposes only.
 *
 * ```
 * ensureConfig(['LMS_BASE_URL', 'LOGIN_URL'], 'MySpecialComponent');
 *
 * // Will log a warning with:
 * // "App configuration error: LOGIN_URL is required by MySpecialComponent."
 * // if LOGIN_URL is undefined, for example.
 * ```
 *
 * *NOTE*: `ensureConfig` waits until `APP_CONFIG_INITIALIZED` is published to verify the existence
 * of the specified properties.  This means that this function is compatible with custom `config`
 * phase handlers responsible for loading additional configuration data in the initialization
 * sequence.
 *
 * @param {Array} keys
 * @param {string} [requester='unspecified application code']
 */
export function ensureConfig(keys, requester = 'unspecified application code') {
  subscribe(APP_CONFIG_INITIALIZED, () => {
    keys.forEach((key) => {
      if (config[key] === undefined) {
        // eslint-disable-next-line no-console
        console.warn(`App configuration error: ${key} is required by ${requester}.`);
      }
    });
  });
}

/**
 * An object describing the current application configuration.
 *
 * In its most basic form, the initialization process loads this document via `process.env`
 * variables.  There are other ways to add configuration variables to the ConfigDocument as
 * documented above (JavaScript File Configuration, Runtime Configuration, and the Initialization
 * Config Handler)
 *
 * ```
 * {
 *   BASE_URL: process.env.BASE_URL,
 *   // ... other vars
 * }
 * ```
 *
 * When using Webpack (i.e., normal usage), the build process is responsible for supplying these
 * variables via command-line environment variables.  That means they must be supplied at build
 * time.
 *
 * @name ConfigDocument
 * @memberof module:Config
 * @property {string} ACCESS_TOKEN_COOKIE_NAME
 * @property {string} ACCOUNT_PROFILE_URL
 * @property {string} ACCOUNT_SETTINGS_URL
 * @property {string} BASE_URL The URL of the current application.
 * @property {string} CREDENTIALS_BASE_URL
 * @property {string} CSRF_TOKEN_API_PATH
 * @property {string} DISCOVERY_API_BASE_URL
 * @property {string} PUBLISHER_BASE_URL
 * @property {string} ECOMMERCE_BASE_URL
 * @property {string} ENVIRONMENT This is one of: development, production, or test.
 * @property {string} IGNORED_ERROR_REGEX
 * @property {string} LANGUAGE_PREFERENCE_COOKIE_NAME
 * @property {string} LEARNING_BASE_URL
 * @property {string} LMS_BASE_URL
 * @property {string} LOGIN_URL
 * @property {string} LOGOUT_URL
 * @property {string} STUDIO_BASE_URL
 * @property {string} MARKETING_SITE_BASE_URL
 * @property {string} ORDER_HISTORY_URL
 * @property {string} REFRESH_ACCESS_TOKEN_ENDPOINT
 * @property {boolean} SECURE_COOKIES
 * @property {string} SEGMENT_KEY
 * @property {string} SITE_NAME
 * @property {string} USER_INFO_COOKIE_NAME
 * @property {string} LOGO_URL
 * @property {string} LOGO_TRADEMARK_URL
 * @property {string} LOGO_WHITE_URL
 * @property {string} FAVICON_URL
 * @property {string} MFE_CONFIG_API_URL
 * @property {string} APP_ID
 * @property {string} SUPPORT_URL
 */