Skip to content

Commit

Permalink
Showing 27 changed files with 23,754 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PORT=2999
ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
BASE_URL=localhost:2999
CSRF_TOKEN_API_PATH=/csrf/api/v1/token
LMS_BASE_URL=http://localhost:18000
LOGIN_URL=http://localhost:18000/login
LOGOUT_URL=http://localhost:18000/login
REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh
SITE_NAME=edX
Empty file added .env.test
Empty file.
5 changes: 5 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
coverage
dist
example
node_modules
jest.config.js
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('eslint');
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Default CI
on:
push:
branches:
- 'master'
pull_request:
branches:
- '**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v3
10 changes: 10 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Run commitlint on the commit messages in a pull request.

name: Lint Commit Messages

on:
- pull_request

jobs:
commitlint:
uses: edx/.github/.github/workflows/commitlint.yml@master
13 changes: 13 additions & 0 deletions .github/workflows/lockfileversion-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Check package-lock file version

name: Lockfile Version check

on:
push:
branches:
- master
pull_request:

jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.cache
.DS_Store
coverage
dist
node_modules
temp
src/i18n/transifex_input.json
.vscode/
*~
module.config.js
.idea/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export TRANSIFEX_RESOURCE = frontend-component-authn-edx
transifex_resource = frontend-component-authn-edx
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"

transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json

# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs

build:
rm -rf ./dist
./node_modules/.bin/fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/__mocks__,**/__snapshots__,**/setupTest.js --copy-files
@# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
@rm -rf dist/**/*.test.jsx
@rm -rf dist/**/__snapshots__
@rm -rf dist/__mocks__

requirements:
npm install

test:
npm run test

i18n.extract:
# Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp)
npm run-script i18n_extract

i18n.concat:
# Gathering JSON messages into one file...
$(transifex_utils) $(transifex_temp) $(transifex_input)

extract_translations: | requirements i18n.extract i18n.concat

# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)

# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh

# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)

# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('babel-preserve-modules');
10 changes: 10 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: auto
threshold: 0%
patch:
default:
target: auto
threshold: 0%
32 changes: 32 additions & 0 deletions docs/decisions/0001-record-architecture-decisions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
1. Record Architecture Decisions
--------------------------------

Status
------

Accepted

Context
-------

We would like to keep a historical record on the architectural
decisions we make with this app as it evolves over time.

Decision
--------

We will use Architecture Decision Records, as described by
Michael Nygard in `Documenting Architecture Decisions`_

.. _Documenting Architecture Decisions: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions

Consequences
------------

See Michael Nygard's article, linked above.

References
----------

* https://resources.sei.cmu.edu/asset_files/Presentation/2017_017_001_497746.pdf
* https://github.com/npryce/adr-tools/tree/master/doc/adr
213 changes: 213 additions & 0 deletions docs/decisions/0002-feature-based-application-organization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
2. Feature-based Application Organization
-----------------------------------------

Status
------

Proposed

Context
-------

The common, naive approach to organizing React/Redux applications says that code should be grouped into folders by type, putting like-types of code together. This means having directories like:

- components
- actions
- reducers
- constants
- selectors
- sagas
- services

This is often referred to as a "Ruby on Rails" approach, which organizes applications similarly.

As applications grow, it's acknowledged by the community that this organization starts to fall down and become difficult to maintain. It does nothing to help engineers keep their code modular and decoupled, as it groups code by how it looks and not how it's used. Code that functions as part of a unit is spread out over 7+ directories.

This ADR documents an approach and rules of thumb for organizing code modularly by feature, informed by articles and prior art.

Note on terminology: "feature" and "module" are used interchangeably in this ADR. In general, the feature refers to the semantically significant thing, whereas the module refers to the directory of code pertaining to that feature.

Decision
--------

**Following the spirit of these principles is more important than following them to the letter.**

These rules are guidelines. It won't always be reasonable or necessary to follow them to the letter. They provide a set of tools for dealing with complexity. It follows, then, that if you don't have complexity, then you may not need the tools.

Primary guiding principles
==========================

**1. Code is organized into feature directories.**

A feature is a logical or semantically significant grouping of code; what comprises a feature is subjective. It also may not be obvious at first - if code tends to be related or change together, then it's probably part of the same feature.

It's unlikely to be worth agonizing over your feature breakdown; time will tell what's correct moreso than overthinking it. That said, a sufficiently complex set of features will need a similarly robust taxonomy and organizational hierarchy. (This document endeavors to help inform that hierarchy.) A nice rule of thumb is that a feature should conceptually be able to be extracted into its own npm package with minimal effort.

