Contributing Guidelines

Table of Contents

Project Structure

.
├── nginx.conf                # Nginx server configuration
├── public/                   # Static files
├── src/                      # Single page application source script
├── .dockerignore             # .dockerignore
├── .eslintrc                 # Eslint configuration
├── .gitignore                # .gitignore
├── .bitbucket-pipelines.yml  # CI/CD script
├── CONTRIBUTING.md           # Contributing guidelines
├── docker-compose.yml        # Docker compose script to set dev environment
├── Dockerfile                # Docker file
├── package.json              # package.json
├── README.md                 # README

Contributing

Requirements

  • docker>=17.0.0

  • docker-compose>=1.17.0

  • node>=10.0.0

Set-up dev environment

#. If not yet done, clone project locally

git clone <project-url> && cd <project-folder>

#. If not yet done, install node dependencies

npm install # install node dependencies locally

#. Start application

npm start # start application in dev mode

You can now access application in dev mode at http://localhost

Start developing

  1. Create a new branch

Requirements

  • New branch MUST be started from development (which is our dev branch)

  • Feature branch MUST be named feature/[a-zA-Z0-9\-]+

  • Bug fix branch MUST be named fix/[a-zA-Z0-9\-]+

  • Branch referring to an open issue MUST be named (fix|feature)/<issue-ID>

  1. Develop on new branch being careful to write test for every change you proceed to

  2. Push branch

git push origin -u <branch>

Pushing will trigger a CI pipeline (see section CI/CD)

  1. Create a pull request

Install new dependencies

npm install <package> # install package, and update package.json & package-lock.json

Command above can sometime error with EACCES code, this means that docker wrote some files (usually cache) in your local node_modules folder. To solve it you can change right access of the folder by running command

sudo chown -R $USER:$USER node_modules

Release a new version

  1. Create a release branch release/x.x.x

  2. Bump to version x.x.x

    • package.json: change version to x.x.x

    • CHANGELOG.md: ensure release section x.x.x documentation is exhaustive and set release date

  3. Commit with message bump version to x.x.x

git commit -am "bump version to x.x.x"
  1. Tag version

git tag -a x.x.x -m "Version x.x.x"
  1. Bump to version increment(x.x.x)-dev

    • package.json: change version to increment(x.x.x)-dev

    • CHANGLOG.md: create empty increment(x.x.x)-dev section with unreleased status

  2. Push branch and tags

git push origin -u release/x.x.x --follow-tags
  1. Proceed to merge request release/x.x.x -> master

Tests

Commands

Run test

npm test

Display more DOM lines when a test fails

DEBUG_PRINT_LIMIT=10000 npm test

Run coverage

npm test -- --coverage

Practices

Test framework

