diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 75c6f350..c7ec84db 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,12 +26,12 @@ jobs:
npm run build
npm pack
mv openedx-frontend-base* openedx-frontend-base.tgz
- cd test-project
+ cd test-site
npm i ../openedx-frontend-base.tgz
npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- - name: Test Project
- run: npm run test-project
+ - name: Build Test Site
+ run: npm run test-site:build
diff --git a/.gitignore b/.gitignore
index d043b493..db3cc0ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,4 @@ scss
node_modules
npm-debug.log
docs/api
-/openedx-frontend-base-1.0.0.tgz
+/*.tgz
diff --git a/docs/decisions/0006-middleware-support-for-http-clients.rst b/docs/decisions/0006-middleware-support-for-http-clients.rst
index c162446f..c9470529 100644
--- a/docs/decisions/0006-middleware-support-for-http-clients.rst
+++ b/docs/decisions/0006-middleware-support-for-http-clients.rst
@@ -35,7 +35,7 @@ If a consumer chooses not to use ``initialize`` and instead the ``configureAuth`
configureAuth({
loggingService: getLoggingService(),
- config: getConfig(),
+ config: getSiteConfig(),
options: {
middleware: [axiosCaseConverter, (client) => axiosRetry(client, { retries: 3 })]
}
diff --git a/docs/decisions/0008-stylesheet-import-in-site-config.md b/docs/decisions/0008-stylesheet-import-in-site-config.md
index 4888ad1f..4df16b2e 100644
--- a/docs/decisions/0008-stylesheet-import-in-site-config.md
+++ b/docs/decisions/0008-stylesheet-import-in-site-config.md
@@ -18,7 +18,7 @@ As a best practice, a project should have a top-level SCSS file as a peer to the
## Implementation
-The `project.scss` file should import the stylesheet from the shell:
+The `site.scss` file should import the stylesheet from the shell:
```diff
+ @import '@openedx/frontend-base/shell/app.scss';
@@ -29,11 +29,11 @@ The `project.scss` file should import the stylesheet from the shell:
The site.config file should then import the top-level SCSS file:
```diff
-+ import './project.scss';
++ import './site.scss';
-const config = {
+const siteConfig = {
// config document
}
-export default config;
+export default siteConfig;
```
diff --git a/docs/decisions/0009-slot-naming-and-lifecycle.rst b/docs/decisions/0009-slot-naming-and-lifecycle.rst
index 3735e13a..c2c1693b 100644
--- a/docs/decisions/0009-slot-naming-and-lifecycle.rst
+++ b/docs/decisions/0009-slot-naming-and-lifecycle.rst
@@ -106,7 +106,7 @@ Where:
For example:
-* org.openedx.frontend.slot.devProject.foobar (unsupported slot, as version is empty)
+* org.openedx.frontend.slot.devSite.foobar (unsupported slot, as version is empty)
* org.openedx.frontend.slot.footer.main.unstable (unstable slot)
* org.openedx.frontend.slot.learning.navigationSidebar.v2 (this slot is on version 2)
diff --git a/docs/how_tos/automatic-case-conversion.rst b/docs/how_tos/automatic-case-conversion.rst
index 38e82c0a..33ffdb2b 100644
--- a/docs/how_tos/automatic-case-conversion.rst
+++ b/docs/how_tos/automatic-case-conversion.rst
@@ -32,7 +32,7 @@ Or, if you choose to use ``configureAuth`` instead::
configureAuth({
loggingService: getLoggingService(),
- config: getConfig(),
+ config: getSiteConfig(),
options: {
middleware: [axiosCaseConverter, (client) => axiosRetry(client, { retries: 3 })]
}
diff --git a/docs/how_tos/i18n.rst b/docs/how_tos/i18n.rst
index 644c780e..ec301ac2 100644
--- a/docs/how_tos/i18n.rst
+++ b/docs/how_tos/i18n.rst
@@ -101,7 +101,7 @@ Load up your translation files
configureI18n({
messages,
- config: getConfig(), // environment and languagePreferenceCookieName are required
+ config: getSiteConfig(), // environment and languagePreferenceCookieName are required
loggingService: getLoggingService(), // An object with logError and logInfo methods
});
diff --git a/docs/how_tos/migrate-frontend-app.md b/docs/how_tos/migrate-frontend-app.md
index 489a2862..c14e9593 100644
--- a/docs/how_tos/migrate-frontend-app.md
+++ b/docs/how_tos/migrate-frontend-app.md
@@ -127,10 +127,8 @@ With the exception of any custom scripts, replace the `scripts` section of your
```
"scripts": {
- "build": "PORT=YOUR_PORT openedx build",
- "build:legacy": "openedx build:legacy", // TODO: Does this target exist?
+ "build": "openedx build",
"dev": "PORT=YOUR_PORT openedx dev",
- "dev:legacy": "PORT=YOUR_PORT openedx dev:legacy",
"i18n_extract": "openedx formatjs extract",
"lint": "openedx lint .",
"lint:fix": "openedx lint --fix .",
@@ -182,7 +180,7 @@ Create an `app.d.ts` file in the root of your MFE with the following contents:
/// ` tag, rather than using `ReactComponent`. You can see an example of normal SVG imports in `test-project/src/ExamplePage.tsx`.
+We have removed the `@svgr/webpack` loader because it was incompatible with more modern tooling (it requires Babel). As a result, the ability to import SVG files into JS as the `ReactComponent` export no longer works. We know of a total of 5 places where this is happening today in Open edX MFEs - frontend-app-learning and frontend-app-profile use it. Please replace that export with the default URL export and set the URL as the source of an `
` tag, rather than using `ReactComponent`. You can see an example of normal SVG imports in `test-site/src/ExamplePage.tsx`.
Import createConfig and getBaseConfig from @openedx/frontend-base/config
@@ -414,7 +411,7 @@ Replace all imports from @edx/frontend-platform with @openedx/frontend-base
- import { logInfo } from '@edx/frontend-platform/logging';
- import { FormattedMessage } from '@edx/frontend-platform/i18n';
+ import {
-+ getConfig,
++ getSiteConfig,
+ logInfo,
+ FormattedMessage
+ } from '@openedx/frontend-base';
@@ -490,7 +487,7 @@ Required config
The required configuration at the time of this writing is:
-- appId: string
+- siteId: string
- siteName: string
- baseUrl: string
- lmsBaseUrl: string
@@ -521,6 +518,7 @@ Note that the .env files and env.config.js files also include a number of URLs f
```
// Creating a route role with for 'example' in an App
const app: App = {
+ ...
routes: [{
path: '/example',
id: 'example.page',
@@ -538,17 +536,16 @@ const examplePageUrl = getUrlForRouteRole('example');
App-specific config values
--------------------------
-App-specific configuration can be expressed by adding a `custom` section to SiteConfig which allows arbitrary config variables.
+App-specific configuration can be expressed by adding an `config` section to the app, allowing arbitrary variables:
```
-const config: ProjectSiteConfig = {
- // ... Other config
-
- custom: {
+const app: App = {
+ ...
+ config: {
appId: 'myapp',
myCustomVariableName: 'my custom variable value',
- }
-}
+ },
+};
```
These variables can be used in code with the `getAppConfig` function:
@@ -557,49 +554,58 @@ These variables can be used in code with the `getAppConfig` function:
getAppConfig('myapp').myCustomVariableName
```
-If you have fully converted your app over to the new module architecture, you can add custom variables to the `config` object in your `App` definition and they will be available via `getAppConfig`.
+Or via `useAppConfig()` (with no need to specify the appId), if `AppProvider` is wrapping your app.
-Replace the .env.test file with configuration in test.site.config.tsx file
+Replace the .env.test file with configuration in site.config.test.tsx file
==========================================================================
-We're moving away from .env files because they're not expressive enough (only string types!) to configure an Open edX frontend. Instead, the test suite has been configured to expect a `test.site.config.tsx` file. If you're initializing an application in your tests, `frontend-base` will pick up this configuration and make it available to `getConfig()`, etc. If you need to manually access the variables, you can import `site.config` in your test files:
-
-Note that test.site.config.tsx has a different naming scheme than `site.config.*.tsx` because it's intended to be checked in, and `site.config.*.tsx` is git-ignored.
+We're moving away from .env files because they're not expressive enough (only string types!) to configure an Open edX frontend. Instead, the test suite has been configured to expect a `site.config.test.tsx` file. If you're initializing an application in your tests, `frontend-base` will pick up this configuration and make it available to `getSiteConfig()`, etc. If you need to manually access the variables, you can import `site.config` in your test files:
```diff
-+ import config from 'site.config';
++ import siteConfig from 'site.config';
```
-The Jest configuration has been set up to find `site.config` in a `test.site.config.tsx` file.
+The Jest configuration has been set up to find `site.config` in a `site.config.test.tsx` file.
Once you've verified your test suite still works, you should delete the `.env.test` file.
-A sample `test.site.config.tsx` file:
+A sample `site.config.test.tsx` file:
```
-import { ProjectSiteConfig } from '@openedx/frontend-base';
+import { SiteConfig } from '@openedx/frontend-base';
-const config: ProjectSiteConfig = {
- apps: [],
- accessTokenCookieName: 'edx-jwt-cookie-header-payload',
+const siteConfig: SiteConfig = {
+ siteId: 'test',
+ siteName: 'localhost',
baseUrl: 'http://localhost:8080',
- csrfTokenApiPath: '/csrf/api/v1/token',
- languagePreferenceCookieName: 'openedx-language-preference',
lmsBaseUrl: 'http://localhost:18000',
loginUrl: 'http://localhost:18000/login',
logoutUrl: 'http://localhost:18000/logout',
+ environment: 'dev',
+ apps: [{
+ appId: 'test-app',
+ routes: [{
+ path: '/app1',
+ element: (
+
Authenticated
} />Authenticated
} />Authenticated
} />This slot has no opinionated layout, it just renders its children.
-This slot has default content, and it exposes a slot prop to widgets.
-This slot uses a horizontal flexbox layout from a component.
-This slot uses a horizontal flexbox layout from a JSX element.
-This slot uses a horizontal flexbox layout, but it was added by a layout replace operation.
-These slots use a custom layout that takes options. The first shows the default title, the second shows it set to "Bar"
-This slot has a prepended element (and two appended elements).
-This slot has elements inserted before and after the second element. Also note that the insert operations are declared before the related element is declared, but can still insert themselves relative to it.
-This slot has an element replacing element two.
-This slot has removed element two (WidgetOperationTypes.REMOVE).
Both widgets accept options. The first shows the default title, the second shows it set to "Bar"
-Visit error page.
Is context.config equal to getConfig()? {printTestResult(config === getConfig())}
+Is context.config equal to getSiteConfig()? {printTestResult(config === getSiteConfig())}
Is context.authenticatedUser equal to getAuthenticatedUser()? {printTestResult(authenticatedUser === getAuthenticatedUser())}
Photo by Louis Hansel @shotsoflouis on Unsplash
{intl.formatMessage(messages['test-project.message'])}
+{intl.formatMessage(messages['test-site.message'])}
- This is a component that lives in the test-project but is loaded into the page via an iframe. This emulates a real-world scenario. It is NOT testing for cross-origin security issues though, since it's on the same host name. + This is a component that lives in the test-site but is loaded into the page via an iframe. This emulates a real-world scenario. It is NOT testing for cross-origin security issues though, since it's on the same host name.
1?t[1]=i:t.push(i)}else t[0]&&t[0].headers&&e(t[0].headers,o)&&(this.dt=o)}),u.on("fetch-start",function(t,e){this.params={},this.metrics={},this.startTime=a.now(),this.dt=e,t.length>=1&&(this.target=t[0]),t.length>=2&&(this.opts=t[1]);var n,r=this.opts||{},i=this.target;"string"==typeof i?n=i:"object"==typeof i&&i instanceof g?n=i.url:window.URL&&"object"==typeof i&&i instanceof URL&&(n=i.href),o(this,n);var s=(""+(i&&i instanceof g&&i.method||r.method||"GET")).toUpperCase();this.params.method=s,this.txSize=m(r.body)||0}),u.on("fetch-done",function(t,e){this.endTime=a.now(),this.params||(this.params={}),this.params.status=e?e.status:0;var n;"string"==typeof this.rxSize&&this.rxSize.length>0&&(n=+this.rxSize);var r={txSize:this.txSize,rxSize:n,duration:a.now()-this.startTime};s("xhr",[this.params,r,this.startTime,this.endTime,"fetch"],this)})}},{}],18:[function(t,e,n){var r={};e.exports=function(t){if(t in r)return r[t];var e=document.createElement("a"),n=window.location,o={};e.href=t,o.port=e.port;var i=e.href.split("://");!o.port&&i[1]&&(o.port=i[1].split("/")[0].split("@").pop().split(":")[1]),o.port&&"0"!==o.port||(o.port="https"===i[0]?"443":"80"),o.hostname=e.hostname||n.hostname,o.pathname=e.pathname,o.protocol=i[0],"/"!==o.pathname.charAt(0)&&(o.pathname="/"+o.pathname);var a=!e.protocol||":"===e.protocol||e.protocol===n.protocol,s=e.hostname===document.domain&&e.port===n.port;return o.sameOrigin=a&&(!e.hostname||s),"/"===o.pathname&&(r[t]=o),o}},{}],19:[function(t,e,n){function r(t,e){var n=t.responseType;return"json"===n&&null!==e?e:"arraybuffer"===n||"blob"===n||"json"===n?o(t.response):"text"===n||""===n||void 0===n?o(t.responseText):void 0}var o=t(22);e.exports=r},{}],20:[function(t,e,n){function r(){}function o(t,e,n,r){return function(){return u.recordSupportability("API/"+e+"/called"),i(t+e,[f.now()].concat(s(arguments)),n?null:this,r),n?void 0:this}}var i=t("handle"),a=t(32),s=t(33),c=t("ee").get("tracer"),f=t("loader"),u=t(25),d=NREUM;"undefined"==typeof window.newrelic&&(newrelic=d);var p=["setPageViewName","setCustomAttribute","setErrorHandler","finished","addToTrace","inlineHit","addRelease"],l="api-",h=l+"ixn-";a(p,function(t,e){d[e]=o(l,e,!0,"api")}),d.addPageAction=o(l,"addPageAction",!0),d.setCurrentRouteName=o(l,"routeName",!0),e.exports=newrelic,d.interaction=function(){return(new r).get()};var m=r.prototype={createTracer:function(t,e){var n={},r=this,o="function"==typeof e;return i(h+"tracer",[f.now(),t,n],r),function(){if(c.emit((o?"":"no-")+"fn-start",[f.now(),r,o],n),o)try{return e.apply(this,arguments)}catch(t){throw c.emit("fn-err",[arguments,this,t],n),t}finally{c.emit("fn-end",[f.now()],n)}}}};a("actionText,setName,setAttribute,save,ignore,onEnd,getContext,end,get".split(","),function(t,e){m[e]=o(h,e)}),newrelic.noticeError=function(t,e){"string"==typeof t&&(t=new Error(t)),u.recordSupportability("API/noticeError/called"),i("err",[t,f.now(),!1,e])}},{}],21:[function(t,e,n){function r(t){if(NREUM.init){for(var e=NREUM.init,n=t.split("."),r=0;r 1?t[1]=i:t.push(i)}else t[0]&&t[0].headers&&e(t[0].headers,o)&&(this.dt=o)}),u.on("fetch-start",function(t,e){this.params={},this.metrics={},this.startTime=a.now(),this.dt=e,t.length>=1&&(this.target=t[0]),t.length>=2&&(this.opts=t[1]);var n,r=this.opts||{},i=this.target;"string"==typeof i?n=i:"object"==typeof i&&i instanceof g?n=i.url:window.URL&&"object"==typeof i&&i instanceof URL&&(n=i.href),o(this,n);var s=(""+(i&&i instanceof g&&i.method||r.method||"GET")).toUpperCase();this.params.method=s,this.txSize=m(r.body)||0}),u.on("fetch-done",function(t,e){this.endTime=a.now(),this.params||(this.params={}),this.params.status=e?e.status:0;var n;"string"==typeof this.rxSize&&this.rxSize.length>0&&(n=+this.rxSize);var r={txSize:this.txSize,rxSize:n,duration:a.now()-this.startTime};s("xhr",[this.params,r,this.startTime,this.endTime,"fetch"],this)})}},{}],18:[function(t,e,n){var r={};e.exports=function(t){if(t in r)return r[t];var e=document.createElement("a"),n=window.location,o={};e.href=t,o.port=e.port;var i=e.href.split("://");!o.port&&i[1]&&(o.port=i[1].split("/")[0].split("@").pop().split(":")[1]),o.port&&"0"!==o.port||(o.port="https"===i[0]?"443":"80"),o.hostname=e.hostname||n.hostname,o.pathname=e.pathname,o.protocol=i[0],"/"!==o.pathname.charAt(0)&&(o.pathname="/"+o.pathname);var a=!e.protocol||":"===e.protocol||e.protocol===n.protocol,s=e.hostname===document.domain&&e.port===n.port;return o.sameOrigin=a&&(!e.hostname||s),"/"===o.pathname&&(r[t]=o),o}},{}],19:[function(t,e,n){function r(t,e){var n=t.responseType;return"json"===n&&null!==e?e:"arraybuffer"===n||"blob"===n||"json"===n?o(t.response):"text"===n||""===n||void 0===n?o(t.responseText):void 0}var o=t(22);e.exports=r},{}],20:[function(t,e,n){function r(){}function o(t,e,n,r){return function(){return u.recordSupportability("API/"+e+"/called"),i(t+e,[f.now()].concat(s(arguments)),n?null:this,r),n?void 0:this}}var i=t("handle"),a=t(32),s=t(33),c=t("ee").get("tracer"),f=t("loader"),u=t(25),d=NREUM;"undefined"==typeof window.newrelic&&(newrelic=d);var p=["setPageViewName","setCustomAttribute","setErrorHandler","finished","addToTrace","inlineHit","addRelease"],l="api-",h=l+"ixn-";a(p,function(t,e){d[e]=o(l,e,!0,"api")}),d.addPageAction=o(l,"addPageAction",!0),d.setCurrentRouteName=o(l,"routeName",!0),e.exports=newrelic,d.interaction=function(){return(new r).get()};var m=r.prototype={createTracer:function(t,e){var n={},r=this,o="function"==typeof e;return i(h+"tracer",[f.now(),t,n],r),function(){if(c.emit((o?"":"no-")+"fn-start",[f.now(),r,o],n),o)try{return e.apply(this,arguments)}catch(t){throw c.emit("fn-err",[arguments,this,t],n),t}finally{c.emit("fn-end",[f.now()],n)}}}};a("actionText,setName,setAttribute,save,ignore,onEnd,getContext,end,get".split(","),function(t,e){m[e]=o(h,e)}),newrelic.noticeError=function(t,e){"string"==typeof t&&(t=new Error(t)),u.recordSupportability("API/noticeError/called"),i("err",[t,f.now(),!1,e])}},{}],21:[function(t,e,n){function r(t){if(NREUM.init){for(var e=NREUM.init,n=t.split("."),r=0;r