**2. Create strict module boundaries**

A module should have a public interface exposed via an index.jsx file in the module directory. Consumers of a feature should limit themselves to importing only from the public exports.

::

import { MyComponent, reducer as myComponentReducer } from './submodule'; // Good
import MyComponent from './submodule/MyComponent'; // Bad
import reducer from './submodule/data/reducers'; // Bad

Modules are configured by their parent. Generally a module will expose a few things which need to be configured make use of them in the consuming code. The reason for doing this is so that the module doesn't make assumptions about it's context (effectively dependency injection).

Examples:

* For React components, this involves including them in JSX and giving them props.
* For services, this is calling their "configure" method and providing them apiClient/configuration, etc.
* For reducers, this is mounting the reducer at an agreed-upon place in the redux store's state tree.

**3. Avoid circular dependencies**

Circular dependencies are unresolvable in webpack, and will result in something being imported as 'undefined'. They're also incredibly difficult and frustrating to track down. Properly factoring your features and supporting modules should help avoid these sorts of issues. In general, a feature should never need to import from its parent or grandparents, and a more "general purpose" module should never be importing from a more specific one. If you find yourself importing from a domain-specific feature in your general utility module, then something is probably ill-factored.

File and directory naming
=========================

This section details a specific taxonomy and hierarchy to help make code modular, approachable and maintainable.

**A. Separate data management from components.**

In order to isolate our view layer (React) from the management of our data, global state, APIs, and side effects, we want to adopt the "ducks" organization (see references). This involves isolating data management into a
sub directory of a feature. We'll use the directory name "data" rather than the traditional "ducks".

**C. React components will be named semantically.**

The convention for React components is for the file to be named for what the component does, so we will preserve this. A given feature may break up its view layer into multiple sub-components without a sub-feature being present.

**B. Files in a module's data directory are named by function.**

In the data sub-directory, the file names describe what each piece of code does. Unlike React components, all of the data handlers (actions, reducers, sagas, selectors, services, etc.) are generally short functions, and so we put them all in the same file together with others of their kind.

::

/profile
/index.jsx // public interface
/ProfilePage.jsx // view
/ProfilePhotoUploader.jsx // supporting view
/data // Note: most files here are named with a plural, as they contain many of the things in question.
/actions.js
/constants.js
/reducers.js
/sagas.js
/selectors.js
/service.js // Note: singular - there's one 'service' here that provides many methods.

If you find yourself desiring to have multiple files of a particular type in the data directory, this is a strong sign that you actually need a sub-feature instead.

**C. Sub-features follow the same naming scheme.**

Sub-features should follow the same rules as any other module.

A module with a sub-module:

::

/profile
/index.jsx // public interface
/ProfilePage.jsx
/Avatar.jsx // additional components for a feature reside here at the top level, not in a "components" subdirectory.
/data
/actions.js
/reducers.js
/sagas.js
/service.js
/profile-photo
/index.jsx // public interface
/ProfilePhoto.jsx
/data
/actions.js
/reducers.js
/selectors.js
/education // Sparse sub-module
/index.jsx // public interface
/Education.jsx
/site-language // No view layer sub-module
/index.jsx // public interface
/data
/actions.js
/reducers.js

Note that a given feature need not contain files of all types, nor is having files of all types a prerequisite for having a feature. A feature may not contain a view (Component) layer, or in contrast to that, may not need a data directory at all!

Importing rules of thumb
========================

It can be difficult to figure out where it's okay to import from. Following these rules of thumb will help maintain a healthy code organization and should prevent the possibility of circular dependencies.

**I. A feature may not import from its parentage.**

As described above in "Avoid circular dependencies", features should not import from their parent, grandparent, etc. A feature should be agnostic to the context in which it is used. If a module is importing from its parent or grandparent, that implies something is ill-factored.

**II. A feature may import from its children, but not its grandchildren.**

The feature may only import from the exports of its child, which may include exports of the grandchildren. Importing directly from grandchildren (or great grandchildren, etc.) would violate the strict module boundary of the child.

**II. Features may import from their siblings.**

It's acceptable to import from a module's siblings, or the siblings of their parents, grandparents, etc. This is necessary to support code re-use. As an example, assume we have a sub-module with common code to support our web forms.

::

/feature1
/sub-form-1
/sub-form-2
/forms-common-code

The sub-form modules can import from forms-common-code. The latter has its own strict module boundary and could conceptually be extracted into its own repository/completely independent module as far as they're concerned. They're unaware, conceptually, that it's a child of feature1, and they don't care.

