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: /// declare module 'site.config' { - export default ProjectSiteConfig; + export default SiteConfig; } declare module '*.svg' { @@ -210,7 +208,6 @@ Create a `tsconfig.json` file and add the following contents to it: "babel.config.js", "eslint.config.js", "jest.config.js", - "test.site.config.tsx", "site.config.*.tsx", ], } @@ -306,13 +303,13 @@ module.exports = config; Merge site.config into config in setupTest.js ============================================= -frontend-platform used environment variables to seed the configuration object, meaning it had default values at the time code is loaded based on `process.env` variables. frontend-base has a hard-coded, minimal configuration object that _must_ be augmented by a valid site config file at initialization time. This means that any tests that rely on configuration (e.g., via `getConfig()`) must first initialize the configuration object. This can be done for tests by adding these lines to `setupTest.js`: +frontend-platform used environment variables to seed the configuration object, meaning it had default values at the time code is loaded based on `process.env` variables. frontend-base has a hard-coded, minimal configuration object that _must_ be augmented by a valid site config file at initialization time. This means that any tests that rely on configuration (e.g., via `getSiteConfig()`) must first initialize the configuration object. This can be done for tests by adding these lines to `setupTest.js`: ``` import siteConfig from 'site.config'; -import { mergeConfig } from '@openedx/frontend-base'; +import { mergeSiteConfig } from '@openedx/frontend-base'; -mergeConfig(siteConfig); +mergeSiteConfig(siteConfig); ``` @@ -388,7 +385,7 @@ Description fields are now required on all i18n messages in the repository. Thi SVGR "ReactComponent" imports have been removed =============================================== -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-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: ( +
Test App 1
+ ), + handle: { + role: 'test-app-1' + } + }] + }], + accessTokenCookieName: 'edx-jwt-cookie-header-payload', + csrfTokenApiPath: '/csrf/api/v1/token', + languagePreferenceCookieName: 'openedx-language-preference', refreshAccessTokenApiPath: '/login_refresh', segmentKey: '', - siteName: 'localhost', userInfoCookieName: 'edx-user-info', - appId: 'authn', - environment: 'dev', ignoredErrorRegex: null, publicPath: '/', }; -export default config; +export default siteConfig; ``` @@ -629,12 +635,12 @@ Remove core-js and regenerator-runtime We don't need these libraries anymore, remove them from the package.json dependencies and remove any imports of them in the code. -Create a project.scss file -========================== +Create a site.scss file +======================= This is required if you intend to run builds from the app itself. -Create a new `project.scss` file at the top of your application. It's responsible for: +Create a new `site.scss` file at the top of your application. It's responsible for: 1. Importing the shell's stylesheet, which includes Paragon's core stylesheet. 2. Importing your brand stylesheet. @@ -643,16 +649,16 @@ Create a new `project.scss` file at the top of your application. It's responsib You must then import this new stylesheet into your `site.config` file: ```diff -+ import './project.scss'; ++ import './site.scss'; -const config: ProjectSiteConfig = { +const siteConfig: SiteConfig = { // config document } -export default config; +export default siteConfig; ``` -This file will be ignored via `.gitignore`, as it is part of your 'project', not the module library. +This file will be ignored via `.gitignore`, as it is part of your 'site', not the module library. Document module-specific configuration needs @@ -731,7 +737,7 @@ Refactor plugin-slots First, rename `src/plugin-slots`, if it exists, to `src/slots`. Modify imports and documentation across the codebase accordingly. -Next, the frontend-base equivalent to `` is ``, and has a different API. This includes a change in the slot ID, according to the [new slot naming ADR](../decisions/0009-slot-naming-and-lifecycle.rst) in this repository. Rename them accordingly. You can refer to the `src/shell/dev-project` in this repository for examples. +Next, the frontend-base equivalent to `` is ``, and has a different API. This includes a change in the slot ID, according to the [new slot naming ADR](../decisions/0009-slot-naming-and-lifecycle.rst) in this repository. Rename them accordingly. You can refer to the `src/shell/dev-site` in this repository for examples. Find your module boundaries diff --git a/eslint.config.js b/eslint.config.js index b10cb1ab..079f4afc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,7 @@ module.exports = tseslint.config( { ignores: [ 'tools/*', - 'test-project/*', + 'test-site/*', 'config/*', 'docs/*', ], diff --git a/frontend-base.d.ts b/frontend-base.d.ts index 14df28b0..c0baca11 100644 --- a/frontend-base.d.ts +++ b/frontend-base.d.ts @@ -1,5 +1,5 @@ declare module 'site.config' { - export default ProjectSiteConfig; + export default SiteConfig; } declare module '*.svg' { diff --git a/package.json b/package.json index 71cb2177..07a41090 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "dev:shell": "npm run build && node ./tools/dist/cli/openedx.js dev:shell", "docs": "jsdoc -c jsdoc.json", "docs:watch": "nodemon -w runtime -w docs/template -w README.md -e js,jsx,ts,tsx --exec npm run docs", - "lint": "eslint .; npm run lint:tools; npm --prefix ./test-project run lint", + "lint": "eslint .; npm run lint:tools; npm --prefix ./test-site run lint", "lint:tools": "cd ./tools && eslint . && cd ..", "test": "jest", - "test-project": "npm --prefix ./test-project i; npm --prefix ./test-project run build", - "test-project:refresh": "npm run build && npm pack && cd test-project && npm i --audit=false --fund=false ../openedx-frontend-base-1.0.0.tgz && cd ../" + "test-site:build": "npm --prefix ./test-site i; npm --prefix ./test-site run build", + "test-site:refresh": "npm run build && npm pack && cd test-site && npm i --audit=false --fund=false ../openedx-frontend-base-1.0.0.tgz && cd ../" }, "repository": { "type": "git", diff --git a/runtime/analytics/interface.js b/runtime/analytics/interface.js index fda71053..a641c4ee 100755 --- a/runtime/analytics/interface.js +++ b/runtime/analytics/interface.js @@ -9,7 +9,7 @@ * * ``` * import { - * getConfig, + * getSiteConfig, * getAuthenticatedHttpClient, * getLoggingService, * configureAnalytics, @@ -17,7 +17,7 @@ * } from '@openedx/frontend-base'; * * configureAnalytics(SegmentAnalyticsService, { - * config: getConfig(), + * config: getSiteConfig(), * loggingService: getLoggingService(), * httpClient: getAuthenticatedHttpClient(), * }); diff --git a/runtime/auth/AxiosJwtAuthService.test.jsx b/runtime/auth/AxiosJwtAuthService.test.jsx index 540516ed..ddc704a1 100644 --- a/runtime/auth/AxiosJwtAuthService.test.jsx +++ b/runtime/auth/AxiosJwtAuthService.test.jsx @@ -2,7 +2,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import Cookies from 'universal-cookie'; -import { getConfig } from '../config'; +import { getSiteConfig } from '../config'; import AxiosJwtAuthService from './AxiosJwtAuthService'; const mockLoggingService = { @@ -12,7 +12,7 @@ const mockLoggingService = { }; const authOptions = { - config: getConfig(), + config: getSiteConfig(), loggingService: mockLoggingService, }; diff --git a/runtime/auth/MockAuthService.js b/runtime/auth/MockAuthService.js index 6f0508a8..48684226 100644 --- a/runtime/auth/MockAuthService.js +++ b/runtime/auth/MockAuthService.js @@ -45,14 +45,14 @@ const optionsPropTypes = { * you could do the following to set up a MockAuthService for your test: * * ``` - * import { getConfig, mergeConfig, configureAuth, MockAuthService } from '@openedx/frontend-base'; + * import { getSiteConfig, mergeSiteConfig, configureAuth, MockAuthService } from '@openedx/frontend-base'; * import MockAdapter from 'axios-mock-adapter'; * * const mockLoggingService = { * logInfo: jest.fn(), * logError: jest.fn(), * }; - * mergeConfig({ + * mergeSiteConfig({ * authenticatedUser: { * userId: 'abc123', * username: 'Mock User', @@ -60,7 +60,7 @@ const optionsPropTypes = { * administrator: false, * }, * }); - * configureAuth(MockAuthService, { config: getConfig(), loggingService: mockLoggingService }); + * configureAuth(MockAuthService, { config: getSiteConfig(), loggingService: mockLoggingService }); * const mockAdapter = new MockAdapter(getAuthenticatedHttpClient()); * // Mock calls for your tests. This configuration can be done in any sort of test setup. * mockAdapter.onGet(...); diff --git a/runtime/auth/interface.js b/runtime/auth/interface.js index be885fb7..5d6ebb2e 100644 --- a/runtime/auth/interface.js +++ b/runtime/auth/interface.js @@ -13,13 +13,13 @@ * configureAuth, * fetchAuthenticatedUser, * getAuthenticatedHttpClient, - * getConfig, + * getSiteConfig, * getLoggingService * } from '@openedx/frontend-base'; * * configureAuth({ * loggingService: getLoggingService(), - * config: getConfig(), + * config: getSiteConfig(), * }); * * const authenticatedUser = await fetchAuthenticatedUser(); // validates and decodes JWT token diff --git a/runtime/config/getExternalLinkUrl.test.js b/runtime/config/getExternalLinkUrl.test.js index 97452f81..20631725 100644 --- a/runtime/config/getExternalLinkUrl.test.js +++ b/runtime/config/getExternalLinkUrl.test.js @@ -1,19 +1,19 @@ -import { getExternalLinkUrl, setConfig } from '.'; +import { getExternalLinkUrl, setSiteConfig } from '.'; describe('getExternalLinkUrl', () => { afterEach(() => { // Reset config after each test to avoid cross-test pollution - setConfig({}); + setSiteConfig({}); }); it('should return the url passed in when externalLinkUrlOverrides is not set', () => { - setConfig({}); + setSiteConfig({}); const url = 'https://foo.example.com'; expect(getExternalLinkUrl(url)).toBe(url); }); it('should return the url passed in when externalLinkUrlOverrides does not have the url mapping', () => { - setConfig({ + setSiteConfig({ externalLinkUrlOverrides: { 'https://bar.example.com': 'https://mapped.example.com', }, @@ -25,39 +25,39 @@ describe('getExternalLinkUrl', () => { it('should return the mapped url when externalLinkUrlOverrides has the url mapping', () => { const url = 'https://foo.example.com'; const mappedUrl = 'https://mapped.example.com'; - setConfig({ externalLinkUrlOverrides: { [url]: mappedUrl } }); + setSiteConfig({ externalLinkUrlOverrides: { [url]: mappedUrl } }); expect(getExternalLinkUrl(url)).toBe(mappedUrl); }); it('should handle empty externalLinkUrlOverrides object', () => { - setConfig({ externalLinkUrlOverrides: {} }); + setSiteConfig({ externalLinkUrlOverrides: {} }); const url = 'https://foo.example.com'; expect(getExternalLinkUrl(url)).toBe(url); }); it('should guard against empty string argument', () => { const fallbackResult = '#'; - setConfig({ externalLinkUrlOverrides: { foo: 'bar' } }); + setSiteConfig({ externalLinkUrlOverrides: { foo: 'bar' } }); expect(getExternalLinkUrl(undefined)).toBe(fallbackResult); }); it('should guard against non-string argument', () => { const fallbackResult = '#'; - setConfig({ externalLinkUrlOverrides: { foo: 'bar' } }); + setSiteConfig({ externalLinkUrlOverrides: { foo: 'bar' } }); expect(getExternalLinkUrl(null)).toBe(fallbackResult); expect(getExternalLinkUrl(42)).toBe(fallbackResult); }); it('should not throw if externalLinkUrlOverrides is not an object', () => { - setConfig({ externalLinkUrlOverrides: null }); + setSiteConfig({ externalLinkUrlOverrides: null }); const url = 'https://foo.example.com'; expect(getExternalLinkUrl(url)).toBe(url); - setConfig({ externalLinkUrlOverrides: 42 }); + setSiteConfig({ externalLinkUrlOverrides: 42 }); expect(getExternalLinkUrl(url)).toBe(url); }); it('should work with multiple mappings', () => { - setConfig({ + setSiteConfig({ externalLinkUrlOverrides: { 'https://a.example.com': 'https://mapped-a.example.com', 'https://b.example.com': 'https://mapped-b.example.com', diff --git a/runtime/config/index.ts b/runtime/config/index.ts index 6f83e70f..42d73bea 100644 --- a/runtime/config/index.ts +++ b/runtime/config/index.ts @@ -32,16 +32,16 @@ * * Exporting a config object: * ``` - * const config = { + * const siteConfig = { * lmsBaseUrl: 'http://localhost:18000' * }; * - * export default config; + * export default siteConfig; * ``` * * Exporting a function that returns an object: * ``` - * function getConfig() { + * function getSiteConfig() { * return { * lmsBaseUrl: 'http://localhost:18000' * }; @@ -50,7 +50,7 @@ * * Exporting a function that returns a promise that resolves to an object: * ``` - * function getAsyncConfig() { + * function getAsyncSiteConfig() { * return new Promise((resolve, reject) => { * resolve({ * lmsBaseUrl: 'http://localhost:18000' @@ -58,7 +58,7 @@ * }); * } * - * export default getAsyncConfig; + * export default getAsyncSiteConfig; * ``` * * ##### Initialization Config Handler @@ -71,7 +71,7 @@ * initialize({ * handlers: { * config: () => { - * mergeConfig({ + * mergeSiteConfig({ * CUSTOM_VARIABLE: 'custom value', * lmsBaseUrl: 'http://localhost:18001' // You can override variables, but this is uncommon. * }, 'App config override handler'); @@ -104,44 +104,34 @@ import merge from 'lodash.merge'; import { AppConfig, EnvironmentTypes, - OptionalSiteConfig, - RequiredSiteConfig, SiteConfig } from '../../types'; import { ACTIVE_ROLES_CHANGED, CONFIG_CHANGED } from '../constants'; import { publish } from '../subscriptions'; -let config: SiteConfig = { +let siteConfig: SiteConfig = { + // Required + siteId: '', + baseUrl: '', + siteName: '', + loginUrl: '', + logoutUrl: '', + lmsBaseUrl: '', + + // Optional + environment: EnvironmentTypes.PRODUCTION, + apps: [], + externalRoutes: [], + externalLinkUrlOverrides: [], accessTokenCookieName: 'edx-jwt-cookie-header-payload', csrfTokenApiPath: '/csrf/api/v1/token', - environment: EnvironmentTypes.PRODUCTION, ignoredErrorRegex: null, languagePreferenceCookieName: 'openedx-language-preference', publicPath: '/', refreshAccessTokenApiPath: '/login_refresh', userInfoCookieName: 'edx-user-info', mfeConfigApiUrl: null, - segmentKey: null, - - apps: [], - externalRoutes: [], - externalLinkUrlOverrides: [], - - appId: '', - baseUrl: '', - siteName: '', - - // Frontends - loginUrl: '', - logoutUrl: '', - - // Backends - lmsBaseUrl: '', - - custom: { - appId: '', - }, }; /** @@ -151,17 +141,17 @@ let config: SiteConfig = { * Example: * * ``` - * import { getConfig } from '@openedx/frontend-base'; + * import { getSiteConfig } from '@openedx/frontend-base'; * * const { * lmsBaseUrl, - * } = getConfig(); + * } = getSiteConfig(); * ``` * * @returns {SiteConfig} */ -export function getConfig() { - return config; +export function getSiteConfig() { + return siteConfig; } /** @@ -170,26 +160,26 @@ export function getConfig() { * Example: * * ``` - * import { setConfig } from '@openedx/frontend-base'; + * import { setSiteConfig } from '@openedx/frontend-base'; * - * setConfig({ + * setSiteConfig({ * lmsBaseUrl, // This is overriding the ENTIRE document - this is not merged in! * }); * ``` * * @param newConfig A replacement SiteConfig which will completely override the current SiteConfig. */ -export function setConfig(newConfig: SiteConfig) { - config = newConfig; +export function setSiteConfig(newSiteConfig: SiteConfig) { + siteConfig = newSiteConfig; publish(CONFIG_CHANGED); } /** - * Merges additional configuration values into the site config returned by `getConfig`. Will + * Merges additional configuration values into the site config returned by `getSiteConfig`. Will * override any values that exist with the same keys. * * ``` - * mergeConfig({ + * mergeSiteConfig({ * NEW_KEY: 'new value', * OTHER_NEW_KEY: 'other new value', * }); @@ -198,10 +188,10 @@ export function setConfig(newConfig: SiteConfig) { * which means they will be merged recursively. See https://lodash.com/docs/latest#merge for * documentation on the exact behavior. * - * @param {Object} newConfig + * @param {Object} newSiteConfig */ -export function mergeConfig(newConfig: Partial & RequiredSiteConfig>) { - config = merge(config, newConfig); +export function mergeSiteConfig(newSiteConfig: Partial) { + siteConfig = merge(siteConfig, newSiteConfig); publish(CONFIG_CHANGED); } @@ -209,16 +199,20 @@ const appConfigs: Record = {}; /** * addAppConfigs finds any AppConfig objects in the apps in SiteConfig and makes their config - * available to be used by Apps via the getAppConfig() function. This is used at initialization - * time to process any AppConfigs bundled with the site. + * available to be used by Apps via getAppConfig(appId) or useAppConfig() functions. This is + * used at initialization time to process any AppConfigs bundled with the site. */ export function addAppConfigs() { - const { apps } = getConfig(); + const { apps } = getSiteConfig(); + if (!apps) return; + for (const app of apps) { - if (app.config !== undefined) { - patchAppConfig(app.config); + const { appId, config } = app; + if (config !== undefined) { + appConfigs[appId] = config; } } + publish(CONFIG_CHANGED); } @@ -226,10 +220,6 @@ export function getAppConfig(id: string) { return appConfigs[id]; } -export function patchAppConfig(appConfig: AppConfig) { - appConfigs[appConfig.appId] = appConfig; -} - let activeRouteRoles: string[] = []; export function setActiveRouteRoles(roles: string[]) { @@ -296,6 +286,6 @@ export function getExternalLinkUrl(url: string): string { return '#'; } - const overriddenLinkUrls = getConfig().externalLinkUrlOverrides ?? {}; + const overriddenLinkUrls = getSiteConfig().externalLinkUrlOverrides ?? {}; return overriddenLinkUrls[url] ?? url; } diff --git a/runtime/constants.ts b/runtime/constants.ts index 04173a50..f3661b74 100644 --- a/runtime/constants.ts +++ b/runtime/constants.ts @@ -1,7 +1,7 @@ /** @constant */ -export const APP_TOPIC = 'APP'; +export const SITE_TOPIC = 'APP'; -export const APP_PUBSUB_INITIALIZED = `${APP_TOPIC}.PUBSUB_INITIALIZED`; +export const SITE_PUBSUB_INITIALIZED = `${SITE_TOPIC}.PUBSUB_INITIALIZED`; /** * Event published when the application initialization sequence has finished loading any dynamic @@ -9,7 +9,7 @@ export const APP_PUBSUB_INITIALIZED = `${APP_TOPIC}.PUBSUB_INITIALIZED`; * * @event */ -export const APP_CONFIG_INITIALIZED = `${APP_TOPIC}.CONFIG_INITIALIZED`; +export const SITE_CONFIG_INITIALIZED = `${SITE_TOPIC}.CONFIG_INITIALIZED`; /** * Event published when the application initialization sequence has finished determining the user's @@ -17,7 +17,7 @@ export const APP_CONFIG_INITIALIZED = `${APP_TOPIC}.CONFIG_INITIALIZED`; * * @event */ -export const APP_AUTH_INITIALIZED = `${APP_TOPIC}.AUTH_INITIALIZED`; +export const SITE_AUTH_INITIALIZED = `${SITE_TOPIC}.AUTH_INITIALIZED`; /** * Event published when the application initialization sequence has finished initializing @@ -25,7 +25,7 @@ export const APP_AUTH_INITIALIZED = `${APP_TOPIC}.AUTH_INITIALIZED`; * * @event */ -export const APP_I18N_INITIALIZED = `${APP_TOPIC}.I18N_INITIALIZED`; +export const SITE_I18N_INITIALIZED = `${SITE_TOPIC}.I18N_INITIALIZED`; /** * Event published when the application initialization sequence has finished initializing the @@ -33,7 +33,7 @@ export const APP_I18N_INITIALIZED = `${APP_TOPIC}.I18N_INITIALIZED`; * * @event */ -export const APP_LOGGING_INITIALIZED = `${APP_TOPIC}.LOGGING_INITIALIZED`; +export const SITE_LOGGING_INITIALIZED = `${SITE_TOPIC}.LOGGING_INITIALIZED`; /** * Event published when the application initialization sequence has finished initializing the @@ -41,7 +41,7 @@ export const APP_LOGGING_INITIALIZED = `${APP_TOPIC}.LOGGING_INITIALIZED`; * * @event */ -export const APP_ANALYTICS_INITIALIZED = `${APP_TOPIC}.ANALYTICS_INITIALIZED`; +export const SITE_ANALYTICS_INITIALIZED = `${SITE_TOPIC}.ANALYTICS_INITIALIZED`; /** * Event published when the application initialization sequence has finished. Applications should @@ -49,7 +49,7 @@ export const APP_ANALYTICS_INITIALIZED = `${APP_TOPIC}.ANALYTICS_INITIALIZED`; * * @event */ -export const APP_READY = `${APP_TOPIC}.READY`; +export const SITE_READY = `${SITE_TOPIC}.READY`; /** * Event published when the application initialization sequence has aborted. This is frequently @@ -58,15 +58,11 @@ export const APP_READY = `${APP_TOPIC}.READY`; * @see {@link module:React~ErrorPage} * @event */ -export const APP_INIT_ERROR = `${APP_TOPIC}.INIT_ERROR`; +export const SITE_INIT_ERROR = `${SITE_TOPIC}.INIT_ERROR`; /** @constant */ export const CONFIG_TOPIC = 'CONFIG'; export const CONFIG_CHANGED = `${CONFIG_TOPIC}.CHANGED`; -export const APPS_TOPIC = 'APPS'; - -export const APPS_CHANGED = `${APPS_TOPIC}.CHANGED`; - export const ACTIVE_ROLES_CHANGED = 'ACTIVE_ROLES_CHANGED'; diff --git a/runtime/i18n/injectIntlWithShim.jsx b/runtime/i18n/injectIntlWithShim.jsx index 4f5239c9..b4717119 100644 --- a/runtime/i18n/injectIntlWithShim.jsx +++ b/runtime/i18n/injectIntlWithShim.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { injectIntl } from 'react-intl'; import { EnvironmentTypes } from '../../types'; -import { getConfig } from '../config'; +import { getSiteConfig } from '../config'; import { getLoggingService } from '../logging'; import { intlShape } from './lib'; @@ -20,7 +20,7 @@ const injectIntlWithShim = (WrappedComponent) => { value: (definition, ...args) => { if (definition === undefined || definition.id === undefined) { const error = new Error('i18n error: An undefined message was supplied to intl.formatMessage.'); - if (getConfig().environment === EnvironmentTypes.DEVELOPMENT) { + if (getSiteConfig().environment === EnvironmentTypes.DEVELOPMENT) { console.error(error); // eslint-disable-line no-console return '!!! Missing message supplied to intl.formatMessage !!!'; } diff --git a/runtime/i18n/lib.ts b/runtime/i18n/lib.ts index 29d180f1..9ac6eff2 100644 --- a/runtime/i18n/lib.ts +++ b/runtime/i18n/lib.ts @@ -4,7 +4,7 @@ import { MessageFormatElement } from 'react-intl'; import Cookies from 'universal-cookie'; import { LocalizedMessages } from '../../types'; -import { getConfig } from '../config'; +import { getSiteConfig } from '../config'; import { publish } from '../subscriptions'; const cookies = new Cookies(); @@ -143,10 +143,14 @@ export function getLocale(locale?: string) { } // 2. User setting in cookie - const cookieLangPref = cookies.get(getConfig().languagePreferenceCookieName); - if (cookieLangPref) { - return findSupportedLocale(cookieLangPref.toLowerCase()); + const { languagePreferenceCookieName } = getSiteConfig(); + if (languagePreferenceCookieName) { + const languagePreference = cookies.get(languagePreferenceCookieName); + if (languagePreference) { + return findSupportedLocale(languagePreference.toLowerCase()); + } } + // 3. Browser language (default) // Note that some browers prefer upper case for the region part of the locale, while others don't. // Thus the toLowerCase, for consistency. @@ -239,10 +243,12 @@ export function mergeMessages(newMessages = {}) { * @memberof module:Internationalization */ export function addAppMessages() { - const { apps } = getConfig(); - apps.forEach((app) => { - mergeMessages(app.messages); - }); + const { apps } = getSiteConfig(); + if (apps) { + apps.forEach((app) => { + mergeMessages(app.messages); + }); + } } interface ConfigureI18nOptions { diff --git a/runtime/index.ts b/runtime/index.ts index 51beb759..b8cadebd 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -32,12 +32,11 @@ export { } from './auth'; export { - getConfig, - setConfig, - mergeConfig, + getSiteConfig, + setSiteConfig, + mergeSiteConfig, addAppConfigs, getAppConfig, - patchAppConfig, setActiveRouteRoles, getActiveRouteRoles, addActiveWidgetRole, @@ -97,15 +96,18 @@ export { export { AppContext, AppProvider, + SiteContext, + SiteProvider, AuthenticatedPageRoute, Divider, ErrorBoundary, ErrorPage, LoginRedirect, PageWrap, - useAppEvent, + useSiteEvent, useAuthenticatedUser, - useConfig + useSiteConfig, + useAppConfig } from './react'; export { diff --git a/runtime/initialize.async.function.config.test.js b/runtime/initialize.async.function.config.test.js index e8bb479d..2f25c3fb 100644 --- a/runtime/initialize.async.function.config.test.js +++ b/runtime/initialize.async.function.config.test.js @@ -3,7 +3,7 @@ import { fetchAuthenticatedUser, hydrateAuthenticatedUser, } from './auth'; -import { getConfig } from './config'; +import { getSiteConfig } from './config'; import { initialize } from './initialize'; import { logError, @@ -26,7 +26,7 @@ let config = null; describe('initialize with async function js file config', () => { beforeEach(() => { jest.resetModules(); - config = getConfig(); + config = getSiteConfig(); fetchAuthenticatedUser.mockReset(); ensureAuthenticatedUser.mockReset(); hydrateAuthenticatedUser.mockReset(); diff --git a/runtime/initialize.const.config.test.js b/runtime/initialize.const.config.test.js index ec0d7fbe..dd6d8cfb 100644 --- a/runtime/initialize.const.config.test.js +++ b/runtime/initialize.const.config.test.js @@ -3,7 +3,7 @@ import { fetchAuthenticatedUser, hydrateAuthenticatedUser, } from './auth'; -import { getConfig } from './config'; +import { getSiteConfig } from './config'; import { initialize } from './initialize'; import { logError, @@ -24,7 +24,7 @@ let config = null; describe('initialize with constant js file config', () => { beforeEach(() => { jest.resetModules(); - config = getConfig(); + config = getSiteConfig(); fetchAuthenticatedUser.mockReset(); ensureAuthenticatedUser.mockReset(); hydrateAuthenticatedUser.mockReset(); diff --git a/runtime/initialize.function.config.test.js b/runtime/initialize.function.config.test.js index d1a65587..1e1a96f9 100644 --- a/runtime/initialize.function.config.test.js +++ b/runtime/initialize.function.config.test.js @@ -3,7 +3,7 @@ import { fetchAuthenticatedUser, hydrateAuthenticatedUser, } from './auth'; -import { getConfig } from './config'; +import { getSiteConfig } from './config'; import { initialize } from './initialize'; import { logError, @@ -24,7 +24,7 @@ let config = null; describe('initialize with function js file config', () => { beforeEach(() => { jest.resetModules(); - config = getConfig(); + config = getSiteConfig(); fetchAuthenticatedUser.mockReset(); ensureAuthenticatedUser.mockReset(); hydrateAuthenticatedUser.mockReset(); diff --git a/runtime/initialize.js b/runtime/initialize.js index a7ef0ae1..6f644df6 100644 --- a/runtime/initialize.js +++ b/runtime/initialize.js @@ -7,10 +7,10 @@ * ``` * import { * initialize, - * APP_INIT_ERROR, - * APP_READY, + * SITE_INIT_ERROR, + * SITE_READY, * subscribe, - * AppProvider, + * SiteProvider, * ErrorPage, * PageWrap * } from '@openedx/frontend-base'; @@ -18,9 +18,9 @@ * import ReactDOM from 'react-dom'; * import { Routes, Route } from 'react-router-dom'; * - * subscribe(APP_READY, () => { + * subscribe(SITE_READY, () => { * ReactDOM.render( - * + * *
*
* @@ -28,12 +28,12 @@ * *
*