Skip to content

Commit b18d520

Browse files
authored
feat: JavaScript file configuration of apps (#474)
* feat: JavaScript file configuration of apps This adds the ability to configure an application via an env.config.js file rather than environment variables or .env files. This mechanism is much more flexible and powerful than environment variables as described in the associated ADR. Note that this is not in _use_ by any MFEs as of this PR being merged - this merely adds the capability. Guidance will be forthcoming on how to use this mechanism, along with a DEPR issue for the deprecation of environment variable configuration later this year. It also improves documentation around how to configure applications, providing more detail on the various methods. Finally, it allows the logging, analytics, and auth services to be configured via the configuration document now that we can supply an alternate implementation in an env.config.js file. This allows configuration of these services without forking the MFE. We have agreed we’d like to DEPR environment variable config, so I’m going to merge the ADR as “Accepted”
1 parent 3f6ab4f commit b18d520

9 files changed

+463
-22
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
Promote JavaScript file configuration and deprecate environment variable configuration
2+
======================================================================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
Our webpack build process allows us to set environment variables on the command
13+
line or via .env files. These environment variables are available in the
14+
application via ``process.env``.
15+
16+
The implementation of this uses templatization and string interpolation to
17+
replace any instance of ``process.env.XXXX`` with the value of the environment
18+
variable named ``XXXX``. As an example, in our source code we may write::
19+
20+
const LMS_BASE_URL = process.env.LMS_BASE_URL;
21+
22+
After the build process runs, the compiled source code will instead read::
23+
24+
const LMS_BASE_URL = 'http://localhost:18000';
25+
26+
Put another way, `process.env` is not actually an object available at runtime,
27+
it's a templatization token that helps the build replace it with a string
28+
literal.
29+
30+
This approach has several important limitations:
31+
32+
- There's no way to add variables without hard-coding process.env.XXXX
33+
somewhere in the file, complicating our ability to add additional
34+
application-specific configuration without explicitly merging it into the
35+
configuration document after it's been created in frontend-platform.
36+
- The method can *only* handle strings.
37+
38+
Other data types are converted to strings::
39+
40+
# Build command:
41+
BOOLEAN_VAR=false NULL_VAR=null NUMBER_VAR=123 npm run build
42+
43+
...
44+
45+
// Source code:
46+
const BOOLEAN_VAR = process.env.BOOLEAN_VAR;
47+
const NULL_VAR = process.env.NULL_VAR;
48+
const NUMBER_VAR = process.env.NUMBER_VAR;
49+
50+
...
51+
52+
// Compiled source after the build runs:
53+
const BOOLEAN_VAR = "false";
54+
const NULL_VAR = "null";
55+
const NUMBER_VAR = "123";
56+
57+
This is not good!
58+
59+
- It makes it very difficult to supply array and object configuration
60+
variables, and unreasonable to supply class or function config since we'd
61+
have to ``eval()`` them.
62+
63+
Related to all this, frontend-platform has long had the ability to replace the
64+
implementations of its analytics, auth, and logging services, but no way to
65+
actually *configure* the app with a new implementation. Because of the above
66+
limitations, there's no reasonable way to configure a JavaScript class via
67+
environment variables.
68+
69+
Decision
70+
--------
71+
72+
For the above reasons, we will deprecate environment variable configuration in
73+
favor of JavaScript file configuration.
74+
75+
This method makes use of an ``env.config.js`` file to supply configuration
76+
variables to an application::
77+
78+
const config = {
79+
LMS_BASE_URL: 'http://localhost:18000',
80+
BOOLEAN_VAR: false,
81+
NULL_VAR: null,
82+
NUMBER_VAR: 123
83+
};
84+
85+
export default config;
86+
87+
This file is imported by the frontend-build webpack build process if it exists,
88+
and expected by frontend-platform as part of its initialization process. If the
89+
file doesn't exist, frontend-build falls back to importing an empty object for
90+
backwards compatibility. This functionality already exists today in
91+
frontend-build in preparation for using it here in frontend-platform.
92+
93+
This interdependency creates a peerDependency for frontend-platform on `frontend-build v8.1.0 <frontend_build_810_>`_ or
94+
later.
95+
96+
Using a JavaScript file for configuration is standard practice in the
97+
JavaScript/node community. Babel, webpack, eslint, Jest, etc., all accept
98+
configuration via JavaScript files (which we take advantage of in
99+
frontend-build), so there is ample precedent for using a .js file for
100+
configuration.
101+
102+
In order to achieve deprecation of environment variable configuration, we will
103+
follow the deprecation process described in
104+
`OEP-21: Deprecation and Removal <oep21_>`_. In addition, we will add
105+
build-time warnings to frontend-build indicating the deprecation of environment
106+
variable configuration. Practically speaking, this will mean adjusting build
107+
processes throughout the community and in common tools like Tutor.
108+
109+
Relationship to runtime configuration
110+
*************************************
111+
112+
JavaScript file configuration is compatible with runtime MFE configuration.
113+
frontend-platform loads configuration in a predictable order:
114+
115+
- environment variable config
116+
- optional handlers (commonly used to merge MFE-specific config in via additional
117+
process.env variables)
118+
- JS file config
119+
- runtime config
120+
121+
In the end, runtime config wins. That said, JS file config solves some use
122+
cases that runtime config can't solve around extensibility and customization.
123+
124+
In the future if we deprecate environment variable config, it's likely that
125+
we keep both JS file config and runtime configuration around. JS file config
126+
primarily to handle extensibility, and runtime config for everything else.
127+
128+
Rejected Alternatives
129+
---------------------
130+
131+
Another option was to use JSON files for this purpose. This solves some of our
132+
issues (limited use of non-string primitive data types) but is otherwise not
133+
nearly as expressive or flexible as using a JavaScript file directly.
134+
Anecdotally, in the past frontend-build used JSON versions of many of
135+
its configuration files (Babel, eslint, jest) but over time they were all
136+
converted to JavaScript files so we could express more complicated
137+
configuration needs. Since one of the primary use cases and reasons we need a
138+
new configuration method is to allow developers to supply alternate
139+
implementations of frontend-platform's core services (analytics, logging), JSON
140+
was effectively a non-starter.
141+
142+
.. _oep21: https://docs.openedx.org/projects/openedx-proposals/en/latest/processes/oep-0021-proc-deprecation.html
143+
.. _frontend_build_810: https://github.com/openedx/frontend-build/releases/tag/v8.1.0

env.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// NOTE: This file is used by the example app. frontend-build expects the file
2+
// to be in the root of the repository. This is not used by the actual frontend-platform library.
3+
// Also note that in an actual application this file would be added to .gitignore.
4+
const config = {
5+
JS_FILE_VAR: 'JS_FILE_VAR_VALUE_FOR_EXAMPLE_APP',
6+
};
7+
8+
export default config;

example/ExamplePage.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mergeConfig({
1313

1414
ensureConfig([
1515
'EXAMPLE_VAR',
16+
'JS_FILE_VAR',
1617
], 'ExamplePage');
1718

1819
class ExamplePage extends Component {
@@ -45,6 +46,7 @@ class ExamplePage extends Component {
4546
<p>{this.props.intl.formatMessage(messages['example.message'])}</p>
4647
{this.renderAuthenticatedUser()}
4748
<p>EXAMPLE_VAR env var came through: <strong>{getConfig().EXAMPLE_VAR}</strong></p>
49+
<p>JS_FILE_VAR var came through: <strong>{getConfig().JS_FILE_VAR}</strong></p>
4850
<p>Visit <Link to="/authenticated">authenticated page</Link>.</p>
4951
<p>Visit <Link to="/error_example">error page</Link>.</p>
5052
</div>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
},
7676
"peerDependencies": {
7777
"@edx/paragon": ">= 10.0.0 < 21.0.0",
78+
"@edx/frontend-build": ">= 8.1.0",
7879
"prop-types": "^15.7.2",
7980
"react": "^16.9.0 || ^17.0.0",
8081
"react-dom": "^16.9.0 || ^17.0.0",

src/config.js

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,122 @@
22
* #### Import members from **@edx/frontend-platform**
33
*
44
* The configuration module provides utilities for working with an application's configuration
5-
* document (ConfigDocument). This module uses `process.env` to import configuration variables
6-
* from the command-line build process. It can be dynamically extended at run-time using a
7-
* `config` initialization handler. Please see the Initialization documentation for more
8-
* information on handlers and initialization phases.
5+
* document (ConfigDocument). Configuration variables can be supplied to the
6+
* application in four different ways. They are applied in the following order:
7+
*
8+
* - Build-time Configuration
9+
* - Environment Variables
10+
* - JavaScript File
11+
* - Runtime Configuration
12+
*
13+
* Last one in wins. Variables with the same name defined via the later methods will override any
14+
* defined using an earlier method. i.e., if a variable is defined in Runtime Configuration, that
15+
* will override the same variable defined in either Build-time Configuration method (environment
16+
* variables or JS file). Configuration defined in a JS file will override environment variables.
17+
*
18+
* ##### Build-time Configuration
19+
*
20+
* Build-time configuration methods add config variables into the app when it is built by webpack.
21+
* This saves the app an API call and means it has all the information it needs to initialize right
22+
* away. There are two methods of supplying build-time configuration: environment variables and a
23+
* JavaScript file.
24+
*
25+
* ###### Environment Variables
26+
*
27+
* A set list of required config variables can be supplied as
28+
* command-line environment variables during the build process.
29+
*
30+
* As a simple example, these are supplied on the command-line before invoking `npm run build`:
931
*
1032
* ```
11-
* import { getConfig } from '@edx/frontend-platform';
33+
* LMS_BASE_URL=http://localhost:18000 npm run build
34+
* ```
1235
*
13-
* const {
14-
* BASE_URL,
15-
* LMS_BASE_URL,
16-
* LOGIN_URL,
17-
* LOGIN_URL,
18-
* REFRESH_ACCESS_TOKEN_ENDPOINT,
19-
* ACCESS_TOKEN_COOKIE_NAME,
20-
* CSRF_TOKEN_API_PATH,
21-
* } = getConfig();
36+
* Note that additional variables _cannot_ be supplied via this method without using the `config`
37+
* initialization handler. The app won't pick them up and they'll appear `undefined`.
38+
*
39+
* This configuration method is being deprecated in favor of JavaScript File Configuration.
40+
*
41+
* ###### JavaScript File Configuration
42+
*
43+
* Configuration variables can be supplied in an optional file named env.config.js. This file must
44+
* export either an Object containing configuration variables or a function. The function must
45+
* return an Object containing configuration variables or, alternately, a promise which resolves to
46+
* an Object.
47+
*
48+
* Using a function or async function allows the configuration to be resolved at runtime (because
49+
* the function will be executed at runtime). This is not common, and the capability is included
50+
* for the sake of flexibility.
51+
*
52+
* JavaScript File Configuration is well-suited to extensibility use cases or component overrides,
53+
* in that the configuration file can depend on any installed JavaScript module. It is also the
54+
* preferred way of doing build-time configuration if runtime configuration isn't used by your
55+
* deployment of the platform.
56+
*
57+
* Exporting a config object:
58+
* ```
59+
* const config = {
60+
* LMS_BASE_URL: 'http://localhost:18000'
61+
* };
62+
*
63+
* export default config;
64+
* ```
65+
*
66+
* Exporting a function that returns an object:
67+
* ```
68+
* function getConfig() {
69+
* return {
70+
* LMS_BASE_URL: 'http://localhost:18000'
71+
* };
72+
* }
73+
* ```
74+
*
75+
* Exporting a function that returns a promise that resolves to an object:
76+
* ```
77+
* function getAsyncConfig() {
78+
* return new Promise((resolve, reject) => {
79+
* resolve({
80+
* LMS_BASE_URL: 'http://localhost:18000'
81+
* });
82+
* });
83+
* }
84+
*
85+
* export default getAsyncConfig;
86+
* ```
87+
*
88+
* ##### Runtime Configuration
89+
*
90+
* Configuration variables can also be supplied using the "runtime configuration" method, taking
91+
* advantage of the Micro-frontend Config API in edx-platform. More information on this API can be
92+
* found in the ADR which introduced it:
93+
*
94+
* https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst
95+
*
96+
* The runtime configuration method can be enabled by supplying a MFE_CONFIG_API_URL via one of the other
97+
* two configuration methods above.
98+
*
99+
* Runtime configuration is particularly useful if you need to supply different configurations to
100+
* a single deployment of a micro-frontend, for instance. It is also a perfectly valid alternative
101+
* to build-time configuration, though it introduces an additional API call to edx-platform on MFE
102+
* initialization.
103+
*
104+
* ##### Initialization Config Handler
105+
*
106+
* The configuration document can be extended by
107+
* applications at run-time using a `config` initialization handler. Please see the Initialization
108+
* documentation for more information on handlers and initialization phases.
109+
*
110+
* ```
111+
* initialize({
112+
* handlers: {
113+
* config: () => {
114+
* mergeConfig({
115+
* CUSTOM_VARIABLE: 'custom value',
116+
* LMS_BASE_URL: 'http://localhost:18001' // You can override variables, but this is uncommon.
117+
* }, 'App config override handler');
118+
* },
119+
* },
120+
* });
22121
* ```
23122
*
24123
* @module Config
@@ -76,8 +175,17 @@ let config = {
76175

77176
/**
78177
* Getter for the application configuration document. This is synchronous and merely returns a
79-
* reference to an existing object, and is thus safe to call as often as desired. The document
80-
* should have the following keys at a minimum:
178+
* reference to an existing object, and is thus safe to call as often as desired.
179+
*
180+
* Example:
181+
*
182+
* ```
183+
* import { getConfig } from '@edx/frontend-platform';
184+
*
185+
* const {
186+
* LMS_BASE_URL,
187+
* } = getConfig();
188+
* ```
81189
*
82190
* @returns {ConfigDocument}
83191
*/
@@ -91,6 +199,16 @@ export function getConfig() {
91199
* The supplied config document will be tested with `ensureDefinedConfig` to ensure it does not
92200
* have any `undefined` keys.
93201
*
202+
* Example:
203+
*
204+
* ```
205+
* import { setConfig } from '@edx/frontend-platform';
206+
*
207+
* setConfig({
208+
* LMS_BASE_URL, // This is overriding the ENTIRE document - this is not merged in!
209+
* });
210+
* ```
211+
*
94212
* @param {ConfigDocument} newConfig
95213
*/
96214
export function setConfig(newConfig) {
@@ -157,7 +275,10 @@ export function ensureConfig(keys, requester = 'unspecified application code') {
157275
/**
158276
* An object describing the current application configuration.
159277
*
160-
* The implementation loads this document via `process.env` variables.
278+
* In its most basic form, the initialization process loads this document via `process.env`
279+
* variables. There are other ways to add configuration variables to the ConfigDocument as
280+
* documented above (JavaScript File Configuration, Runtime Configuration, and the Initialization
281+
* Config Handler)
161282
*
162283
* ```
163284
* {

0 commit comments

Comments
 (0)