**III. Features may import from the siblings of their parentage.**

This is less intuitive, but is not really any different than the above.

If another feature (feature2) also needs forms-common-code, it should be brought up a level so it's available to feature2, as feature2 cannot "reach into" feature1:

::

/feature1
/sub-form-1
/sub-form-2
/forms-common-code
/feature2 // can now use forms-common-code

In a complex app, you could imagine that forms-common-code needs to be brought up several levels, in which case our imports might look like:

::

import { formStuff } from '../../../forms-common-code';

This is okay. Conceptually it's no different than importing from a third party npm package, we just happen to know the code we want is up a few directories nearby, rather than using the syntactic sugar of a pathless import from node_modules.

At some point, if forms-common-code is general purpose enough, we may want to extract it from this repository/set of features all together.

Consequences
------------

This organization has been implemented in several of our micro-frontends so far (frontend-app-account and frontend-app-payment most significantly) and we feel it has improved the organization and approachability of the apps. When converting frontend-app-account to use this organization, it took 2-3 days to refactor the code.

It's worth noting that to get this right, it may actually involve changing the way the modules interact with each other. It isn't as simple as just moving files around and copy/pasting code. For instance, in frontend-app-account, it became obvious very quickly that to create strict module boundaries, we had to change the way that our service layers (server requests) were configured to keep them from importing their own configuration from their parent/grandparent. Similarly, our redux store tree of reducers became more complex and deeply nested.

References
----------

Articles on react/redux application organization:

* Primary reference:

- https://jaysoo.ca/2016/02/28/organizing-redux-application/

* Ducks references:

- https://github.com/erikras/ducks-modular-redux
- https://medium.freecodecamp.org/scaling-your-redux-app-with-ducks-6115955638be

* Other reading:

- https://hackernoon.com/fractal-a-react-app-structure-for-infinite-scale-4dab943092af
- https://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://redux.js.org/faq/code-structure

22 changes: 22 additions & 0 deletions example/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';

import React from 'react';
import ReactDOM from 'react-dom';
import { AppProvider } from '@edx/frontend-platform/react';
import { initialize, subscribe, APP_READY } from '@edx/frontend-platform';

import './index.scss';

subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
<div>Load the forms here</div>
</AppProvider>,
document.getElementById('root'),
);
});

initialize({
messages: []
});
4 changes: 4 additions & 0 deletions example/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import "@edx/brand/paragon/fonts";
@import "@edx/brand/paragon/variables";
@import "@openedx/paragon/scss/core/core";
@import "@edx/brand/paragon/overrides";
13 changes: 13 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
// If you want to add config BEFORE jest loads, use setupFiles instead.
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
coveragePathIgnorePatterns: [
'src/setupTest.js',
'src/i18n',
],
});
22,515 changes: 22,515 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@edx/frontend-component-authn-edx",
"version": "0.1.0",
"description": "Authentication related forms for edX",
"main": "dist/index.js",
"publishConfig": {
"access": "public"
},
"files": [
"/dist"
],
"scripts": {
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-component-authn-edx#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-component-authn-edx.git"
},
"bugs": {
"url": "https://github.com/openedx/frontend-component-authn-edx/issues"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.0.0",
"core-js": "3.36.0",
"react-redux": "7.2.9",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"regenerator-runtime": "0.14.1"
},
"devDependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@2.1.2",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "7.1.2",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "13.0.28",
"glob": "7.2.3",
"husky": "7.0.4",
"jest": "29.7.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"redux": "4.2.1"
},
"peerDependencies": {
"@edx/frontend-platform": "^7.1.0",
"@openedx/paragon": "^22.0.0",
"prop-types": "^15.8.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
}
}
12 changes: 12 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Authentication | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
</head>
<body>
<div id="root"></div>
</body>
</html>
33 changes: 33 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"extends": [
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"
}
2 changes: 2 additions & 0 deletions src/i18n/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Placeholder be overridden by `make pull_translations`
export default [];
Empty file added src/index.jsx
Empty file.
Empty file added src/index.scss
Empty file.
2 changes: 2 additions & 0 deletions src/setupTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
10 changes: 10 additions & 0 deletions webpack.dev.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const path = require('path');
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('webpack-dev', {
entry: path.resolve(__dirname, 'example'),
output: {
path: path.resolve(__dirname, 'example/dist'),
publicPath: '/',
},
});

0 comments on commit 203d56d

Please sign in to comment.