We use Jest test framework (directly packed into create-react-app

We use React testing library to test components

Requirements

  • Tests MUST be written in a **/__tests__ folder located in the same directory as the element tested

  • Tests file for <filename>.js MUST be named <filename>.test.js

Folder structure

src/
:
├── path/to/folder/
│   ├── __test__/
│   │   ├── file1.test.js
│   │   └── file2.test.js
│   ├── file1.js
│   └── file2.js
:

Testing guidelines

Here are some useful reading to help you write tests:

  • https://kentcdodds.com/blog/how-to-know-what-to-test

  • https://kentcdodds.com/blog/common-mistakes-with-react-testing-library

  • Anything displayed here: https://kentcdodds.com/testing/

Test example

// Section 1: Imports (Low level & from application)
import React from "react";
import TheComponent from "../TheComponent";

import {render, screen} from '@testing-library/react'

// Section 2: Constants & Helper functions
const FAKE_CONSTANT = 'FAKE_CONSTANT'

// Implement "reducer" function with initial state as default state
describe("TheComponent component", () => {
    it('renders without crashing', () => {
      render(<TheComponent></TheComponent>)
    })

    it('does this when..', () => {
      // ...
    })
})

Code linting

Framework

We use ESLint (packed in create-react-app)

This project use a combination of husky, lint-staged, prettier to format code automatically including .js,.jsx, .json and .css (c.f create-react-app documentation.

Some pre-commit hooks are set-up so whenever you make a commit, prettier will format the changed files automatically.

Requirements

  • Code MUST respect linting rules defined in .eslintrc

Commit Linting

Framework

We use commitlint

The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.

The commit message should be structured as follows:

<type>[optional scope]: <description>

[optional body]

[optional footer]

Types of commit:

  • feat: Add a new feature to the codebase (MINOR in semantic versioning).

  • fix: Fix a bug (equivalent to a PATCH in Semantic Versioning).

  • docs: Documentation changes.

  • style: Code style change (semicolon, indentation...).

  • refactor: Refactor code without changing public API.

  • perf: Update code performances.

  • test: Add test to an existing feature.

  • ci: Update continuous integration processes

  • chore: Update something without impacting the user (ex: bump a dependency in package.json).

Requirements

  1. Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space.

  2. The type feat MUST be used when a commit adds a new feature to your application or library.

  3. The type fix MUST be used when a commit represents a bug fix for your application.

  4. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser):

  5. A description MUST immediately follow the colon and space after the type/scope prefix. The description is a short summary of the code changes, e.g., fix: array parsing issue when multiple spaces were contained in string.

  6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description.

  7. A commit body is free-form and MAY consist of any number of newline separated paragraphs.

  8. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a :<space> or <space># separator, followed by a string value (this is inspired by the git trailer convention).

  9. A footer’s token MUST use - in place of whitespace characters, e.g., Acked-by (this helps differentiate the footer section from a multi-paragraph body). An exception is made for BREAKING CHANGE, which MAY also be used as a token.

  10. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed.

  11. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer.

  12. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE: environment variables now take precedence over config files.

  13. If included in the type/scope prefix, breaking changes MUST be indicated by a ! immediately before the :. If ! is used, BREAKING CHANGE: MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change.

  14. Types other than feat and fix MAY be used in your commit messages, e.g., docs: updated ref docs.

  15. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.

  16. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer

Files organization & file naming convention

We make active usage of redux, if not familiar with redux we recommend going through redux documentation before going through this section.

Requirements

  • Actions MUST go into a into src/redux/actions

  • Reducers MUST go into a into src/redux/reducers

  • Enhancers MUST go into a into src/redux/enhancers

Folder structure

src/
:
├── redux/
│   ├── actions/               # Actions
│   ├── enhancers/             # Enhancers
│   └── reducers/              # Reducers
:

Reducers

We highly recommend you reading more about structuring reducer

Requirements

  • State structure (or state shape) MUST be defined in terms of domain data & app status, it MUST NOT be defined after your UI component tree

  • Root reducer (feeded to createStore()) MUST combine together specialized reducers:

    • data reducers, handling the data application needs to show, use, or modify (typically information retrieved from some APIs)

    • status reducers, handling information related to application's behavior (such as "there is a request in progress")

    • ui reducer, handling how the UI is currently displayed (such as "Sidebar is open")

  • Global state shape MUST reflect src/redux/reducers/ folder structure. Keys in global state MUST be the same as file names src/redux/reducers/

  • Specialized reducer files MUST implement a reducer function exported as default

Folder structure

src/
:
├── redux/
│   :
│   ├── reducers/
│   │   ├── data/
│   │   │   ├── domain1.js        # Reducer responsible to handle state slice related to domain 1 data
│   │   │   └── domain2.js        # Reducer responsible to handle state slice related to domain 2 data
│   │   ├── status/
│   │   │   └── bahaviour1.js      # Reducer responsible to handle state slice related to application status behavior 1
│   │   └── ui/
│   │       └── element1.js       # Reducer responsible to handle state slice related to UI element 1
:   :

Corresponding state shape would be

{
    data : {
        domain1: {},
        domain2: {}
    },
    status : {
        category1: {}
    },
    ui : {
        element1: {}
    }
}

Example

// Import action of interest as constants
import { OPEN_SIDEBAR, CLOSE_SIDEBAR } from "actions/ui";

// Define initial state
const initialState = {
    open: false
};

// Implement "reducer" function with initial state as default state
export default (state = initialState, { type }) => {
    switch (type) {
        case OPEN_SIDEBAR:
            return {
                ...state,
                open: true
            };

        case CLOSE_SIDEBAR:
            return {
                ...state,
                open: false
            };

        default:
            return state;
    }
};

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Requirements

  • Actions MUST be grouped by piece of state they cover (e.g. UI actions are defined into src/redux/actions/ui.js)

  • Each action type MUST be declared as a constant

  • Each action SHOULD come with an action creator function

  • Action types & creator functions MUST be implemented in the same file

  • Actions MUST be serializable (if you think in passing a function into an action for handling by reducers, it means that you actually need a redux middleware)

  • Actions MUST follow Flux-Standard-Action(https://github.com/redux-utilities/flux-standard-action#actions) format

    • MUST be a plain JavaScript object

    • MUST have a type property

    • MAY have an error property

    • MAY have an payload property

    • MAY have an meta property

    • MUST NOT include property other than type, error, payload and meta

  • There SHOULD NOT be a 1-to-1 link between actions and reducers. Typically an action could be reduced by multiple reducers

Folder structure

src/
:
├── redux/
│   ├── actions/              #
│   │   ├── data              # Action related to domain Data
│   │   │   ├── domain1.js    # Action responsible to handle actions of domain 1
│   │   │   └── domain2.js    # Action responsible to handle actions of domain 2
│   │   ├── ui                # Action related to UI modification
│   │   │   └── ui.js         # Action responsible to handle actions of the UI
│   │   └── status            # Actions related to status information
:

Example

// Section 1: Utilities imports and low level imports
import axios from "axios";

// Section 2: Action types declarations and action creators
// Declare action type as a constant
export const ADD_TODO = "ADD_TODO";
// Declare action creator (remove the export on actions creators if handled only by section 3 async funcs)
export const addTodo = (text, category) => ({
    // Respect FSA standard format (type, payload, meta properties)
    type: ADD_TODO,
    payload: {
        text,
    },
    meta: {
        category,
    },
});
// Section 3: Action dispatch methods and async funcs
export const onLendActivate = ({ param1, param2 }) => async (dispatch) => {
    try {
        const response = await axios.get("/todo");
        dispatch(addTodo(response.data.text, response.data.category));
    } catch (error) {
        dispatch(someErrorNotification(error));
    }
};

Enhancers and middlewares

Store enhancers are higher-order function that composes a store creator to return a new, enhanced store creator. In our case, we mainly use enhancer to add redux middlewares allowing to hook custom behaviors when dispatching actions.

We highly recommend you reading more about middlewares in redux

Requirements

  • Middlewares MUST go into src/redux/enhancers/middlewares

Folder structure

src/
:
├── enhancers/
│   ├── middlewares/         # Middlewares
│   │   ├── index.js         # Combine middlewares into one enhancer
│   │   └── router.js        # Middleware managing router
│   ├── reduxDevTools.js     # Enhancer to include ReduxDevTool
│   └── index.js             # Combine enhancers into on root enhancer
:

Components go into src/components

Requirements

  • All components MUST go into src/components

  • Components MUST have a unique name

Module specific components go into src/components/

Requirements

  • Component files MUST go into a into a folder named after the component in src/components/

  • Component code MUST be split into as many atomic sub components as necessary

  • Component MUST follow naming pattern path-based-component-naming, which consists in naming the component accordingly to its relative path from src/components/

Examples of those components are

  • Skeleton elements such as AppBar, Sidebar

  • View panels such as Home

  • Layout elements such as Layout

Folder structure and file naming

src/
:
├── components/
│   :
│   ├── AppBar/              # Component file goes into a folder named after the component
│   │   ├── __tests__/       # Component tests folder
│   │   ├── AppBar.js        # Main component file (named as the folder)
│   │   ├── IconButton.js    # Sub component file (we do not repeat AppBar in file's name)
│   │   ├── Title.js         # Sub component file (we do not repeat AppBar in file's name)
│   │   └── Toolbar.js       # Sub component file (we do not repeat AppBar in file's name)
:   :   :

Component naming

Main component

# src/components/AppBar/AppBar.js

// Follow path-based-component-naming (not repeating)
const AppBar = () => (
    // Component code comes here
);

Sub component

# src/components/AppBar/IconButton.js

// Follow path-based-component-naming
const AppBarIconButton = () => (
    // Component code comes here
);

Generic atomic reusable components go into src/components/UI

Requirements

  • Generic UI components MUST NOT held business logic specific to the application (they actually could be stored on some external npm library)

  • MUST follow naming pattern path-based-component-naming, which consists in naming the component accordingly to its relative path from src/components/UI

Examples of those components are: Button, Input, Checkbox, Select, Modal, etc…

Folder structure and file naming

src/
:
├── components/
│   :
│   ├── UI/
│   │   ├── ListItem/
│   │   │   ├── __test__/
│   │   │   ├── 1.js
│   │   │   └── 2.js
│   │   ├── Button/
│   │   │   ├── __test__/
│   │   │   └── 1.js
:   :   :

Containers go into src/containers

Requirements

  • Container MUST go into src/containers

  • Container MUST follow same relative path from src/containers as the component it wraps from src/components

  • Container MUST have same name as the component it wraps

Folder structure

src/
:
├── containers/
│   :
│   ├── AppBar/
│   │   ├── __tests__/
│   │   ├── AppBar.js
│   │   ├── IconButton.js
│   │   └── Title.js
:   :   :

Custom hooks go into src/hooks

Requirements

  • Custom hook MUST go into src/hooks

  • Custom hook's name MUST start with use (for example: useTimer)

Folder structure

src/
:
├── hooks/
│   :
│   ├── useHook.js
:   :

Component & Container separation pattern

We respect a separation between presentational components & containers.

Motivation for the pattern

  • Better separation of concerns makes app understandable and better UI writing components this way.

  • Better reusability. Same presentational component can be used with completely different state sources, and turn those into separate container components that can be further reused.

  • Presentational components are essentially app’s “palette”. It is possible to put them on a single page and let designers tweak all their variations without touching the app’s logic. It is possible to run screenshot regression tests on that page.

  • This forces to extract “layout components” such as Sidebar, AppBar, etc. and use this.props.children instead of duplicating the same markup and layout in several container components.

You can read more about it here

Components (or presentational components) are concerned with how things look

Requirements

  • SHOULD implement some DOM markup and styles of their own

  • MAY contain both presentational components and containers

  • SHOULD allow containment via this.props.children.

  • SHOULD NOT have their own state (when they do, it MUST be UI state and MUST NOT be data)

  • SHOULD be written as functional components unless they need state, lifecycle hooks, or performance optimizations

  • MUST receive data and callbacks exclusively via props.

  • MUST NOT have dependencies with the rest of the app, such as redux actions or stores

  • MUST NOT specify how the data is loaded or mutated (API calls MUST NOT be defined in a component)

  • MUST NOT define any route

Implement a component

Set imports

Requirements

  • MAY import components from UI libraries typically Material-UI

  • MAY import components and containers from the rest of the application

  • SHOULD NOT import any resources related to Redux, except compose() that is sometime convenient to connect multiple material-ui wrappers (withStyles(), withTheme()...)

  • SHOULD NOT have any dependencies in the rest of the application, except components or containers

  • MUST be organized in 3 ordered sections: 1. low-level React imports / 2. Material-UI imports / 3. intra-application imports

Example

// Section 1: React low level imports
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import { compose } from "redux";

// Section 2: Material-UI imports
import MuiAppBar from "@material-ui/core/AppBar";
import { withStyles } from "@material-ui/core/styles";

// Section 3: Components & Containers import from the application
import AppBarToolbar from "./Toolbar";

Define styles

Requirements

  • MUST be a function taking theme as argument and returning an object

Example

const styles = (theme) => ({
    appBar: {
        position: "fixed",
        top: 0,
        zIndex: theme.zIndex.drawer + 1,
        transition: theme.transitions.create(["width", "margin"], {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
        }),
    },
    appBarShifted: {
        marginLeft: 240,
        width: `calc(100% - ${240}px)`,
        transition: theme.transitions.create(["width", "margin"], {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.enteringScreen,
        }),
        [theme.breakpoints.down("sm")]: {
            width: "100%",
        },
    },
});

Code component

Requirements

  • SHOULD be a function taking a props object as an argument (except lifecycle, UI state or some optimization are required)

  • MUST respect component naming convention (see below)

  • MUST be atomic

  • MUST be agnostic of the rest of the application, this MUST include every variable namings. Think it as it should be able to exist on its own

  • MUST be documented with PropTypes

Example

const AppBar = ({
    classes,
    shifted, // for agnosticity we use variable name "shifted" over "sidebarOpen"
}) => (
    <MuiAppBar
        position="absolute"
        className={classNames(classes.appBar, shifted && classes.appBarShifted)}
    >
        <AppBarToolbar shifted={shifted} />
    </MuiAppBar>
);

// Documentation with PropTypes
AppBar.propTypes = {
    classes: PropTypes.object.isRequired,
    shifted: PropTypes.bool,
};

Connect styles and export

Requirements

  • MAY inject styles information using withStyle(), makeStyle(), _withTheme(), withWidth()

  • There MUST be one unique export

Example

Basic: only withStyle()

export default withStyles(styles)(AppBar);

Advanced: using compose() from redux

export default compose(withTheme(), withStyles(styles))(AppBar);

Containers are concerned with how things work

Requirements

  • MAY contain both presentational components and containers

  • SHOULD NOT define DOM markup of their own (except for some wrapping divs)

  • MUST NOT have styles

  • MAY provide the data and behavior to presentational or other container components.

  • MAY organise components/containers using routes

  • MAY implement redux related elements mapStateToProp(), mapDispatchToProps(), mergeProps() and connect it to presentational component using connect()

  • MAY implement API calls or other side effects

  • MAY be stateful, as they tend to serve as data sources

Implement a container

Set imports

Requirements

  • MAY import elements from state management libraries elements (redux, react-redux)

  • MAY import elements from src/redux such as actions or selectors

  • MAY import elements from routing library

  • MAY import Material-UI utilities such as isWidthUp()

Example

// Section 1: React/Redux low level imports
import React from "react";

// Section 2: internal imports
import Markets from "components/Markets/Markets";

import useFetchMarkets from "hooks/useFetchMarkets";

// Section 3: Selectors

// Section 4: Container body
const MarketsContainer = () => {
    const { supplyArray, borrowArray } = useFetchMarkets(true);

    return <Markets supplyArray={supplyArray} borrowArray={borrowArray} />;
};

// Section 5: Exports
export default MarketsContainer;

Code container

Requirements

  • SHOULD NOT define DOM markup of their own (except for some wrapping divs)

  • MAY define route

  • MAY implement hooks on React lifecycle

  • MUST respect

Example

Lifecycle container

class WithLifecycleHooks extends Component {
    componentDidMount() {
        // Could perform some API calls here
    }
    componentWillUnmount() {
        // What to do when lifecycle ends
    }

    render() {
        // return a presentational component
    }
}

Routing container

const Layout = () => (
    <Switch>
        <Route path="/" exact component={() => <Redirect to={HOME} />} />
        <Route component={LayoutSkeleton} />
    </Switch>
);

Implement mapStateToProps(state, [ownProps]), mapDispatchToProps(dispatch, [ownProps]), mergeProps(stateProps, dispatchProps, ownProps)

Requirements

  • MAY implement mapStateToProps(state, [ownProps])

  • MAY implement mapDispatchToProps(dispatch, [ownProps]). If it is only required to map action creators it MUST be an object

  • MAY implement mergeProps(stateProps, dispatchProps, ownProps)

  • Aggregating props MUST NOT be performed within container, ownProps and mergeProps() allow to accomplish it properly out of the component (c.f. https://github.com/reduxjs/react-redux/blob/master/docs/api.md)

Example

const mapStateToProps = (state) => ({
    shifted: state.ui.sideBarOpen,
});

const mapDispatchToProps = {
    onClick: openSidebar,
};

Connect container & export

Requirements

  • MAY connect redux information to a comp

  • MUST be one unique export

Example

export default connect(mapStateToProps)(AppBar);

Others

Error handling: Refer to to this article for handling async errors using our built ErrorFallback.js file.

Material-UI

Theme

Material UI theme is declared in ./src/themes/theme.js

CSS classes

To apply styling on specific components and not in the full app, use Material-UI overrides

UI width

To make UI fully responsive, you can use Material-UI breakpoints

Audit application

  • Use Lighthouse of Google in order to audit your PWA and see what's missing (icons, https, ...)

  • Follow the guidelines here (Google opinionated way of building PWA)

Conventions

RFC keywords

  1. MUST This word, or the terms "REQUIRED" or "SHALL", mean that the definition is an absolute requirement of the specification.

  2. MUST NOT This phrase, or the phrase "SHALL NOT", mean that the definition is an absolute prohibition of the specification.

  3. SHOULD This word, or the adjective "RECOMMENDED", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.

  4. SHOULD NOT This phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood and the case carefully weighed before implementing any behaviour described with this label.

  5. MAY This word, or the adjective "OPTIONAL", mean that an item is truly optional. One vendor may choose to include the item because a particular marketplace requires it or because the vendor feels that it enhances the product while another vendor may omit the same item. An implementation which does not include a particular option MUST be prepared to interoperate with another implementation which does include the option, though perhaps with reduced functionality. In the same vein an implementation which does include a particular option MUST be prepared to interoperate with another implementation which does not include the option (except, of course, for the feature the option provides.)

Last updated