├── .env.development
├── .env.test
├── .eslintignore
├── .eslintrc.js
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── add-depr-ticket-to-depr-board.yml
│ ├── add-remove-label-on-comment.yml
│ ├── ci.yml
│ ├── commitlint.yml
│ ├── lockfileversion-check.yml
│ ├── manual-publish.yml
│ ├── npm-deprecate.yml
│ ├── release.yml
│ ├── self-assign-issue.yml
│ └── update-browserslist-db.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .releaserc
├── LICENSE
├── Makefile
├── README.md
├── __mocks__
└── universal-cookie.js
├── babel.config.js
├── catalog-info.yaml
├── codecov.yml
├── docs
├── addTagsPlugin.js
├── auth-API.md
├── decisions
│ ├── 0001-record-architecture-decisions.rst
│ ├── 0002-frontend-base-design-goals.rst
│ ├── 0003-consolidation-into-frontend-platform.rst
│ ├── 0004-axios-caching-implementation.rst
│ ├── 0005-token-null-after-successful-refresh.rst
│ ├── 0006-middleware-support-for-http-clients.rst
│ └── 0007-javascript-file-configuration.rst
├── how_tos
│ ├── assets
│ │ └── paragon-theme-loader.png
│ ├── automatic-case-conversion.rst
│ ├── caching.rst
│ ├── i18n.rst
│ └── theming.md
├── removeExport.js
└── template
│ └── edx
│ ├── README.md
│ ├── publish.js
│ ├── static
│ ├── fonts
│ │ ├── OpenSans-Bold-webfont.eot
│ │ ├── OpenSans-Bold-webfont.svg
│ │ ├── OpenSans-Bold-webfont.woff
│ │ ├── OpenSans-BoldItalic-webfont.eot
│ │ ├── OpenSans-BoldItalic-webfont.svg
│ │ ├── OpenSans-BoldItalic-webfont.woff
│ │ ├── OpenSans-Italic-webfont.eot
│ │ ├── OpenSans-Italic-webfont.svg
│ │ ├── OpenSans-Italic-webfont.woff
│ │ ├── OpenSans-Light-webfont.eot
│ │ ├── OpenSans-Light-webfont.svg
│ │ ├── OpenSans-Light-webfont.woff
│ │ ├── OpenSans-LightItalic-webfont.eot
│ │ ├── OpenSans-LightItalic-webfont.svg
│ │ ├── OpenSans-LightItalic-webfont.woff
│ │ ├── OpenSans-Regular-webfont.eot
│ │ ├── OpenSans-Regular-webfont.svg
│ │ └── OpenSans-Regular-webfont.woff
│ ├── scripts
│ │ ├── linenumber.js
│ │ └── prettify
│ │ │ ├── Apache-License-2.0.txt
│ │ │ ├── lang-css.js
│ │ │ └── prettify.js
│ └── styles
│ │ ├── jsdoc-default.css
│ │ ├── prettify-jsdoc.css
│ │ └── prettify-tomorrow.css
│ └── tmpl
│ ├── augments.tmpl
│ ├── container.tmpl
│ ├── details.tmpl
│ ├── example.tmpl
│ ├── examples.tmpl
│ ├── exceptions.tmpl
│ ├── layout.tmpl
│ ├── mainpage.tmpl
│ ├── members.tmpl
│ ├── method.tmpl
│ ├── modifies.tmpl
│ ├── params.tmpl
│ ├── properties.tmpl
│ ├── returns.tmpl
│ ├── source.tmpl
│ ├── tutorial.tmpl
│ └── type.tmpl
├── env.config.js
├── example
├── AuthenticatedPage.jsx
├── ExamplePage.jsx
├── index.jsx
├── index.scss
├── messages.js
└── src
│ └── i18n
│ ├── README.md
│ └── messages
│ ├── frontend-app-sample
│ ├── ar.json
│ ├── eo.json
│ └── es_419.json
│ ├── frontend-component-emptylangs
│ └── ar.json
│ ├── frontend-component-nolangs
│ └── .gitignore
│ └── frontend-component-singlelang
│ └── ar.json
├── jest.config.js
├── jsdoc.json
├── openedx.yaml
├── package-lock.json
├── package.json
├── public
└── index.html
├── renovate.json
├── service-interface.png
├── src
├── analytics
│ ├── MockAnalyticsService.js
│ ├── SegmentAnalyticsService.js
│ ├── index.js
│ ├── interface.js
│ └── interface.test.js
├── auth
│ ├── AxiosCsrfTokenService.js
│ ├── AxiosJwtAuthService.js
│ ├── AxiosJwtAuthService.test.jsx
│ ├── AxiosJwtTokenService.js
│ ├── LocalForageCache.js
│ ├── MockAuthService.js
│ ├── index.js
│ ├── interceptors
│ │ ├── createCsrfTokenProviderInterceptor.js
│ │ ├── createJwtTokenProviderInterceptor.js
│ │ ├── createProcessAxiosRequestErrorInterceptor.js
│ │ ├── createRetryInterceptor.js
│ │ └── createRetryInterceptor.test.js
│ ├── interface.js
│ └── utils.js
├── config.js
├── constants.js
├── i18n
│ ├── countries.js
│ ├── index.js
│ ├── injectIntlWithShim.jsx
│ ├── languages.js
│ ├── lib.js
│ ├── lib.test.js
│ └── scripts
│ │ ├── README.md
│ │ ├── intl-imports.js
│ │ ├── intl-imports.test.js
│ │ └── transifex-utils.js
├── index.js
├── initialize.async.function.config.test.js
├── initialize.const.config.test.js
├── initialize.function.config.test.js
├── initialize.js
├── initialize.test.js
├── logging
│ ├── MockLoggingService.js
│ ├── NewRelicLoggingService.js
│ ├── NewRelicLoggingService.test.js
│ ├── index.js
│ └── interface.js
├── pubSub.js
├── react
│ ├── AppContext.jsx
│ ├── AppProvider.jsx
│ ├── AppProvider.test.jsx
│ ├── AuthenticatedPageRoute.jsx
│ ├── AuthenticatedPageRoute.test.jsx
│ ├── ErrorBoundary.jsx
│ ├── ErrorBoundary.test.jsx
│ ├── ErrorPage.jsx
│ ├── LoginRedirect.jsx
│ ├── OptionalReduxProvider.jsx
│ ├── PageWrap.jsx
│ ├── constants.js
│ ├── hooks
│ │ ├── index.js
│ │ ├── paragon
│ │ │ ├── index.js
│ │ │ ├── useParagonTheme.js
│ │ │ ├── useParagonTheme.test.js
│ │ │ ├── useParagonThemeCore.js
│ │ │ ├── useParagonThemeCore.test.js
│ │ │ ├── useParagonThemeUrls.js
│ │ │ ├── useParagonThemeUrls.test.js
│ │ │ ├── useParagonThemeVariants.js
│ │ │ ├── useParagonThemeVariants.test.js
│ │ │ ├── useTrackColorSchemeChoice.js
│ │ │ ├── useTrackColorSchemeChoice.test.js
│ │ │ ├── utils.js
│ │ │ └── utils.test.js
│ │ └── useAppEvent.js
│ ├── index.js
│ └── reducers.js
├── scripts
│ ├── GoogleAnalyticsLoader.js
│ ├── GoogleAnalyticsLoader.test.js
│ └── index.js
├── setupTest.js
├── testing
│ ├── index.js
│ ├── initializeMockApp.js
│ ├── initializeMockApp.test.js
│ └── mockMessages.js
├── utils.js
└── utils.test.js
└── webpack.dev.config.js
/.env.development:
--------------------------------------------------------------------------------
1 | EXAMPLE_VAR=Example Value
2 | ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
3 | ACCOUNT_PROFILE_URL=http://localhost:1995
4 | ACCOUNT_SETTINGS_URL=http://localhost:1997
5 | BASE_URL=http://localhost:8080
6 | CREDENTIALS_BASE_URL=http://localhost:18150
7 | CSRF_TOKEN_API_PATH=/csrf/api/v1/token
8 | DISCOVERY_API_BASE_URL=http://localhost:18381
9 | PUBLISHER_BASE_URL=http://localhost:18400
10 | ECOMMERCE_BASE_URL=http://localhost:18130
11 | LANGUAGE_PREFERENCE_COOKIE_NAME=openedx-language-preference
12 | LEARNING_BASE_URL=http://localhost:2000
13 | LMS_BASE_URL=http://localhost:18000
14 | LOGIN_URL=http://localhost:18000/login
15 | LOGOUT_URL=http://localhost:18000/logout
16 | STUDIO_BASE_URL=http://localhost:18010
17 | MARKETING_SITE_BASE_URL=http://localhost:18000
18 | ORDER_HISTORY_URL=http://localhost:1996/orders
19 | REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh
20 | SEGMENT_KEY=''
21 | SITE_NAME=localhost
22 | USER_INFO_COOKIE_NAME=edx-user-info
23 | LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
24 | LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
25 | LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
26 | FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
27 | IGNORED_ERROR_REGEX=
28 | MFE_CONFIG_API_URL=
29 | APP_ID=
30 | SUPPORT_URL=https://support.edx.org
31 | PARAGON_THEME_URLS={}
32 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | EXAMPLE_VAR=Example Value
2 | ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
3 | ACCOUNT_PROFILE_URL=http://localhost:1995
4 | ACCOUNT_SETTINGS_URL=http://localhost:1997
5 | BASE_URL=http://localhost:8080
6 | CREDENTIALS_BASE_URL=http://localhost:18150
7 | CSRF_TOKEN_API_PATH=/csrf/api/v1/token
8 | DISCOVERY_API_BASE_URL=http://localhost:18381
9 | PUBLISHER_BASE_URL=http://localhost:18400
10 | ECOMMERCE_BASE_URL=http://localhost:18130
11 | LANGUAGE_PREFERENCE_COOKIE_NAME=openedx-language-preference
12 | LEARNING_BASE_URL=http://localhost:2000
13 | LMS_BASE_URL=http://localhost:18000
14 | LOGIN_URL=http://localhost:18000/login
15 | LOGOUT_URL=http://localhost:18000/logout
16 | STUDIO_BASE_URL=http://localhost:18010
17 | MARKETING_SITE_BASE_URL=http://localhost:18000
18 | ORDER_HISTORY_URL=http://localhost:1996/orders
19 | REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh
20 | SEGMENT_KEY=''
21 | SITE_NAME=localhost
22 | USER_INFO_COOKIE_NAME=edx-user-info
23 | LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
24 | LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
25 | LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
26 | FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
27 | IGNORED_ERROR_REGEX=
28 | MFE_CONFIG_API_URL=
29 | APP_ID=
30 | SUPPORT_URL=https://support.edx.org
31 | PARAGON_THEME_URLS={}
32 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .idea
2 | coverage
3 | dist
4 | docs
5 | node_modules
6 | src/analytics/segment.js
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | const { getBaseConfig } = require('@openedx/frontend-build');
3 |
4 | const config = getBaseConfig('eslint');
5 |
6 | config.rules = {
7 | 'import/no-extraneous-dependencies': ['error', {
8 | devDependencies: [
9 | '**/*.config.js',
10 | '**/*.test.jsx',
11 | '**/*.test.js',
12 | 'example/*',
13 | ],
14 | }],
15 | 'import/extensions': ['error', {
16 | ignore: ['@edx/frontend-platform*'],
17 | }],
18 | 'import/no-unresolved': ['error', {
19 | ignore: ['@edx/frontend-platform*'],
20 | }],
21 | 'jsx-a11y/anchor-is-valid': ['error', {
22 | components: ['Link'],
23 | specialLink: ['to'],
24 | aspects: ['noHref', 'invalidHref', 'preferButton'],
25 | }],
26 | };
27 |
28 | module.exports = config;
29 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Description:**
2 |
3 | Describe what this pull request changes, and why. Include implications for people using this change.
4 |
5 | **Merge checklist:**
6 |
7 | - [ ] Consider running your code modifications in the included example app within `frontend-platform`. This can be done by running `npm start` and opening http://localhost:8080.
8 | - [ ] Consider testing your code modifications in another local micro-frontend using local aliases configured via [the `module.config.js` file in `frontend-build`](https://github.com/openedx/frontend-build#local-module-configuration-for-webpack).
9 | - [ ] Verify your commit title/body conforms to the conventional commits format (e.g., `fix`, `feat`) and is appropriate for your code change. Consider whether your code is a breaking change, and modify your commit accordingly.
10 |
11 | **Post merge:**
12 |
13 | - [ ] After the build finishes for the merged commit, verify the new release has been pushed to [NPM](https://www.npmjs.com/package/@edx/frontend-platform).
14 |
--------------------------------------------------------------------------------
/.github/workflows/add-depr-ticket-to-depr-board.yml:
--------------------------------------------------------------------------------
1 | # Run the workflow that adds new tickets that are either:
2 | # - labelled "DEPR"
3 | # - title starts with "[DEPR]"
4 | # - body starts with "Proposal Date" (this is the first template field)
5 | # to the org-wide DEPR project board
6 |
7 | name: Add newly created DEPR issues to the DEPR project board
8 |
9 | on:
10 | issues:
11 | types: [opened]
12 |
13 | jobs:
14 | routeissue:
15 | uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
16 | secrets:
17 | GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
18 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.github/workflows/add-remove-label-on-comment.yml:
--------------------------------------------------------------------------------
1 | # This workflow runs when a comment is made on the ticket
2 | # If the comment starts with "label: " it tries to apply
3 | # the label indicated in rest of comment.
4 | # If the comment starts with "remove label: ", it tries
5 | # to remove the indicated label.
6 | # Note: Labels are allowed to have spaces and this script does
7 | # not parse spaces (as often a space is legitimate), so the command
8 | # "label: really long lots of words label" will apply the
9 | # label "really long lots of words label"
10 |
11 | name: Allows for the adding and removing of labels via comment
12 |
13 | on:
14 | issue_comment:
15 | types: [created]
16 |
17 | jobs:
18 | add_remove_labels:
19 | uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: node_js CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - "**"
10 |
11 | jobs:
12 | tests:
13 | runs-on: ubuntu-22.04
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup Nodejs
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version-file: '.nvmrc'
22 |
23 | - name: Install dependencies
24 | run: npm ci
25 |
26 | - name: Lint
27 | run: npm run lint
28 |
29 | - name: Test
30 | run: npm run test
31 |
32 | - name: Build
33 | run: npm run build
34 |
35 | - name: Docs
36 | run: npm run docs
37 |
38 | - name: Run Coverage
39 | uses: codecov/codecov-action@v4
40 | with:
41 | fail_ci_if_error: true
42 | token: ${{ secrets.CODECOV_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.github/workflows/commitlint.yml:
--------------------------------------------------------------------------------
1 | # Run commitlint on the commit messages in a pull request.
2 |
3 | name: Lint Commit Messages
4 |
5 | on:
6 | - pull_request
7 |
8 | jobs:
9 | commitlint:
10 | uses: openedx/.github/.github/workflows/commitlint.yml@master
11 |
--------------------------------------------------------------------------------
/.github/workflows/lockfileversion-check.yml:
--------------------------------------------------------------------------------
1 | #check package-lock file version
2 |
3 | name: lockfileVersion check
4 |
5 | on:
6 | push:
7 | branches:
8 | - master
9 | pull_request:
10 |
11 | jobs:
12 | version-check:
13 | uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
14 |
--------------------------------------------------------------------------------
/.github/workflows/manual-publish.yml:
--------------------------------------------------------------------------------
1 | name: Manual Publish
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | tag:
6 | required: true
7 | description: NPM distribution tag to use for the backported release (npm publish --tag )
8 | jobs:
9 | release:
10 | name: Manual Publish
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 | - name: Setup Nodejs Env
18 | run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
19 | - name: Setup Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: ${{ env.NODE_VER }}
23 | - name: Install dependencies
24 | run: npm ci
25 | - name: Lint
26 | run: npm run lint
27 | - name: Test
28 | run: npm run test
29 | - name: Coverage
30 | uses: codecov/codecov-action@v4
31 | with:
32 | fail_ci_if_error: false
33 | token: ${{ secrets.CODECOV_TOKEN }}
34 | - name: Build
35 | run: npm run build
36 | # NPM expects to be authenticated for publishing. This step will fail CI if NPM is not authenticated
37 | - name: Check NPM authentication
38 | run: |
39 | echo "//registry.npmjs.org/:_authToken=${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}" >> .npmrc
40 | npm whoami
41 | - name: Release
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
44 | NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
45 | # `npm publish` relies on version specified in package.json file
46 | run: npm publish ./dist --tag ${{github.event.inputs.tag}} # e.g., old-version
47 |
--------------------------------------------------------------------------------
/.github/workflows/npm-deprecate.yml:
--------------------------------------------------------------------------------
1 | name: NPM Deprecate
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version:
6 | description: 'Version to deprecate'
7 | required: true
8 | message:
9 | description: 'Reason for deprecation'
10 | required: true
11 |
12 | jobs:
13 | deprecate:
14 | name: Deprecate
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Check NPM authentication
18 | run: |
19 | echo "//registry.npmjs.org/:_authToken=${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}" >> .npmrc
20 | npm whoami
21 | - name: NPM deprecate
22 | run: npm deprecate @edx/frontend-platform@"${{ github.event.inputs.version }}" "${{ github.event.inputs.message }}"
23 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - alpha
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-22.04
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Setup Nodejs Env
19 | run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
20 |
21 | - name: Setup Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: ${{ env.NODE_VER }}
25 |
26 | - name: Install dependencies
27 | run: npm ci
28 |
29 | - name: Create Build
30 | run: npm run build
31 |
32 | - name: Release to npm/Github
33 | run: npx semantic-release@22
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
36 | NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
37 |
38 | - name: Docs
39 | run: npm run docs
40 |
41 | - name: Deploy to GitHub Pages
42 | uses: JamesIves/github-pages-deploy-action@v4.7.3
43 | with:
44 | branch: gh-pages # The branch the action should deploy to.
45 | folder: docs/api/@edx/frontend-platform/1.0.0-semantically-released # The folder the action should deploy.
46 | clean: true # Automatically remove deleted files from the deploy branch
47 |
--------------------------------------------------------------------------------
/.github/workflows/self-assign-issue.yml:
--------------------------------------------------------------------------------
1 | # This workflow runs when a comment is made on the ticket
2 | # If the comment starts with "assign me" it assigns the author to the
3 | # ticket (case insensitive)
4 |
5 | name: Assign comment author to ticket if they say "assign me"
6 | on:
7 | issue_comment:
8 | types: [created]
9 |
10 | jobs:
11 | self_assign_by_comment:
12 | uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
13 |
--------------------------------------------------------------------------------
/.github/workflows/update-browserslist-db.yml:
--------------------------------------------------------------------------------
1 | name: Update Browserslist DB
2 | on:
3 | schedule:
4 | - cron: '0 0 * * 1'
5 | workflow_dispatch:
6 |
7 | jobs:
8 | update-browserslist:
9 | uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
10 |
11 | secrets:
12 | requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .vscode
4 | coverage
5 | dist
6 | src/i18n/transifex_input.json
7 | node_modules
8 | /docs/api
9 | .env.private
10 | /temp/
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | __mocks__
2 | babel.config.js
3 | codecov.yml
4 | commitlint.config.js
5 | coverage
6 | example
7 | jest.config.js
8 | Makefile
9 | public
10 | renovate.json
11 | src/**/*.test.js
12 | src/**/*.test.jsx
13 | src/**/tests
14 | src/**/setupTest.js
15 | webpack.dev.config.js
16 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "master",
4 | {name: "alpha", prerelease: true}
5 | ],
6 | "tagFormat": "v${version}",
7 | "verifyConditions": [
8 | {
9 | "path": "@semantic-release/npm",
10 | "pkgRoot": "dist"
11 | },
12 | {
13 | "path": "@semantic-release/github"
14 | }
15 | ],
16 | "analyzeCommits": "@semantic-release/commit-analyzer",
17 | "generateNotes": "@semantic-release/release-notes-generator",
18 | "prepare": [
19 | {
20 | "path": "@semantic-release/npm",
21 | "pkgRoot": "dist"
22 | }
23 | ],
24 | "publish": [
25 | {
26 | "path": "@semantic-release/npm",
27 | "pkgRoot": "dist"
28 | },
29 | {
30 | "path": "@semantic-release/github"
31 | }
32 | ],
33 | "success": [],
34 | "fail": []
35 | }
36 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Documentation CLI usage: https://github.com/documentationjs/documentation/blob/master/docs/USAGE.md
2 |
3 | i18n = ./src/i18n
4 | transifex_input = $(i18n)/transifex_input.json
5 | transifex_utils = $(i18n)/scripts/transifex-utils.js
6 |
7 | # This directory must match .babelrc .
8 | transifex_temp = ./temp/babel-plugin-formatjs
9 |
10 | doc_command = ./node_modules/.bin/documentation build src -g -c ./docs/documentation.config.yml -f md -o ./docs/_API-body.md --sort-order alpha
11 | cat_docs_command = cat ./docs/_API-header.md ./docs/_API-body.md > ./docs/API.md
12 |
13 | build:
14 | rm -rf ./dist
15 | ./node_modules/.bin/fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
16 | @# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
17 | @find dist -name '*.test.js*' -delete
18 | rm ./dist/setupTest.js
19 | cp ./package.json ./dist/package.json
20 | cp ./LICENSE ./dist/LICENSE
21 | cp ./README.md ./dist/README.md
22 |
23 | docs-build:
24 | ${doc_command}
25 | ${cat_docs_command}
26 | rm ./docs/_API-body.md
27 |
28 | docs-watch:
29 | @echo "NOTE: Please load _API-body.md to see watch results."
30 | ${doc_command} -w
31 |
32 | docs-lint:
33 | ./node_modules/.bin/documentation lint src
34 |
35 |
36 | .PHONY: requirements
37 | requirements: ## install ci requirements
38 | npm ci
39 |
40 | i18n.extract:
41 | # Pulling display strings from .jsx files into .json files...
42 | rm -rf $(transifex_temp)
43 | npm run-script i18n_extract
44 |
45 | i18n.concat:
46 | # Gathering JSON messages into one file...
47 | $(transifex_utils) $(transifex_temp) $(transifex_input)
48 |
49 | extract_translations: | requirements i18n.extract i18n.concat
50 |
--------------------------------------------------------------------------------
/__mocks__/universal-cookie.js:
--------------------------------------------------------------------------------
1 | const mockCookiesImplementation = {
2 | get: jest.fn(),
3 | remove: jest.fn(),
4 | };
5 |
6 | module.exports = () => mockCookiesImplementation;
7 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const { createConfig } = require('@openedx/frontend-build');
2 |
3 | module.exports = createConfig('babel-preserve-modules');
4 |
--------------------------------------------------------------------------------
/catalog-info.yaml:
--------------------------------------------------------------------------------
1 | # This file records information about this repo. Its use is described in OEP-55:
2 | # https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
3 |
4 | apiVersion: backstage.io/v1alpha1
5 | kind: Component
6 | metadata:
7 | name: 'frontend-platform'
8 | description: "A modest application framework for Open edX micro-frontend applications and their supporting libraries"
9 | links:
10 | - url: "https://github.com/openedx/frontend-platform"
11 | title: "Frontend Platform"
12 | icon: "Code"
13 | - url: "https://openedx.github.io/frontend-platform/"
14 | title: "Documentation"
15 | icon: "Article"
16 | annotations:
17 | openedx.org/arch-interest-groups: ""
18 | spec:
19 | owner: "group:committers-frontend"
20 | type: 'library'
21 | lifecycle: 'production'
22 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: auto
6 | threshold: 0%
7 | patch:
8 | default:
9 | target: auto
10 | threshold: 0%
11 |
--------------------------------------------------------------------------------
/docs/addTagsPlugin.js:
--------------------------------------------------------------------------------
1 | exports.defineTags = function (dictionary) {
2 | dictionary.defineTag("service", {
3 | mustHaveValue: true,
4 | canHaveType: false,
5 | canHaveName: true,
6 | onTagged: function (doclet, tag) {
7 | doclet.service = tag.value;
8 | }
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/docs/auth-API.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## LoginRedirect : ReactComponent
4 | **Kind**: global class
5 |
6 |
7 | ## redirectToLogin(redirectUrl)
8 | Redirect the user to login
9 |
10 | **Kind**: global function
11 |
12 | | Param | Type | Description |
13 | | --- | --- | --- |
14 | | redirectUrl | string | the url to redirect to after login |
15 |
16 |
17 |
18 | ## redirectToLogout(redirectUrl)
19 | Redirect the user to logout
20 |
21 | **Kind**: global function
22 |
23 | | Param | Type | Description |
24 | | --- | --- | --- |
25 | | redirectUrl | string | the url to redirect to after logout |
26 |
27 |
28 |
29 | ## getAuthenticatedApiClient(config) ⇒ [HttpClient](#HttpClient)
30 | Gets the apiClient singleton which is an axios instance.
31 |
32 | **Kind**: global function
33 | **Returns**: [HttpClient](#HttpClient) - Singleton. A configured axios http client
34 |
35 | | Param | Type | Description |
36 | | --- | --- | --- |
37 | | config | object | |
38 | | [config.appBaseUrl] | string | |
39 | | [config.authBaseUrl] | string | |
40 | | [config.loginUrl] | string | |
41 | | [config.logoutUrl] | string | |
42 | | [config.loggingService] | object | requires logError and logInfo methods |
43 | | [config.refreshAccessTokenEndpoint] | string | |
44 | | [config.accessTokenCookieName] | string | |
45 | | [config.csrfTokenApiPath] | string | |
46 |
47 |
48 |
49 | ## getAuthenticatedUser() ⇒ [Promise.<UserData>](#UserData) \| Promise.<null>
50 | Gets the authenticated user's access token. Resolves to null if the user is unauthenticated.
51 |
52 | **Kind**: global function
53 | **Returns**: [Promise.<UserData>](#UserData) \| Promise.<null> - Resolves to the user's access token if they are logged in.
54 |
55 |
56 | ## ensureAuthenticatedUser(route) ⇒ [Promise.<UserData>](#UserData)
57 | Ensures a user is authenticated. It will redirect to login when not authenticated.
58 |
59 | **Kind**: global function
60 |
61 | | Param | Type | Description |
62 | | --- | --- | --- |
63 | | route | string | to return user after login when not authenticated. |
64 |
65 |
66 |
67 | ## PrivateRoute() : ReactComponent
68 | **Kind**: global function
69 |
70 |
71 | ## HttpClient
72 | A configured axios client. See axios docs for more
73 | info https://github.com/axios/axios. All the functions
74 | below accept isPublic and isCsrfExempt in the request
75 | config options. Setting these to true will prevent this
76 | client from attempting to refresh the jwt access token
77 | or a csrf token respectively.
78 |
79 | ```
80 | // A public endpoint (no jwt token refresh)
81 | apiClient.get('/path/to/endpoint', { isPublic: true });
82 | ```
83 |
84 | ```
85 | // A csrf exempt endpoint
86 | apiClient.post('/path/to/endpoint', { data }, { isCsrfExempt: true });
87 | ```
88 |
89 | **Kind**: global typedef
90 | **Properties**
91 |
92 | | Name | Type | Description |
93 | | --- | --- | --- |
94 | | get | function | |
95 | | head | function | |
96 | | options | function | |
97 | | delete | function | (csrf protected) |
98 | | post | function | (csrf protected) |
99 | | put | function | (csrf protected) |
100 | | patch | function | (csrf protected) |
101 |
102 |
103 |
104 | ## UserData
105 | **Kind**: global typedef
106 | **Properties**
107 |
108 | | Name | Type |
109 | | --- | --- |
110 | | userId | string |
111 | | username | string |
112 | | roles | array |
113 | | administrator | bool |
114 |
115 |
--------------------------------------------------------------------------------
/docs/decisions/0001-record-architecture-decisions.rst:
--------------------------------------------------------------------------------
1 | 1. Record Architecture Decisions
2 | --------------------------------
3 |
4 | Status
5 | ------
6 |
7 | Accepted
8 |
9 | Context
10 | -------
11 |
12 | We would like to keep a historical record on the architectural
13 | decisions we make with this app as it evolves over time.
14 |
15 | Decision
16 | --------
17 |
18 | We will use Architecture Decision Records, as described by
19 | Michael Nygard in `Documenting Architecture Decisions`_
20 |
21 | .. _Documenting Architecture Decisions: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
22 |
23 | Consequences
24 | ------------
25 |
26 | See Michael Nygard's article, linked above.
27 |
28 | References
29 | ----------
30 |
31 | * https://resources.sei.cmu.edu/asset_files/Presentation/2017_017_001_497746.pdf
32 | * https://github.com/npryce/adr-tools/tree/master/doc/adr
33 |
--------------------------------------------------------------------------------
/docs/decisions/0003-consolidation-into-frontend-platform.rst:
--------------------------------------------------------------------------------
1 | Consolidation of libraries into frontend-platform
2 | =================================================
3 |
4 | Status
5 | ------
6 |
7 | Accepted
8 |
9 | Context
10 | -------
11 |
12 | The frontend-platform repository replaces five earlier repositories:
13 |
14 | - edx/frontend-analytics
15 | - edx/frontend-auth
16 | - edx/frontend-base
17 | - edx/frontend-i18n
18 | - edx/frontend-logging
19 |
20 | We found that development across these five libraries was growing very difficult in a number of ways.
21 |
22 | Dependency graph complexity
23 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 |
25 | The overhead involved in maintaining individual packages and semantic version numbers was very difficult to reason about and maintain. Updates to low-level packages tended to cause cascading updates through the dependency tree. If a breaking change or incompatibility present in the tree forces dependent packages to absorb unrelated work, it can be very difficult to realize it's happening, decreasing developer velocity and destabilizing our applications.
26 |
27 | As a rough illustration of this complexity:
28 |
29 | - 12 Micro-frontends depend on 5 libraries. (60 edges max)
30 | - 12 Micro-frontends depend on 5 reusable organisms (headers, footers, Paragon) (60 edges max)
31 | - 5 Reusable organisms depend on the 5 libraries. (25 edges max)
32 | - The 5 libraries sometimes depend on each other. (8 edges total)
33 |
34 | This graph has 153 possible edges, each of which represents a version number with its own set of compatibilities. By combining the libraries, we drop the number of edges to 77. Further combining some of our reusable organisms could get this number as low as 51.
35 |
36 | For a single micro-frontend, these numbers go from 43 to 11, then 9.
37 |
38 | Difficult cross-cutting feature development
39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 |
41 | Often we've found that features and bug fixes require us to work in several of the above repositories simultaneously. Getting npm to link packages from peer directories is tedious and error prone, making this kind of work difficult to get right.
42 |
43 | Interdependent services
44 | ~~~~~~~~~~~~~~~~~~~~~~~
45 |
46 | Going along with the difficulty of cross-cutting concerns, we realized at some point that many of packages were simply rather inter-connected. Meaningful features and behaviors span packages, especially with respect to frontend-base's usage of the other libraries. This means that validating correct behavior requires a fully functioning system; developing the libraries in isolation prevented us from doing substantive integration testing.
47 |
48 | Decision
49 | --------
50 |
51 | By collapsing these libraries into a single repository published together under a single version number, we drastically reduce the layers of complexity involved in publishing new library versions.
52 | Note that this is a tactical technology choice - architecturally and strategically, the libraries are still independent and contain strong boundaries isolating them from each other.
53 |
54 | Note that we've preserved the platform's ability to utilize different service implementations, meaning that using the platform does not preclude applications from providing customized/extended functionality. This means that we haven't really lost much flexibility by the consolidation.
55 |
56 | Alternatives
57 | ------------
58 |
59 | We strongly considered making a Lerna monorepo with each of the original five packages published with its own version number. There were two problems with this approach - first, it wouldn't address the dependency graph complexity, which was one of the most difficult-to-reason aspects of the original repositories. Secondary to that, our usage of babel and building to a dist directory erases some of the developer experience benefits of using Lerna. One of Lerna's strengths is that it automatically rewires your package.json dependencies to point at other packages in the repository, rather than pulling them down from the package registry. Since we have a build step, we'd have to manually build each package any time we wanted to use it, which isn't all that different from what we had to do with the five separate repos. Lerna just didn't really seem worth it.
60 |
61 | Adoption
62 | --------
63 |
64 | As of this writing, frontend-platform is in use in four of our existing micro-frontends. It's our intention to help teams migrate the others (approximately 8) to use the platform as well, and to archive the five legacy implementations mentioned above.
65 |
66 | The platform's API surface is very similar to the original five libraries, simply with a different import package. If necessary, we expect that consumers of the old libraries will be able to do gradual, partial upgrades by cutting over individual libraries until they reach full adoption.
67 |
68 | Consequences
69 | ------------
70 |
71 | If we collapse these libraries into a single versioned platform, consumers of the platform (applications) may still have to absorb undesired breaking changes at times, but we've greatly reduced the complexity of doing so. Updating an application or dependent library involves consulting a single changelog for breaking changes, rather than five and trying to reason which are compatible with each other.
72 |
--------------------------------------------------------------------------------
/docs/decisions/0004-axios-caching-implementation.rst:
--------------------------------------------------------------------------------
1 | Axios Caching Layer Implementation
2 | =================================================
3 |
4 | Status
5 | ------
6 |
7 | Accepted
8 |
9 | Context
10 | -------
11 |
12 | It was noticed that on a lot of pages the same API calls are repeatedly being made as the user navigates around the website. The data being returned from these requests doesn't change between page loads leading to the same endpoints reloading the same data repeatedly. We decided that we wanted to avoid repeatedly loading unchanged data and reuse the data that was in the response from the initial load.
13 |
14 | We think it's beneficial to cut as much overhead as possible so each page becomes interactive more quickly.
15 |
16 | We decided to add a frontend caching layer to the AxiosJwtAuthService by leveraging an already existing npm package: `axios-cache-adapter `_
17 |
18 | Having a frontend caching layer would also allow for a stale version of a page to be displayed for a short amount of time in the case of a backend outage.
19 |
20 | When Frontend Caching Should Be Used
21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 |
23 | The frontend cache should be used to eliminate the reloading of unchanged data as a user navigates between different pages in the application. It shouldn't be used to reduce the number of times multiple components on the same page try to make the same network call. Reducing the number of times the same network call is made on a single page should be done by moving the call higher up in the hierarchy so the call only gets made once and all lower level components are able to access the response data from it.
24 |
25 | Requests that are good candidates to use frontend caching are ones that:
26 | * The data being returned changes very infrequently, and independently from the actions of the user on the page
27 |
28 | - For example changes made by a background job or an admin
29 |
30 | * The data being returned isn't specific to the authenticated user
31 |
32 | * The data returned is specific to the authenticated user but front end does not make the request when there isn't an authenticated user
33 |
34 | It should not be used as the only caching strategy that gets used. It is preferable to use a serverside caching strategy in conjunction with frontend caching.
35 |
36 | How To Use Frontend Caching
37 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
38 |
39 | The frontend caching layer can be used by passing {useCache: true} into the function that gets the httpClient.
40 |
41 | e.g.
42 | service.getAuthenticatedHttpClient({ useCache: true });
43 |
44 | By default the response for a GET request will be stored in IndexedDB for 5 minutes and that cached value will get invalidated on any POST, PUT, PATCH, or DELETE request made to the same url. The caching layer also works with the standard cache-control headers: max-age, no-cache, and no-store
45 |
46 | Each request can also override the default cache configurations. `The How To document about caching `_ has more detailed examples about how this caching layer can be used.
47 |
48 |
49 | Implementation Details
50 | ~~~~~~~~~~~~~~~~~~~~~~
51 |
52 | For both versions of the current AxiosJwtAuthService HTTP clients, the authenticated client and the base unauthenticated clients, there are now two additional versions that use the cache (i.e. CachedAuthenticated and CachedUnauthenticated). Bringing the total number of httpClient instances up to four.
53 | In order to keep the interface more simple the cached clients don’t have their own getter functions exposing this detail, in order to use the cached http client micro frontends pass in a param for using the cache with the current getter functions.
54 |
55 | In the case of the cached clients failing to get created the uncached clients will be returned and used instead.
56 |
57 | The async hydrateAuthenticatedUser function will continue only ever using the uncached authenticated HTTP client so there won't be any concerns about the cache changing expected behavior of authentication.
58 |
59 | Only the AxiosJwtAuthService currently has cached http clients.
60 |
61 | Decisions
62 | ---------
63 |
64 | By using the existing axios-cache-adapter we were able to avoid needing to implement our own caching strategy and forgo having micro frontends need to implement their own front end caching strategies.
65 |
66 | The default max-age for cached data was decided to be 5 minutes for now. 5 minutes was deemed to be a reasonable amount of time that was neither too long or too short. The assumption is made that users won’t be switching to different accounts on their browser frequently enough to have a wide impact on their experience.
67 | If they experience any impact at all then they will only see cached data for a maximum of 5 minutes. Micro frontends are also able to override the default cache time to suit their own needs.
68 | The default max-age can also be changed at a later time if the current default of 5 minutes is deemed to be inadequate.
69 |
70 | "Integration tests were added to ensure that the cache adapter does not break the current behavior of the existing interceptors. These tests make real HTTP requests to https://httpbin.org/ as suggested by the tests in the axios-cache-adapter source code."
71 |
72 | Alternatives
73 | ------------
74 |
75 | We discussed the pros and cons of having a frontend-platform cache implementation for micro front ends to use versus micro front ends implementing their own front end caching strategies utilizing Local/Session storage, ETags, and endpoints utilizing Cache-Control headers.
76 | Having a frontend-platform caching solution be widely available to micro frontends was decided to be a good thing and micro frontends are still able to use other caching strategies in conjunction with the caching in frontend-platform.
77 |
78 | Adoption
79 | --------
80 |
81 | As of this writing, only a few HTTP requests within `frontend-app-learner-portal-enterprise `_ have adopted using the cached clients. Usage of the cache clients is on an opt-in basis where micro frontends determine whether or not using the front end cache clients suits their needs.
82 |
83 |
84 | Consequences
85 | ------------
86 |
87 | Axios is version locked to version 0.18.1 so per request overrides work until this issue gets resolved: https://github.com/RasCarlito/axios-cache-adapter/issues/99.
88 | At of this writing it seems that a fix for the issue might possibly be in version 0.20
89 |
--------------------------------------------------------------------------------
/docs/decisions/0005-token-null-after-successful-refresh.rst:
--------------------------------------------------------------------------------
1 | Access Token Null After Successful Refresh
2 | ==========================================
3 |
4 | Status
5 | ------
6 |
7 | Accepted
8 |
9 | Context
10 | -------
11 |
12 | There are cases where our frontend authentication code requests an updated access token (JWT cookies), gets a successful response, but the cookies never appear. This results in the JavaScript error: "[frontend-auth] Access token is still null after successful refresh."
13 |
14 | Decision
15 | --------
16 |
17 | Since we have been unable to determine root cause, the de facto decision is to allow this issue to continue to exist.
18 |
19 | This doc is less about capturing the decision, then to capture the background in-case anyone wishes to understand what has been attempted so far.
20 |
21 | Failed Hypotheses
22 | -----------------
23 |
24 | Several failed hypotheses have been tested to determine why this is happening:
25 |
26 | * Tested if caused by a race condition between getting the response and having the cookies set.
27 |
28 | * See https://github.com/edx-unsupported/frontend-auth/pull/38/files
29 |
30 | * Tested if caused by cookies being disabled, by creating/reading a separate cookie.
31 |
32 | * Failure noted in https://openedx.atlassian.net/browse/ARCH-948?focusedCommentId=401201
33 |
34 | * Tested if the cookies library has a bug by attempting vanilla javascript.
35 |
36 | * Failure noted in https://openedx.atlassian.net/browse/ARCH-948?focusedCommentId=401201
37 |
38 | * Tested if caused by a difference between the browser time and server time, causing the cookie to expire on arrival.
39 |
40 | * Only a small subset of the errors (~0.8%) had a time difference of >5 minutes.
41 |
42 | * See https://github.com/openedx/frontend-platform/pull/207
43 |
44 | Additional Data
45 | ---------------
46 |
47 | Here is a query used in New Relic to find these errors in the Learning MFE::
48 |
49 | SELECT count(*) From JavaScriptError
50 | WHERE errorMessage = '[frontend-auth] Access token is still null after successful refresh.' AND
51 | appName = 'prod-frontend-app-learning'
52 | SINCE 4 days ago FACET userAgentName
53 |
54 | Almost all of these errors are seen in Safari (2.9 k), with the rest (166) found in Chrome and Microsoft Edge.
55 |
56 | Here is an adjusted query searching for errors with a difference of >5 minutes between the server and browser times::
57 |
58 | SELECT count(*) From JavaScriptError
59 | WHERE errorMessage = '[frontend-auth] Access token is still null after successful refresh.' AND
60 | browserDriftSeconds > 60*5
61 | SINCE 4 days ago FACET userAgentName
62 |
63 | There were only 24 of these found in Safari, or ~0.8% of all of these errors found in Safari.
64 |
65 | Lastly, at the time this data was captured (2021-08-17), this error type made up ~12% of all errors in Safari, and ~3.3% of all errors across all browsers in the Learning MFE.
66 |
67 | * It is unclear if these errors can be duplicated with special privacy settings in Safari (or other browsers).
68 |
69 | * We have not learned about this issue from Customer Support, but only from monitoring errors. Since we are not receiving the JWT cookies, we do not know the affected users.
70 |
--------------------------------------------------------------------------------
/docs/decisions/0006-middleware-support-for-http-clients.rst:
--------------------------------------------------------------------------------
1 | Middleware Support for HTTP clients
2 | ===================================
3 |
4 | Status
5 | ------
6 |
7 | Accepted
8 |
9 | Context
10 | -------
11 |
12 | We currently expose HTTP clients(axios instances) via ``getAuthenticatedHttpClient`` and ``getHttpClient`` used to make API requests
13 | in our MFEs. There are instances where it would be helpful if consumers could apply middleware to these clients.
14 | For example the `axios-case-converter `_ package provides
15 | a middleware that handles snake-cased <-> camelCase conversions via axios interceptors. This middleware would allow our MFEs to
16 | avoid having to do this conversion manually.
17 |
18 | Decision
19 | --------
20 |
21 | The ``initialize`` function provided in the initialize module initializes the ``AxiosJwtAuthService`` provided by ``@edx/frontend-platform``.
22 | We will add an optional param ``authMiddleware``, an array of middleware functions that will be applied to all http clients in
23 | the ``AxiosJwtAuthService``.
24 |
25 | Consumers will install the middleware they want to use and provide it to ``initialize``::
26 |
27 | initialize({
28 | messages: [appMessages],
29 | requireAuthenticatedUser: true,
30 | hydrateAuthenticatedUser: true,
31 | authMiddleware: [axiosCaseConverter, (client) => axiosRetry(client, { retries: 3 })],
32 | });
33 |
34 | If a consumer chooses not to use ``initialize`` and instead the ``configure`` function, the middleware can be passed in the options param::
35 |
36 | configure({
37 | loggingService: getLoggingService(),
38 | config: getConfig(),
39 | options: {
40 | middleware: [axiosCaseConverter, (client) => axiosRetry(client, { retries: 3 })]
41 | }
42 | });
43 |
44 | We decided to let consumers install their own middleware packages, removing the need to install the dependency as part of ``@edx/frontend-platform``.
45 |
--------------------------------------------------------------------------------
/docs/decisions/0007-javascript-file-configuration.rst:
--------------------------------------------------------------------------------
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 `_ 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 `_. 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
144 |
--------------------------------------------------------------------------------
/docs/how_tos/assets/paragon-theme-loader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/how_tos/assets/paragon-theme-loader.png
--------------------------------------------------------------------------------
/docs/how_tos/automatic-case-conversion.rst:
--------------------------------------------------------------------------------
1 | #####################################################################
2 | How to: Convert SnakeCase to CamelCase automatically for API Requests
3 | #####################################################################
4 |
5 | Introduction
6 | ************
7 |
8 | When using the HTTP client from ``@edx/frontend-platform``, you are making an API request to an
9 | Open edX service which requires you to handle snake-cased <-> camelCase conversions manually. The manual conversion quickly gets
10 | tedious, and is error prone if you forget to do it.
11 |
12 | Here is how you can configure the HTTP client to automatically convert snake_case <-> camelCase for you.
13 |
14 | How do I use configure automatic case conversion?
15 | *************************************************
16 |
17 | You want to install `axios-case-converter `_, and add it
18 | as a middleware when calling ``initialize`` in the consumer::
19 |
20 | import axiosCaseConverter from 'axios-case-converter';
21 |
22 | initialize({
23 | messages: [],
24 | requireAuthenticatedUser: true,
25 | hydrateAuthenticatedUser: true,
26 | authMiddleware: [axiosCaseConverter],
27 | });
28 |
29 | Or, if you choose to use ``configure`` instead::
30 |
31 | import axiosCaseConverter from 'axios-case-converter';
32 |
33 | configure({
34 | loggingService: getLoggingService(),
35 | config: getConfig(),
36 | options: {
37 | middleware: [axiosCaseConverter, (client) => axiosRetry(client, { retries: 3 })]
38 | }
39 | });
40 |
41 | By default the middleware will convert camelCase -> snake_case for payloads, and snake_case -> camelCase for responses.
42 | If you want to customize middleware behavior, i.e. only have responses transformed, you can configure it like this::
43 | initialize({
44 | messages: [],
45 | requireAuthenticatedUser: true,
46 | hydrateAuthenticatedUser: true,
47 | authMiddleware: [(client) => axiosCaseConverter(client, {
48 | // options for the middleware
49 | ignoreHeaders: true, // don't convert headers
50 | caseMiddleware: {
51 | requestInterceptor: (config) => {
52 | return config;
53 | }
54 | }
55 | })],
56 | });
57 |
58 | See `axios-case-converter `_ for more details on configurations supported by the package.
59 |
--------------------------------------------------------------------------------
/docs/how_tos/caching.rst:
--------------------------------------------------------------------------------
1 | ############################
2 | How to: Caching API Requests
3 | ############################
4 |
5 | .. contents:: Table of Contents
6 |
7 | Introduction
8 | ************
9 |
10 | Often, a web application needs to make the same repeated API calls, where the data returned is mostly static (i.e., does not change often). In such situations, caching is a viable solution to prevent unnecessary and potentially expensive operations, resulting in increased performance and a better user experience.
11 |
12 | When considering caching options, there's a few approaches to consider:
13 |
14 | Server caching
15 | ==============
16 |
17 | Server caching helps to limit the cost of an API request on the server and its dependent systems. When a client makes a request to the API, the server will check for a local copy of the requested resource. If the local resource exists, the server will respond with it; otherwise, the request is processed normally (e.g., performing database lookups). With this approach, the cost savings to the server may be significant, utilizing less resources.
18 |
19 | Client caching
20 | ==============
21 |
22 | Client caching helps to limit the cost of an API request incurred by the user. Rather than relying on a API implementing server caching, commonly referenced data may be cached locally within the client (i.e., browser). Similar to server caching, on the first request of an API, the request will occur as usual but the response data will be stored in the client.
23 |
24 | However, on subsequent requests (within a given timeframe), the client will check if a copy of the requested resource exists. If it does, it will read from the client cache rather than making a network request to the API. This has the benefit of freeing up resources on the server as it no longer needs to handle repeat queries within a given timeframe.
25 |
26 | Typically, client caching stores data for a given time (e.g., 5 minutes) since it was last requested. This allows the client cache to be dynamic in the sense that data will be eventually consistent and unneeded local data can be cleared once it is no longer relevant.
27 |
28 | Hybrid caching
29 | ==============
30 |
31 | Both server and client caching can be combined to provide best of both worlds. Regardless of how an API request is made, utilizing a hybrid approach will ensure data is read from a local cache first, whether that be on the server or the client.
32 |
33 | How do I use client caching with ``@edx/frontend-platform?``
34 | ************************************************************
35 |
36 | The ``@edx/frontend-platform`` package supports an Axios HTTP client that uses client caching under the hood through the use of `axios-cache-interceptor `_.
37 |
38 | Currently, `localForage `_ is configured to be the underlying storage; ``localForage`` is a package that provides a similar API to browser's localStorage, except is asynchronous (non-blocking). ``localForage`` is configured to prefer using IndexedDB, falling back to localStorage if browser support for IndexedDB is lacking. If all else fails, an in-memory store is used instead. IndexedDB is more performant and may contain a broader range (and volume) of data in comparison to localStorage (see `browser support `_ for IndexedDB).
39 |
40 | When importing an HTTP client from ``@edx/frontend-platform``, you may specify options for how you wish to configure the HTTP client::
41 |
42 | import { getAuthenticatedHttpClient } from '@edx/frontend-platform';
43 |
44 | // ``cachedHttpClient`` is configured to use client caching under the hood.
45 | const cachedHttpClient = getAuthenticatedHttpClient({ useCache: true });
46 |
47 | The examples below demonstrate how to configure commonly used caching behavior on a per-request basis. For more use-cases, you may refer to the `axios-cache-interceptor documentation `_.
48 |
49 | Overriding the request id
50 | =========================
51 |
52 | Every request passed through the axios-cache-interceptor interceptor has an id. Each request id is responsible for binding a request to its cache
53 | for referencing (or invalidating) it later.
54 |
55 | const { id: requestId} = cachedHttpClient.get('/courses/', {
56 | id: 'custom-id'
57 | });
58 |
59 | Modify expiry behavior for a single request
60 | ===========================================
61 |
62 | By default, API requests using the cached HTTP client will be cached for 5 minutes. However, cache options may be configured on a per-request basis::
63 |
64 | cachedHttpClient.get('/courses/', {
65 | cache: {
66 | ttl: 15 * 60 * 1000, // 15 minutes instead of the default 5.
67 | },
68 | });
69 |
70 | Invalidate cache for a single request
71 | =====================================
72 |
73 | In certain situations, it may be necessary to invalidate cached data to force a true network request to the server::
74 |
75 | cachedHttpClient.get('/courses/', {
76 | cache: {
77 | override: true, // This will replace the cache when the response arrives.
78 | },
79 | });
80 |
81 | To remove the cache of a request::
82 |
83 | await axios.storage.remove('list-posts');
84 |
85 |
86 | Check if response is served from network or from cache
87 | ======================================================
88 |
89 | If there is a need to know whether a response was served from the network (i.e., server) or from the local client cache, you may refer to the ``response.request`` object::
90 |
91 | cachedHttpClient.get('/courses/').then((response) => {
92 | console.log(response.cached); // will be true if served from the client cache, false otherwise
93 | });
94 |
--------------------------------------------------------------------------------
/docs/removeExport.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JSDoc plugin.
3 | *
4 | * Modifies the source code to remove the "export" keyword before JS doc sees it. This removes
5 | * "exports." prefixes from documented members.
6 | *
7 | * @module plugins/exportKiller
8 | */
9 | exports.handlers = {
10 | beforeParse(e) {
11 | e.source = e.source.replace(/(\nexport function)/g, $ => {
12 | return `\nfunction`;
13 | });
14 | e.source = e.source.replace(/(\nexport const)/g, $ => {
15 | return `\nconst`;
16 | });
17 | e.source = e.source.replace(/(\nexport default)/g, $ => {
18 | return `\n`;
19 | });
20 | e.source = e.source.replace(/(\nexport async function)/g, $ => {
21 | return `\nasync function`;
22 | });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/docs/template/edx/README.md:
--------------------------------------------------------------------------------
1 | The default template for JSDoc 4 uses: the [Underscore Template library](http://underscorejs.org/).
2 |
3 |
4 | ## Generating Typeface Fonts
5 |
6 | The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows:
7 |
8 | 1. Open the [OpenSans page at Font Squirrel]().
9 | 2. Click on the 'Webfont Kit' tab.
10 | 3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'.
11 | 4. Click the 'DOWNLOAD @FONT-FACE KIT' button.
12 | 5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory.
13 |
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Bold-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Bold-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Bold-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Italic-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Italic-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Italic-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Italic-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Light-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Light-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Light-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Regular-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Regular-webfont.eot
--------------------------------------------------------------------------------
/docs/template/edx/static/fonts/OpenSans-Regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openedx/frontend-platform/79d211a3ede966a6889329b932b1961b6cdb4ac6/docs/template/edx/static/fonts/OpenSans-Regular-webfont.woff
--------------------------------------------------------------------------------
/docs/template/edx/static/scripts/linenumber.js:
--------------------------------------------------------------------------------
1 | /*global document */
2 | (() => {
3 | const source = document.getElementsByClassName('prettyprint source linenums');
4 | let i = 0;
5 | let lineNumber = 0;
6 | let lineId;
7 | let lines;
8 | let totalLines;
9 | let anchorHash;
10 |
11 | if (source && source[0]) {
12 | anchorHash = document.location.hash.substring(1);
13 | lines = source[0].getElementsByTagName('li');
14 | totalLines = lines.length;
15 |
16 | for (; i < totalLines; i++) {
17 | lineNumber++;
18 | lineId = `line${lineNumber}`;
19 | lines[i].id = lineId;
20 | if (lineId === anchorHash) {
21 | lines[i].className += ' selected';
22 | }
23 | }
24 | }
25 | })();
26 |
--------------------------------------------------------------------------------
/docs/template/edx/static/scripts/prettify/lang-css.js:
--------------------------------------------------------------------------------
1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com",
2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
3 |
--------------------------------------------------------------------------------
/docs/template/edx/static/styles/prettify-jsdoc.css:
--------------------------------------------------------------------------------
1 | /* JSDoc prettify.js theme */
2 |
3 | /* plain text */
4 | .pln {
5 | color: #000000;
6 | font-weight: normal;
7 | font-style: normal;
8 | }
9 |
10 | /* string content */
11 | .str {
12 | color: #006400;
13 | font-weight: normal;
14 | font-style: normal;
15 | }
16 |
17 | /* a keyword */
18 | .kwd {
19 | color: #000000;
20 | font-weight: bold;
21 | font-style: normal;
22 | }
23 |
24 | /* a comment */
25 | .com {
26 | font-weight: normal;
27 | font-style: italic;
28 | }
29 |
30 | /* a type name */
31 | .typ {
32 | color: #000000;
33 | font-weight: normal;
34 | font-style: normal;
35 | }
36 |
37 | /* a literal value */
38 | .lit {
39 | color: #006400;
40 | font-weight: normal;
41 | font-style: normal;
42 | }
43 |
44 | /* punctuation */
45 | .pun {
46 | color: #000000;
47 | font-weight: bold;
48 | font-style: normal;
49 | }
50 |
51 | /* lisp open bracket */
52 | .opn {
53 | color: #000000;
54 | font-weight: bold;
55 | font-style: normal;
56 | }
57 |
58 | /* lisp close bracket */
59 | .clo {
60 | color: #000000;
61 | font-weight: bold;
62 | font-style: normal;
63 | }
64 |
65 | /* a markup tag name */
66 | .tag {
67 | color: #006400;
68 | font-weight: normal;
69 | font-style: normal;
70 | }
71 |
72 | /* a markup attribute name */
73 | .atn {
74 | color: #006400;
75 | font-weight: normal;
76 | font-style: normal;
77 | }
78 |
79 | /* a markup attribute value */
80 | .atv {
81 | color: #006400;
82 | font-weight: normal;
83 | font-style: normal;
84 | }
85 |
86 | /* a declaration */
87 | .dec {
88 | color: #000000;
89 | font-weight: bold;
90 | font-style: normal;
91 | }
92 |
93 | /* a variable name */
94 | .var {
95 | color: #000000;
96 | font-weight: normal;
97 | font-style: normal;
98 | }
99 |
100 | /* a function name */
101 | .fun {
102 | color: #000000;
103 | font-weight: bold;
104 | font-style: normal;
105 | }
106 |
107 | /* Specify class=linenums on a pre to get line numbering */
108 | ol.linenums {
109 | margin-top: 0;
110 | margin-bottom: 0;
111 | }
112 |
--------------------------------------------------------------------------------
/docs/template/edx/static/styles/prettify-tomorrow.css:
--------------------------------------------------------------------------------
1 | /* Tomorrow Theme */
2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */
3 | /* Pretty printing styles. Used with prettify.js. */
4 | /* SPAN elements with the classes below are added by prettyprint. */
5 | /* plain text */
6 | .pln {
7 | color: #4d4d4c; }
8 |
9 | @media screen {
10 | /* string content */
11 | .str {
12 | color: #718c00; }
13 |
14 | /* a keyword */
15 | .kwd {
16 | color: #8959a8; }
17 |
18 | /* a comment */
19 | .com {
20 | color: #8e908c; }
21 |
22 | /* a type name */
23 | .typ {
24 | color: #4271ae; }
25 |
26 | /* a literal value */
27 | .lit {
28 | color: #f5871f; }
29 |
30 | /* punctuation */
31 | .pun {
32 | color: #4d4d4c; }
33 |
34 | /* lisp open bracket */
35 | .opn {
36 | color: #4d4d4c; }
37 |
38 | /* lisp close bracket */
39 | .clo {
40 | color: #4d4d4c; }
41 |
42 | /* a markup tag name */
43 | .tag {
44 | color: #c82829; }
45 |
46 | /* a markup attribute name */
47 | .atn {
48 | color: #f5871f; }
49 |
50 | /* a markup attribute value */
51 | .atv {
52 | color: #3e999f; }
53 |
54 | /* a declaration */
55 | .dec {
56 | color: #f5871f; }
57 |
58 | /* a variable name */
59 | .var {
60 | color: #c82829; }
61 |
62 | /* a function name */
63 | .fun {
64 | color: #4271ae; } }
65 | /* Use higher contrast and text-weight for printable form. */
66 | @media print, projection {
67 | .str {
68 | color: #060; }
69 |
70 | .kwd {
71 | color: #006;
72 | font-weight: bold; }
73 |
74 | .com {
75 | color: #600;
76 | font-style: italic; }
77 |
78 | .typ {
79 | color: #404;
80 | font-weight: bold; }
81 |
82 | .lit {
83 | color: #044; }
84 |
85 | .pun, .opn, .clo {
86 | color: #440; }
87 |
88 | .tag {
89 | color: #006;
90 | font-weight: bold; }
91 |
92 | .atn {
93 | color: #404; }
94 |
95 | .atv {
96 | color: #060; } }
97 | /* Style */
98 | /*
99 | pre.prettyprint {
100 | background: white;
101 | font-family: Consolas, Monaco, 'Andale Mono', monospace;
102 | font-size: 12px;
103 | line-height: 1.5;
104 | border: 1px solid #ccc;
105 | padding: 10px; }
106 | */
107 |
108 | /* Specify class=linenums on a pre to get line numbering */
109 | ol.linenums {
110 | margin-top: 0;
111 | margin-bottom: 0; }
112 |
113 | /* IE indents via margin-left */
114 | li.L0,
115 | li.L1,
116 | li.L2,
117 | li.L3,
118 | li.L4,
119 | li.L5,
120 | li.L6,
121 | li.L7,
122 | li.L8,
123 | li.L9 {
124 | /* */ }
125 |
126 | /* Alternate shading for lines */
127 | li.L1,
128 | li.L3,
129 | li.L5,
130 | li.L7,
131 | li.L9 {
132 | /* */ }
133 |
--------------------------------------------------------------------------------
/docs/template/edx/tmpl/augments.tmpl:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |