Compare commits

..

8 Commits

Author SHA1 Message Date
David Joy
b3869b97f7 fix: remove unnecessary environment variable (#292)
* fix: remove unnecessary environment variable

* fix: bumping version of frontend-base for csrf env var change

* fix: get IDE linting to work again
2019-10-10 14:16:25 -04:00
Adam Butterworth
cd339d491d Create 0002-build-time-customization-with-npm-aliases.rst (#291) 2019-10-07 16:05:57 -04:00
David Joy
b66cbb3eb6 fix: frontend-base 4.0.0 and a few others (#288)
* fix: bump version of frontend-base to 4.0.0

* fix: bumping a few other versions

* fix: bump footer version and lock travis npm version

* fix: use dist not src for header/footer styless
2019-10-02 15:54:25 -04:00
Adam Butterworth
cd2fdae725 feat: use openedx footer by default (#282)
* feat: use openedx footer by default

* fix: delete footer directory and update package-lock
2019-09-30 16:53:31 -04:00
Adam Butterworth
51b4d9b42a fix: re-add semantic-release (#275)
* fix: re-add semantic-release

* fix: removing semantic release dep
2019-09-27 10:03:53 -04:00
Adam Butterworth
112857de00 fix: update frontend-build to allow production env to pass through (#278) 2019-09-25 15:26:44 -04:00
David Joy
3317d2b148 fix: bump frontend-base to 2.2.0 to initialize segment (#272)
frontend-base 2.1.x had a bug where segment wasn’t being initialized.  Now it is.

Also allowing frontend-base to use its default logging service, which is NewRelicLoggingService.
2019-09-25 13:40:33 -04:00
Adam Butterworth
be67c13914 feat: delegate build scripts to edx/frontend-build (#257) 2019-09-25 11:54:49 -04:00
26 changed files with 3874 additions and 14956 deletions

32
.env Normal file
View File

@@ -0,0 +1,32 @@
NODE_ENV=null
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
USER_INFO_COOKIE_NAME=null
APPLE_APP_STORE_URL=null
CONTACT_URL=null
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=null
ENTERPRISE_MARKETING_URL=null
ENTERPRISE_MARKETING_UTM_CAMPAIGN=null
ENTERPRISE_MARKETING_UTM_SOURCE=null
FACEBOOK_URL=null
GOOGLE_PLAY_URL=null
LINKED_IN_URL=null
OPEN_SOURCE_URL=null
PRIVACY_POLICY_URL=null
REDDIT_URL=null
SUPPORT_URL=null
TERMS_OF_SERVICE_URL=null
TWITTER_URL=null
YOU_TUBE_URL=null

33
.env.development Normal file
View File

@@ -0,0 +1,33 @@
NODE_ENV='development'
PORT=1995
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
CONTACT_URL='http://localhost:18000/contact'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='my_campaign'
ENTERPRISE_MARKETING_UTM_SOURCE='edX profile'
FACEBOOK_URL='https://www.facebook.com'
GOOGLE_PLAY_URL='https://play.google.com/store'
LINKED_IN_URL='https://www.linkedin.com'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
REDDIT_URL='https://www.reddit.com'
SUPPORT_URL='http://localhost:18000/support'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'

15
.env.test Normal file
View File

@@ -0,0 +1,15 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'

View File

@@ -2,3 +2,4 @@ coverage/*
dist/
node_modules/
__mocks__/
__snapshots__/

View File

@@ -1,34 +0,0 @@
{
"extends": "eslint-config-edx",
"parser": "babel-eslint",
"rules": {
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"webpack/*.js",
"**/*.test.jsx",
"**/*.test.js"
]
}
],
// https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/340#issuecomment-338424908
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "to" ]
}],
"jsx-a11y/label-has-for": [ 2, {
"components": [ "label" ],
"required": {
"some": [ "nesting", "id" ]
},
"allowChildren": false
}]
},
"env": {
"jest": true
},
"globals": {
"newrelic": false
}
}

3
.eslintrc.js Normal file
View File

@@ -0,0 +1,3 @@
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

View File

@@ -1,7 +1,7 @@
language: node_js
node_js: 12
before_install:
- npm install -g npm@latest
- npm install -g npm@6
install:
- npm ci
script:
@@ -13,7 +13,7 @@ script:
- npm run npm-build
- npm run is-es5
after_success:
- npm run semantic-release
- npx semantic-release
- codecov
env:
global:

View File

@@ -51,7 +51,7 @@ validate-no-uncommitted-package-lock-changes:
npm-build:
rm -rf ./npm-dist
./node_modules/.bin/babel src/profile --out-dir npm-dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
./node_modules/.bin/fedx-scripts babel src/profile --out-dir npm-dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
@# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
@find npm-dist -name '*.test.js*' -delete
@rm -rf ./npm-dist/__mocks__

View File

@@ -1,39 +0,0 @@
/* eslint-disable */
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
[
'transform-imports',
{
'@fortawesome/free-brands-svg-icons': {
transform: '@fortawesome/free-brands-svg-icons/${member}',
skipDefaultConversion: true,
},
'@fortawesome/free-regular-svg-icons': {
transform: '@fortawesome/free-regular-svg-icons/${member}',
skipDefaultConversion: true,
},
'@fortawesome/free-solid-svg-icons': {
transform: '@fortawesome/free-solid-svg-icons/${member}',
skipDefaultConversion: true,
},
},
],
],
env: {
i18n: {
plugins: [
[
'react-intl',
{
messagesDir: './temp/babel-plugin-react-intl',
moduleSourceName: '@edx/frontend-i18n',
},
],
],
},
},
};

View File

@@ -0,0 +1,72 @@
2. Build time customization using NPM aliases
---------------------------------------------
Status
------
Accepted
Context
-------
Frontend applications created throughout FY2019 contain hardcoded edX brand specific elements
such as the site's header, footer, logo, visual style, and navigational links. This enabled
teams to move more quickly in efforts to adopt a micro-frontend architecture to enable more
rapid UI innovation in the future. There was no easy path for these new applications to be
incorporated into the Open edX platform where they need to be branded properly.
Decision
--------
In order to make frontend applications brand agnostic, we will split branded elements into
npm packages such as ``frontend-component-header``. Frontend applications will expect to
find components or other exports according to a defined interface. Package interfaces will
be defined in the README for each npm package repository.
.. code-block:: javascript
// exports React component as default and a 'messages' object for i18n
import Header, { messages } from '@edx/frontend-component-header';
To build a frontend application for a specific brand (edX or other Open edX implementation) we
will leverage npm aliases to override these npm packages with branded packages that implement the
same interface before build. For example, ``frontend-component-header`` will be overriden with
``frontend-component-header-edx``. This is done using the
`npm alias syntax introduced in version 6.9.0`_.
We install aliases using the syntax below:
.. code-block:: bash
# npm install <package-name>@<type>:<branded-package>
# npm package
npm install @edx/frontend-component-header@npm:@edx/frontend-component-header-edx@latest
# git repository
npm install @edx/frontend-component-header@git:https://github.com/edx/frontend-component-header-edx.git
# local folder
npm install @edx/frontend-component-header@file:../path/to/local/module/during/build
After installing overrides using npm aliases, we build the project normally:
.. code-block:: bash
npm run build
Using this mechanism branded packages are substituted for the default unbranded packages at build
and deployment time.
.. _npm alias syntax introduced in version 6.9.0: https://github.com/npm/rfcs/blob/latest/implemented/0001-package-aliases.md
Consequences
------------
One drawback of this process is the inability to automatically test substituted packages before
deployment. This is a risk we are willing to accept until it becomes an issue.
edX has built a deployment pipeline that will read configuration for npm packages to override
and alias. This code is open source but heavily catered to edX's specific deployment needs.
The Open edX community will need to collaborate on a simpler, less opinionated build and
deployment process for micro-frontend applications.

View File

@@ -1,99 +0,0 @@
import React from 'react';
import SiteFooter from '@edx/frontend-component-footer';
import { sendTrackEvent } from '@edx/frontend-analytics';
import { App, validateConfig } from '@edx/frontend-base';
import {
faFacebookSquare,
faTwitterSquare,
faYoutubeSquare,
faLinkedin,
faRedditSquare,
} from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import FooterLogo from './edx-footer.png';
const config = {
APPLE_APP_STORE_URL: process.env.APPLE_APP_STORE_URL,
CONTACT_URL: process.env.CONTACT_URL,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: process.env.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
ENTERPRISE_MARKETING_URL: process.env.ENTERPRISE_MARKETING_URL,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: process.env.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
ENTERPRISE_MARKETING_UTM_SOURCE: process.env.ENTERPRISE_MARKETING_UTM_SOURCE,
FACEBOOK_URL: process.env.FACEBOOK_URL,
GOOGLE_PLAY_URL: process.env.GOOGLE_PLAY_URL,
LINKED_IN_URL: process.env.LINKED_IN_URL,
OPEN_SOURCE_URL: process.env.OPEN_SOURCE_URL,
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL,
REDDIT_URL: process.env.REDDIT_URL,
SUPPORT_URL: process.env.SUPPORT_URL,
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL,
TWITTER_URL: process.env.TWITTER_URL,
YOU_TUBE_URL: process.env.YOU_TUBE_URL,
};
App.requireConfig(['SITE_NAME', 'MARKETING_SITE_BASE_URL'], 'ProfileFooter');
validateConfig(config, 'ProfileFooter');
export default function ProfileFooter() {
const socialLinks = [
{
title: 'Facebook',
url: config.FACEBOOK_URL,
icon: <FontAwesomeIcon icon={faFacebookSquare} className="social-icon" size="2x" />,
screenReaderText: 'Like edX on Facebook',
},
{
title: 'Twitter',
url: config.TWITTER_URL,
icon: <FontAwesomeIcon icon={faTwitterSquare} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on Twitter',
},
{
title: 'Youtube',
url: config.YOU_TUBE_URL,
icon: <FontAwesomeIcon icon={faYoutubeSquare} className="social-icon" size="2x" />,
screenReaderText: 'Subscribe to the edX YouTube channel',
},
{
title: 'LinkedIn',
url: config.LINKED_IN_URL,
icon: <FontAwesomeIcon icon={faLinkedin} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on LinkedIn',
},
{
title: 'Reddit',
url: config.REDDIT_URL,
icon: <FontAwesomeIcon icon={faRedditSquare} className="social-icon" size="2x" />,
screenReaderText: 'Subscribe to the edX subreddit',
},
];
const enterpriseMarketingLinkData = {
url: config.ENTERPRISE_MARKETING_URL,
queryParams: {
utm_campaign: config.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
utm_source: config.ENTERPRISE_MARKETING_UTM_SOURCE,
utm_medium: config.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
},
};
return (
<SiteFooter
appleAppStoreUrl={config.APPLE_APP_STORE_URL}
contactUrl={config.CONTACT_URL}
enterpriseMarketingLink={enterpriseMarketingLinkData}
googlePlayUrl={config.GOOGLE_PLAY_URL}
handleAllTrackEvents={sendTrackEvent}
marketingSiteBaseUrl={App.config.MARKETING_SITE_BASE_URL}
openSourceUrl={config.OPEN_SOURCE_URL}
privacyPolicyUrl={config.PRIVACY_POLICY_URL}
siteLogo={FooterLogo}
siteName={App.config.SITE_NAME}
socialLinks={socialLinks}
supportUrl={config.SUPPORT_URL}
termsOfServiceUrl={config.TERMS_OF_SERVICE_URL}
/>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

7
jest.config.js Normal file
View File

@@ -0,0 +1,7 @@
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFiles: [
'<rootDir>/src/setupTest.js',
],
});

17906
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,15 @@
"ie 11"
],
"scripts": {
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=webpack/webpack.prod.config.js",
"build": "fedx-scripts webpack",
"npm-build": "make npm-build",
"lint": "eslint --ext .js --ext .jsx .",
"i18n_extract": "BABEL_ENV=i18n babel src --quiet > /dev/null",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"semantic-release": "semantic-release",
"snapshot": "jest --updateSnapshot",
"start": "NODE_ENV=development BABEL_ENV=development webpack-dev-server --config=webpack/webpack.dev.config.js --progress",
"test": "jest --coverage --passWithNoTests"
"lint": "fedx-scripts eslint",
"precommit": "npm run lint",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"files": [
"/npm-dist"
@@ -43,9 +43,9 @@
"dependencies": {
"@edx/frontend-analytics": "3.0.0",
"@edx/frontend-auth": "7.0.1",
"@edx/frontend-base": "2.1.4",
"@edx/frontend-component-footer": "6.0.2",
"@edx/frontend-component-header": "1.1.2",
"@edx/frontend-base": "4.0.1",
"@edx/frontend-component-footer": "9.0.0",
"@edx/frontend-component-header": "1.1.4",
"@edx/frontend-i18n": "3.0.2",
"@edx/frontend-logging": "3.0.1",
"@edx/paragon": "7.1.3",
@@ -82,82 +82,15 @@
"universal-cookie": "3.1.0"
},
"devDependencies": {
"@babel/cli": "7.6.0",
"@babel/core": "7.6.0",
"@babel/plugin-proposal-class-properties": "7.5.5",
"@babel/plugin-proposal-object-rest-spread": "7.5.5",
"@babel/plugin-syntax-dynamic-import": "7.2.0",
"@babel/preset-env": "7.6.0",
"@babel/preset-react": "7.0.0",
"@svgr/webpack": "4.3.2",
"autoprefixer": "9.6.1",
"axios-mock-adapter": "1.16.0",
"babel-eslint": "10.0.3",
"babel-jest": "24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-react-intl": "4.1.16",
"babel-plugin-transform-imports": "2.0.0",
"clean-webpack-plugin": "0.1.19",
"@edx/frontend-build": "1.1.0",
"codecov": "3.1.0",
"copy-webpack-plugin": "4.6.0",
"css-loader": "3.2.0",
"cssnano": "4.1.10",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"es-check": "5.0.0",
"eslint-config-edx": "4.0.4",
"fetch-mock": "6.5.2",
"file-loader": "1.1.11",
"glob": "7.1.3",
"html-webpack-harddisk-plugin": "0.2.0",
"html-webpack-new-relic-plugin": "1.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "0.14.3",
"identity-obj-proxy": "3.0.0",
"image-webpack-loader": "4.6.0",
"jest": "24.9.0",
"mini-css-extract-plugin": "0.4.5",
"new-relic-source-map-webpack-plugin": "1.2.0",
"node-sass": "4.12.0",
"postcss-loader": "3.0.0",
"postcss-rtl": "1.3.3",
"purgecss-webpack-plugin": "1.5.0",
"react-dev-utils": "9.0.3",
"purgecss-webpack-plugin": "1.6.0",
"react-test-renderer": "16.9.0",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.3",
"resolve-url-loader": "3.1.0",
"sass-loader": "6.0.7",
"semantic-release": "15.13.24",
"source-map-loader": "0.2.4",
"style-loader": "0.20.3",
"url-loader": "1.1.2",
"webpack": "4.29.2",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"webpack-merge": "4.2.1"
},
"jest": {
"rootDir": "./src",
"testURL": "http://localhost/",
"setupFiles": [
"./setupTest.js"
],
"moduleNameMapper": {
"\\.svg": "<rootDir>/__mocks__/svgrMock.js",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "identity-obj-proxy"
},
"collectCoverageFrom": [
"**/*.{js,jsx}"
],
"coveragePathIgnorePatterns": [
"setupTest.js",
"index.jsx"
],
"transformIgnorePatterns": [
"/node_modules/(?!((@edx/paragon)|(@edx/frontend-base))/).*/"
]
"redux-mock-store": "1.5.3"
}
}

View File

@@ -1 +0,0 @@
module.exports = 'test-file-stub';

View File

@@ -1 +0,0 @@
module.exports = { ReactComponent: 'IconMock' };

View File

@@ -1,13 +1,12 @@
import 'babel-polyfill';
import { App, AppProvider, APP_ERROR, APP_READY, ErrorPage } from '@edx/frontend-base';
import { NewRelicLoggingService } from '@edx/frontend-logging';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch } from 'react-router-dom';
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
import Footer from '../footer/Footer';
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
import appMessages from './i18n';
import './index.scss';
@@ -35,4 +34,10 @@ App.subscribe(APP_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
});
App.initialize({ messages: [appMessages, headerMessages], loggingService: NewRelicLoggingService });
App.initialize({
messages: [
appMessages,
headerMessages,
footerMessages,
],
});

View File

@@ -3,5 +3,5 @@
@import './profile/index.scss';
@import "~@edx/frontend-component-header/src/index";
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

View File

@@ -38,7 +38,7 @@ import { profilePageSelector } from './data/selectors';
// i18n
import messages from './ProfilePage.messages';
App.requireConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
App.ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
class ProfilePage extends React.Component {
constructor(props, context) {

View File

@@ -2,7 +2,7 @@ import { App } from '@edx/frontend-base';
import { logApiClientError } from '@edx/frontend-logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
const { LMS_BASE_URL } = App.requireConfig(['LMS_BASE_URL'], 'Profile API service');
App.ensureConfig(['LMS_BASE_URL'], 'Profile API service');
function processAccountData(data) {
return camelCaseObject(data);
@@ -20,7 +20,7 @@ function processAndThrowError(error, errorDataProcessor) {
// GET ACCOUNT
export async function getAccount(username) {
const { data } = await App.apiClient.get(`${LMS_BASE_URL}/api/user/v1/accounts/${username}`);
const { data } = await App.apiClient.get(`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}`);
// Process response data
return processAccountData(data);
@@ -31,7 +31,7 @@ export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
const { data } = await App.apiClient
.patch(`${LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
.patch(`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
headers: {
'Content-Type': 'application/merge-patch+json',
},
@@ -49,7 +49,7 @@ export async function patchProfile(username, params) {
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await App.apiClient.post(
`${LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
formData,
{
headers: {
@@ -73,7 +73,7 @@ export async function postProfilePhoto(username, formData) {
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
const { data } = await App.apiClient.delete(`${LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
const { data } = await App.apiClient.delete(`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
@@ -86,7 +86,7 @@ export async function deleteProfilePhoto(username) {
// GET PREFERENCES
export async function getPreferences(username) {
const { data } = await App.apiClient.get(`${LMS_BASE_URL}/api/user/v1/preferences/${username}`);
const { data } = await App.apiClient.get(`${App.config.LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return camelCaseObject(data);
}
@@ -106,7 +106,7 @@ export async function patchPreferences(username, params) {
visibility_time_zone: 'visibility.time_zone',
});
await App.apiClient.patch(`${LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
await App.apiClient.patch(`${App.config.LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
@@ -124,7 +124,7 @@ function transformCertificateData(data) {
cert.download_url.search(/http[s]?:\/\//) !== 0;
const downloadUrl = urlIsPath ?
`${LMS_BASE_URL}${cert.download_url}` :
`${App.config.LMS_BASE_URL}${cert.download_url}` :
cert.download_url;
transformedData.push({
@@ -137,7 +137,7 @@ function transformCertificateData(data) {
}
export async function getCourseCertificates(username) {
const url = `${LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
const url = `${App.config.LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
try {
const { data } = await App.apiClient.get(url);
return transformCertificateData(data);

View File

@@ -1,25 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'babel-polyfill';
Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx-jwt-cookie-header-payload';
process.env.BASE_URL = 'localhost:1995';
process.env.CREDENTIALS_BASE_URL = 'http://localhost:18150';
process.env.CSRF_COOKIE_NAME = 'csrftoken';
process.env.CSRF_TOKEN_API_PATH = '/csrf/api/v1/token';
process.env.ECOMMERCE_BASE_URL = 'http://localhost:18130';
process.env.LANGUAGE_PREFERENCE_COOKIE_NAME = 'openedx-language-preference';
process.env.LMS_BASE_URL = 'http://localhost:18000';
process.env.LOGIN_URL = 'http://localhost:18000/login';
process.env.LOGOUT_URL = 'http://localhost:18000/login';
process.env.MARKETING_SITE_BASE_URL = 'http://localhost:18000';
process.env.ORDER_HISTORY_URL = 'localhost:1996/orders';
process.env.REFRESH_ACCESS_TOKEN_ENDPOINT = 'http://localhost:18000/login_refresh';
process.env.SEGMENT_KEY = null;
process.env.SITE_NAME = 'edX';
process.env.USER_INFO_COOKIE_NAME = 'edx-user-info';

21
webpack.prod.config.js Normal file
View File

@@ -0,0 +1,21 @@
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('webpack-prod', {
plugins: [
// Scan files for class names and ids and remove unused css
new PurgecssPlugin({
paths: [].concat(
// Scan files in this app
glob.sync('src/**/*', { nodir: true }),
// Scan files in any edx frontend-component
glob.sync('node_modules/@edx/frontend-component*/**/*', { nodir: true }),
// Scan files in paragon
glob.sync('node_modules/@edx/paragon/**/*', { nodir: true }),
),
// Protect react-css-transition class names
whitelistPatterns: [/-enter/, /-appear/, /-exit/],
}),
],
});

View File

@@ -1,16 +0,0 @@
// This is the common Webpack config. The dev and prod Webpack configs both
// inherit config defined here.
const path = require('path');
module.exports = {
entry: {
app: path.resolve(__dirname, '../src/index.jsx'),
},
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
},
resolve: {
extensions: ['.js', '.jsx'],
},
};

View File

@@ -1,173 +0,0 @@
// This is the dev Webpack config. All settings here should prefer a fast build
// time at the expense of creating larger, unoptimized bundles.
const Merge = require('webpack-merge');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const PostCssRtlPlugin = require('postcss-rtl');
const commonConfig = require('./webpack.common.config.js');
module.exports = Merge.smart(commonConfig, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
// enable react's custom hot dev client so we get errors reported in the browser
hot: require.resolve('react-dev-utils/webpackHotDevClient'),
app: path.resolve(__dirname, '../src/index.jsx'),
},
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
// The babel-loader transforms newer ES2015+ syntax to older ES5 for older browsers.
// Babel is configured with the .babelrc file at the root of the project.
{
test: /\.(js|jsx)$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../footer'),
path.resolve(__dirname, '../header'),
],
loader: 'babel-loader',
options: {
// Caches result of loader to the filesystem. Future builds will attempt to read from the
// cache to avoid needing to run the expensive recompilation process on each run.
cacheDirectory: true,
},
},
// We are not extracting CSS from the javascript bundles in development because extracting
// prevents hot-reloading from working, it increases build time, and we don't care about
// flash-of-unstyled-content issues in development.
{
test: /(.scss|.css)$/,
use: [
'style-loader', // creates style nodes from JS strings
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [PostCssRtlPlugin()],
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
},
],
},
{
test: /.svg$/,
issuer: {
test: /\.jsx?$/,
},
loader: '@svgr/webpack',
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
// bundles. This makes the javascript bundles ginormous and defeats caching so we will use the
// file-loader instead to copy the files directly to the output directory.
{
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file-loader',
},
{
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
optimizationlevel: 7,
mozjpeg: {
progressive: true,
},
gifsicle: {
interlaced: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
},
},
],
},
],
},
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(__dirname, '../public/index.html'),
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
// App configuration
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'localhost:1995',
CREDENTIALS_BASE_URL: 'http://localhost:18150',
CSRF_COOKIE_NAME: 'csrftoken',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/login',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
ORDER_HISTORY_URL: 'localhost:1996/orders',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
SEGMENT_KEY: null,
SITE_NAME: 'edX',
USER_INFO_COOKIE_NAME: 'edx-user-info',
// ProfileFooter configuration
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
CONTACT_URL: 'http://localhost:18000/contact',
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
ENTERPRISE_MARKETING_URL: 'http://example.com',
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'my_campaign',
ENTERPRISE_MARKETING_UTM_SOURCE: 'edX profile',
FACEBOOK_URL: 'https://www.facebook.com',
GOOGLE_PLAY_URL: 'https://play.google.com/store',
LINKED_IN_URL: 'https://www.linkedin.com',
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
REDDIT_URL: 'https://www.reddit.com',
SUPPORT_URL: 'http://localhost:18000/support',
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
TWITTER_URL: 'https://twitter.com',
YOU_TUBE_URL: 'https://www.youtube.com',
}),
// when the --hot option is not passed in as part of the command
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
// https://webpack.js.org/configuration/dev-server/#devserver-hot
new webpack.HotModuleReplacementPlugin(),
],
// This configures webpack-dev-server which serves bundles from memory and provides live
// reloading.
devServer: {
host: '0.0.0.0',
port: 1995,
historyApiFallback: true,
hot: true,
inline: true,
publicPath: '/',
},
});

View File

@@ -1,222 +0,0 @@
// This is the prod Webpack config. All settings here should prefer smaller,
// optimized bundles at the expense of a longer build time.
const Merge = require('webpack-merge');
const commonConfig = require('./webpack.common.config.js');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackNewRelicPlugin = require('html-webpack-new-relic-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const NewRelicSourceMapPlugin = require('new-relic-source-map-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // eslint-disable-line prefer-destructuring
const PostCssRtlPlugin = require('postcss-rtl');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const CssNano = require('cssnano');
module.exports = Merge.smart(commonConfig, {
mode: 'production',
devtool: 'source-map',
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, '../dist'),
},
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
// The babel-loader transforms newer ES2015+ syntax to older ES5 for older browsers.
// Babel is configured with the .babelrc file at the root of the project.
{
test: /\.(js|jsx)$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../footer'),
path.resolve(__dirname, '../header'),
],
loader: 'babel-loader',
},
{
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
},
// Webpack, by default, includes all CSS in the javascript bundles. Unfortunately, that means:
// a) The CSS won't be cached by browsers separately (a javascript change will force CSS
// re-download). b) Since CSS is applied asyncronously, it causes an ugly
// flash-of-unstyled-content.
//
// To avoid these problems, we extract the CSS from the bundles into separate CSS files that
// can be included as <link> tags in the HTML <head> manually.
//
// We will not do this in development because it prevents hot-reloading from working and it
// increases build time.
{
test: /(.scss|.css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [
PostCssRtlPlugin(),
PostCssAutoprefixerPlugin({ grid: true }),
CssNano(),
],
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
},
],
},
{
test: /.svg$/,
issuer: {
test: /\.jsx?$/,
},
loader: '@svgr/webpack',
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
// bundles. This makes the javascript bundles ginormous and defeats caching so we will use the
// file-loader instead to copy the files directly to the output directory.
{
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file-loader',
},
{
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
optimizationlevel: 7,
mozjpeg: {
progressive: true,
},
gifsicle: {
interlaced: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
},
},
],
},
],
},
// New in Webpack 4. Replaces CommonChunksPlugin. Extract common modules among all chunks to one
// common chunk and extract the Webpack runtime to a single runtime chunk.
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
},
},
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Cleans the dist directory before each build
new CleanWebpackPlugin(['dist'], {
root: path.join(__dirname, '../'),
}),
// Writes the extracted CSS from each entry to a file in the output directory.
new MiniCssExtractPlugin({
filename: '[name].[chunkhash].css',
}),
// Scan files for class names and ids and remove unused css
new PurgecssPlugin({
paths: [].concat(
// Scan files in this app
glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }),
// Scan files in any edx frontend-component
glob.sync(`${path.resolve(__dirname, '../node_modules/@edx/frontend-component')}*/**/*`, { nodir: true }),
// Scan files in paragon
glob.sync(`${path.resolve(__dirname, '../node_modules/@edx/paragon')}/**/*`, { nodir: true }),
),
// Protect react-css-transition class names
whitelistPatterns: [/-enter/, /-appear/, /-exit/],
}),
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(__dirname, '../public/index.html'),
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
// App configuration
ACCESS_TOKEN_COOKIE_NAME: null,
BASE_URL: null,
CREDENTIALS_BASE_URL: null,
CSRF_COOKIE_NAME: 'csrftoken',
CSRF_TOKEN_API_PATH: null,
ECOMMERCE_BASE_URL: null,
LANGUAGE_PREFERENCE_COOKIE_NAME: null,
LMS_BASE_URL: null,
LOGIN_URL: null,
LOGOUT_URL: null,
MARKETING_SITE_BASE_URL: null,
ORDER_HISTORY_URL: null,
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
SEGMENT_KEY: null,
SITE_NAME: null,
USER_INFO_COOKIE_NAME: null,
// ProfileFooter configuration
APPLE_APP_STORE_URL: null,
CONTACT_URL: null,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: null,
ENTERPRISE_MARKETING_URL: null,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: null,
ENTERPRISE_MARKETING_UTM_SOURCE: null,
FACEBOOK_URL: null,
GOOGLE_PLAY_URL: null,
LINKED_IN_URL: null,
OPEN_SOURCE_URL: null,
PRIVACY_POLICY_URL: null,
REDDIT_URL: null,
SUPPORT_URL: null,
TERMS_OF_SERVICE_URL: null,
TWITTER_URL: null,
YOU_TUBE_URL: null,
}),
new HtmlWebpackNewRelicPlugin({
// This plugin fixes an issue where the newrelic script will break if
// not added directly to the HTML.
// We use non empty strings as defaults here to prevent errors for empty configs
license: process.env.NEW_RELIC_LICENSE_KEY || 'fake_app',
applicationID: process.env.NEW_RELIC_APP_ID || 'fake_license',
}),
new NewRelicSourceMapPlugin({
applicationId: process.env.NEW_RELIC_APP_ID,
nrAdminKey: process.env.NEW_RELIC_ADMIN_KEY,
staticAssetUrl: process.env.BASE_URL,
noop: typeof process.env.NEW_RELIC_ADMIN_KEY === 'undefined', // upload source maps in prod builds only
}),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
}),
],
});