├── __mocks__ ├── .gitkeep └── react-i18next.js ├── src ├── types │ └── .gitkeep ├── utils │ └── .gitkeep ├── services │ └── .gitkeep ├── ui │ ├── @contexts │ │ └── .gitkeep │ ├── home │ │ ├── home.module.css │ │ ├── index.ts │ │ └── home.tsx │ ├── @components │ │ ├── alert │ │ │ ├── index.ts │ │ │ └── alert.tsx │ │ ├── spinner │ │ │ ├── index.ts │ │ │ └── spinner.tsx │ │ └── error-boundary │ │ │ ├── index.ts │ │ │ └── error-boundary.tsx │ ├── about │ │ ├── index.ts │ │ └── about.tsx │ ├── application │ │ ├── index.ts │ │ ├── @components │ │ │ ├── layout │ │ │ │ ├── index.ts │ │ │ │ ├── header │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── logo.ts │ │ │ │ │ │ └── logo.svg │ │ │ │ │ ├── navigation │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── language-switcher │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── language-switcher.tsx │ │ │ │ │ │ │ └── language-switcher.test.tsx │ │ │ │ │ │ └── navigation.tsx │ │ │ │ │ └── header.tsx │ │ │ │ └── layout.tsx │ │ │ └── version │ │ │ │ ├── index.tsx │ │ │ │ └── version.tsx │ │ └── application.tsx │ └── @hooks │ │ └── use-title.ts ├── config │ ├── development.ts │ ├── production.ts │ ├── index.ts │ ├── test.ts │ └── config.ts ├── locales │ ├── en │ │ ├── about.json │ │ ├── home.json │ │ ├── index.ts │ │ └── common.json │ ├── fr │ │ ├── about.json │ │ ├── home.json │ │ ├── index.ts │ │ └── common.json │ └── index.ts ├── test-utils.tsx ├── react-app-env.d.ts ├── initializers │ ├── apollo │ │ ├── index.ts │ │ └── apollo.ts │ ├── i18next │ │ ├── index.ts │ │ └── i18next.ts │ ├── sentry │ │ ├── index.ts │ │ └── sentry.ts │ └── index.ts ├── setupTests.ts ├── styles │ └── global.css └── index.tsx ├── .tool-versions ├── .dockerignore ├── .prettierrc ├── .prettierignore ├── typings ├── omit.d.ts ├── styles.d.ts ├── graphql-tag.macro.d.ts ├── images.d.ts └── emotion.d.ts ├── public ├── favicon.ico ├── manifest.json └── index.html ├── scripts ├── docker-entrypoint.sh ├── project-renamer.sh └── ci-check.sh ├── .stylelintrc.json ├── static.json ├── docker-compose.yml ├── Dockerfile ├── .stylelintrc-components.json ├── .gitignore ├── .github └── pull_request_template.md ├── CHANGELOG.md ├── .env.development ├── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE.md ├── package.json ├── CODE_OF_CONDUCT.md ├── tslint.json ├── README.md ├── boilerplate-setup.sh ├── Makefile ├── BOILERPLATE_README.md └── BOILERPLATE_README.fr.md /__mocks__/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/@contexts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 12.14.1 2 | -------------------------------------------------------------------------------- /src/config/development.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/config/production.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './config'; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env* 2 | .git 3 | node_modules 4 | build 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | bracketSpacing: false 3 | -------------------------------------------------------------------------------- /src/locales/en/about.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "About" 3 | } 4 | -------------------------------------------------------------------------------- /src/locales/fr/about.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "À propos" 3 | } 4 | -------------------------------------------------------------------------------- /src/test-utils.tsx: -------------------------------------------------------------------------------- 1 | export * from 'react-testing-library'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ./src/configurations/locales 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/ui/home/home.module.css: -------------------------------------------------------------------------------- 1 | .example { 2 | margin: 20px 0; 3 | color: hotpink; 4 | } 5 | -------------------------------------------------------------------------------- /typings/omit.d.ts: -------------------------------------------------------------------------------- 1 | declare type Omit = Pick>; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirego/react-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/initializers/apollo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apollo'; 2 | export {default} from './apollo'; 3 | -------------------------------------------------------------------------------- /src/initializers/i18next/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18next'; 2 | export {default} from './i18next'; 3 | -------------------------------------------------------------------------------- /src/initializers/sentry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sentry'; 2 | export {default} from './sentry'; 3 | -------------------------------------------------------------------------------- /src/ui/@components/alert/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert'; 2 | export {default} from './alert'; 3 | -------------------------------------------------------------------------------- /scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | npm run build 5 | 6 | serve -l 3000 -s build 7 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import fr from './fr'; 3 | 4 | export default {en, fr}; 5 | -------------------------------------------------------------------------------- /src/ui/@components/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spinner'; 2 | export {default} from './spinner'; 3 | -------------------------------------------------------------------------------- /src/ui/about/index.ts: -------------------------------------------------------------------------------- 1 | import {lazy} from 'react'; 2 | 3 | export default lazy(() => import('./about')); 4 | -------------------------------------------------------------------------------- /src/ui/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application'; 2 | export {default} from './application'; 3 | -------------------------------------------------------------------------------- /src/ui/home/index.ts: -------------------------------------------------------------------------------- 1 | import {lazy} from 'react'; 2 | 3 | export default lazy(() => import('./home')); 4 | -------------------------------------------------------------------------------- /typings/styles.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.scss'; 3 | declare module '*.sass'; 4 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'jest-dom/extend-expect'; 2 | import 'react-testing-library/cleanup-after-each'; 3 | -------------------------------------------------------------------------------- /src/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Home", 3 | "cssModuleExample": "This is a css module example" 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout'; 2 | export {default} from './layout'; 3 | -------------------------------------------------------------------------------- /src/ui/application/@components/version/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './version'; 2 | export {default} from './version'; 3 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | export {default} from './header'; 3 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/logo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logo'; 2 | export {default} from './logo'; 3 | -------------------------------------------------------------------------------- /src/locales/fr/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Accueil", 3 | "cssModuleExample": "Ceci est un exemple de module CSS" 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/@components/error-boundary/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-boundary'; 2 | export {default} from './error-boundary'; 3 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './navigation'; 2 | export {default} from './navigation'; 3 | -------------------------------------------------------------------------------- /typings/graphql-tag.macro.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-tag.macro' { 2 | import {gql} from 'apollo-boost'; 3 | export default gql; 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/navigation/language-switcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './language-switcher'; 2 | export {default} from './language-switcher'; 3 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": [ 4 | "stylelint-config-mirego" 5 | ], 6 | "rules": { 7 | "mirego/prefer-sass-rgba-function": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import about from './about.json'; 2 | import common from './common.json'; 3 | import home from './home.json'; 4 | 5 | export default {about, common, home}; 6 | -------------------------------------------------------------------------------- /src/locales/fr/index.ts: -------------------------------------------------------------------------------- 1 | import about from './about.json'; 2 | import common from './common.json'; 3 | import home from './home.json'; 4 | 5 | export default {about, common, home}; 6 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import '~simple-css-reset/reset.css'; 2 | 3 | body { 4 | font-family: sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/@hooks/use-title.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | const useTitle = (title: string) => { 4 | useEffect(() => { 5 | window.document.title = title; 6 | }, [title]); 7 | }; 8 | 9 | export default useTitle; 10 | -------------------------------------------------------------------------------- /typings/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const fileName: string; 3 | export const ReactComponent: React.ComponentType; 4 | export default fileName; 5 | } 6 | 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | -------------------------------------------------------------------------------- /src/config/test.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | apollo: { 3 | apiUrl: 'http://test-api-url' 4 | }, 5 | 6 | sentry: { 7 | dsn: '', 8 | environment: 'test' 9 | }, 10 | 11 | versionNumber: { 12 | show: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/initializers/apollo/apollo.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-boost'; 2 | import config from 'react-boilerplate/config'; 3 | 4 | const initializeApollo = () => { 5 | return new ApolloClient({uri: config.apollo.apiUrl}); 6 | }; 7 | 8 | export default initializeApollo; 9 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic_auth": true, 3 | "https_only": true, 4 | "root": "build/", 5 | "routes": { 6 | "/**": "index.html" 7 | }, 8 | "headers": { 9 | "/**": { 10 | "Strict-Transport-Security": "max-age=7776000" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | application: 4 | image: react-boilerplate 5 | container_name: react-boilerplate 6 | env_file: .env.development 7 | volumes: 8 | - '.:/usr/src/app' 9 | - '/usr/src/app/node_modules' 10 | ports: 11 | - '3000:3000' 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1-alpine3.9 2 | 3 | WORKDIR /opt/app 4 | 5 | RUN npm install -g serve 6 | 7 | COPY package.json package-lock.json ./ 8 | 9 | COPY scripts ./scripts 10 | 11 | RUN npm ci --no-audit --no-color --unsafe-perm 12 | 13 | COPY . . 14 | 15 | ENTRYPOINT ["./scripts/docker-entrypoint.sh"] 16 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/logo/logo.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import {ReactComponent as OriginalLogo} from './logo.svg'; 3 | 4 | const Logo = styled(OriginalLogo)` 5 | flex: 0 0 100px; 6 | width: 100px; 7 | height: 100px; 8 | `; 9 | 10 | export default Logo; 11 | -------------------------------------------------------------------------------- /src/initializers/index.ts: -------------------------------------------------------------------------------- 1 | import createApolloClient from './apollo'; 2 | import createI18next from './i18next'; 3 | 4 | const initialize = () => { 5 | const i18next = createI18next(); 6 | const apolloClient = createApolloClient(); 7 | 8 | return {apolloClient, i18next}; 9 | }; 10 | 11 | export default initialize; 12 | -------------------------------------------------------------------------------- /src/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "fatalError": "Une erreur inattendue est survenue. Veuillez réessayer plus tard.", 3 | "header": { 4 | "title": "React Boilerplate", 5 | "links": { 6 | "home": "Accueil", 7 | "about": "À propos" 8 | } 9 | }, 10 | "languages": { 11 | "english": "Anglais", 12 | "french": "Français" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.stylelintrc-components.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "processors": ["stylelint-processor-styled-components"], 4 | "extends": [ 5 | "stylelint-config-styled-components", 6 | "stylelint-config-mirego" 7 | ], 8 | "rules": { 9 | "declaration-colon-newline-after": null, 10 | "mirego/prefer-sass-rgba-function": null, 11 | "property-no-unknown": null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "fatalError": "An unexpected error has happened. Please try again later.", 3 | "title": "React Boilerplate", 4 | "header": { 5 | "links": { 6 | "home": "Home", 7 | "about": "About" 8 | } 9 | }, 10 | "languages": { 11 | "english": "English", 12 | "french": "French" 13 | }, 14 | "loading": "Loading..." 15 | } 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /typings/emotion.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@emotion/styled/macro' { 2 | export * from '@emotion/styled'; 3 | export {default} from '@emotion/styled'; 4 | } 5 | 6 | declare module '@emotion/css/macro' { 7 | export * from '@emotion/css'; 8 | export {default} from '@emotion/css'; 9 | } 10 | 11 | declare module 'emotion/macro' { 12 | export * from 'emotion'; 13 | export {default} from 'emotion'; 14 | } 15 | -------------------------------------------------------------------------------- /src/initializers/sentry/sentry.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | import config from 'react-boilerplate/config'; 3 | 4 | const initializeSentry = () => { 5 | Sentry.init({ 6 | dsn: config.sentry.dsn, 7 | enabled: Boolean(config.sentry.dsn), 8 | environment: config.sentry.environment, 9 | release: config.application.version 10 | }); 11 | }; 12 | 13 | export default initializeSentry; 14 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import React, {FunctionComponent} from 'react'; 3 | import Header from './header'; 4 | 5 | const Content = styled.div` 6 | padding: 25px; 7 | `; 8 | 9 | const Layout: FunctionComponent = ({children}) => ( 10 | <> 11 |
12 | {children} 13 | 14 | ); 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📖 Description 2 | 3 | 4 | 5 | ## 📝 Notes 6 | 7 | 8 | 9 | ## 📓 References 10 | 11 | 12 | 13 | ## 🦀 Dispatch 14 | 15 | - `#dispatch/react` 16 | -------------------------------------------------------------------------------- /src/ui/about/about.tsx: -------------------------------------------------------------------------------- 1 | import React, {FunctionComponent} from 'react'; 2 | import useTitle from 'react-boilerplate/ui/@hooks/use-title'; 3 | import {useTranslation} from 'react-i18next'; 4 | 5 | const About: FunctionComponent = () => { 6 | const {t} = useTranslation(['common', 'about']); 7 | useTitle(`${t('title')} | ${t('about:title')}`); 8 | 9 | return

{t('about:title')}

; 10 | }; 11 | 12 | export default About; 13 | -------------------------------------------------------------------------------- /src/ui/home/home.tsx: -------------------------------------------------------------------------------- 1 | import React, {FunctionComponent} from 'react'; 2 | import useTitle from 'react-boilerplate/ui/@hooks/use-title'; 3 | import {useTranslation} from 'react-i18next'; 4 | import styles from './home.module.css'; 5 | 6 | const Home: FunctionComponent = () => { 7 | const {t} = useTranslation(['common', 'home']); 8 | 9 | useTitle(t('title')); 10 | 11 | return ( 12 | <> 13 |

{t('home:title')}

14 | 15 |

{t('home:cssModuleExample')}

16 | 17 | ); 18 | }; 19 | 20 | export default Home; 21 | -------------------------------------------------------------------------------- /__mocks__/react-i18next.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const t = key => key; 4 | const i18n = {language: 'en'}; 5 | 6 | const hookValue = [t, i18n]; 7 | hookValue.t = t; 8 | hookValue.i18n = i18n; 9 | export const useTranslation = () => hookValue; 10 | 11 | export const initReactI18next = { 12 | type: '', 13 | init() { 14 | return; 15 | } 16 | }; 17 | 18 | export const Translation = ({children}) => <>{children(t, {i18n})}; 19 | 20 | export const withTranslation = () => Component => props => ( 21 | 22 | ); 23 | 24 | export const I18nextProvider = ({children}) => <>{children}; 25 | -------------------------------------------------------------------------------- /src/ui/application/@components/version/version.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import React from 'react'; 3 | import config from 'react-boilerplate/config'; 4 | 5 | const VersionNumber = styled.div` 6 | position: fixed; 7 | z-index: 9999; 8 | bottom: 5px; 9 | left: 5px; 10 | padding: 4px; 11 | border-radius: 2px; 12 | background: #61dafb; 13 | font-size: 10px; 14 | line-height: 1; 15 | color: #fff; 16 | `; 17 | 18 | const Version = () => { 19 | if (!config.versionNumber.show) return null; 20 | 21 | return {config.application.version}; 22 | }; 23 | 24 | export default Version; 25 | -------------------------------------------------------------------------------- /src/ui/@components/alert/alert.tsx: -------------------------------------------------------------------------------- 1 | import css from '@emotion/css/macro'; 2 | import styled from '@emotion/styled/macro'; 3 | 4 | type AlertType = 'danger' | 'info' | 'success' | 'warning'; 5 | 6 | interface Props { 7 | type: AlertType; 8 | } 9 | 10 | const HUES = { 11 | danger: 0, 12 | info: 211, 13 | success: 105, 14 | warning: 45 15 | }; 16 | 17 | const Alert = styled.div` 18 | padding: 10px 16px; 19 | margin-bottom: 24px; 20 | ${({type}: Props) => css` 21 | border-left: 4px solid hsl(${HUES[type]}, 100%, 45%); 22 | background: hsl(${HUES[type]}, 100%, 93%); 23 | color: hsl(${HUES[type]}, 100%, 40%); 24 | `} 25 | `; 26 | 27 | export default Alert; 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | Since it is a boilerplate project, there are technically no official (versioned) _releases_. Therefore, the `master` branch should always be stable and usable. 8 | 9 | ## 2020-03-18 10 | 11 | ### Fixed 12 | 13 | - Makefile (`Makefile`) output of the different targets when using numbers 14 | 15 | ## 2020-01-22 16 | 17 | ### Updated 18 | 19 | - Improve Docker-related environment variables in Makefile (#12) 20 | 21 | ## 2019-10-18 22 | 23 | ### Added 24 | 25 | - Project changelog (`CHANGELOG.md`) 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-boilerplate/styles/global.css'; 2 | 3 | import React from 'react'; 4 | import {ApolloProvider} from 'react-apollo'; 5 | import initialize from 'react-boilerplate/initializers'; 6 | import Application from 'react-boilerplate/ui/application'; 7 | import ReactDOM from 'react-dom'; 8 | import {I18nextProvider} from 'react-i18next'; 9 | import {BrowserRouter as Router} from 'react-router-dom'; 10 | 11 | const {apolloClient, i18next} = initialize(); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | -------------------------------------------------------------------------------- /scripts/project-renamer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -z "$1" ]] ; then 4 | echo 'You must specify your project name in kebab-case as first argument.' 5 | exit 0 6 | fi 7 | 8 | # Used as the root module of your app 9 | projectNameBefore="react-boilerplate" 10 | projectNameAfter=${1} 11 | 12 | # Source code files 13 | fileTypes=(.ts .tsx .js .jsx) 14 | for type in ${fileTypes[@]};do find ./src -name "*${type}" -exec sed -i '' -e "s/$projectNameBefore/$projectNameAfter/g" '{}' '+' 2>&1 >/dev/null; done 15 | 16 | # Config files 17 | configFiles=(package.json package-lock.json tsconfig.json) 18 | for file in ${configFiles[@]};do find ./ -maxdepth 1 -name "${file}" -exec sed -i '' -e "s/$projectNameBefore/$projectNameAfter/g" '{}' '+' 2>&1 >/dev/null; done 19 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/header.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import React, {FunctionComponent} from 'react'; 3 | import {useTranslation} from 'react-i18next'; 4 | import Logo from './logo'; 5 | import Navigation from './navigation'; 6 | 7 | const Container = styled.header` 8 | display: flex; 9 | align-items: center; 10 | height: 100px; 11 | background-color: #282c34; 12 | `; 13 | 14 | const Title = styled.span` 15 | flex: 0 0 auto; 16 | font-size: 38px; 17 | font-weight: bold; 18 | color: #fff; 19 | `; 20 | 21 | const Header: FunctionComponent = () => { 22 | const {t} = useTranslation(); 23 | 24 | return ( 25 | 26 | 27 | {t('title')} 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /src/ui/@components/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import {keyframes} from 'emotion/macro'; 3 | import React, {FunctionComponent} from 'react'; 4 | 5 | interface Props { 6 | size: number; 7 | } 8 | 9 | const rotate = keyframes` 10 | from { transform: rotate(0); } 11 | to { transform: rotate(360deg); } 12 | `; 13 | 14 | const Container = styled.div` 15 | display: flex; 16 | justify-content: center; 17 | width: 100%; 18 | padding: 20px; 19 | `; 20 | 21 | const Spinning = styled.div` 22 | font-size: ${({size}: Props) => size}px; 23 | line-height: 1; 24 | animation: ${rotate} 1s linear infinite; 25 | `; 26 | 27 | const Spinner: FunctionComponent = ({size}) => { 28 | return ( 29 | 30 | 🌀 31 | 32 | ); 33 | }; 34 | 35 | export default Spinner; 36 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | const parseEnvBoolean = (value: string, fallback: boolean = false) => { 2 | let bool = fallback; 3 | 4 | try { 5 | bool = Boolean(JSON.parse(value)); 6 | } catch (_error) {} 7 | 8 | return bool; 9 | }; 10 | 11 | import {version} from '../../package.json'; 12 | 13 | // tslint:disable no-non-null-assertion 14 | const config = { 15 | application: { 16 | version 17 | }, 18 | 19 | apollo: { 20 | apiUrl: process.env.REACT_APP_API_URL! 21 | }, 22 | 23 | sentry: { 24 | dsn: process.env.REACT_APP_SENTRY_DSN, 25 | environment: process.env.REACT_APP_SENTRY_ENVIRONMENT_NAME! 26 | }, 27 | 28 | versionNumber: { 29 | show: parseEnvBoolean(process.env.REACT_APP_SHOW_VERSION_NUMBER!) 30 | } 31 | }; 32 | 33 | // tslint:disable-next-line no-var-requires prefer-template 34 | const envConfig: Partial = require('./' + process.env.NODE_ENV); 35 | 36 | export default {...config, ...envConfig}; 37 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/navigation/language-switcher/language-switcher.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import React, {FunctionComponent, useCallback} from 'react'; 3 | import {useTranslation} from 'react-i18next'; 4 | 5 | const SwitcherButton = styled.button` 6 | margin: 15px; 7 | text-decoration: none; 8 | font-size: 18px; 9 | color: #61dafb; 10 | 11 | &:hover { 12 | text-decoration: underline; 13 | color: #fff; 14 | } 15 | `; 16 | 17 | const LanguageSwitcher: FunctionComponent = () => { 18 | const {i18n} = useTranslation(); 19 | const nextLanguage = i18n.language === 'fr' ? 'en' : 'fr'; 20 | const switchLanguage = useCallback(() => { 21 | i18n.changeLanguage(nextLanguage); 22 | }, [i18n.changeLanguage, nextLanguage]); 23 | 24 | return ( 25 | 26 | {nextLanguage} 27 | 28 | ); 29 | }; 30 | 31 | export default LanguageSwitcher; 32 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # This file contains all the environment variables needed (or supported) by the 3 | # application. It is checked into version control so all developers can share 4 | # it and use it as a base to build their own `.env` file. 5 | # 6 | # Current project developers should try to fill the values with the most 7 | # generic information possible for future developers. 8 | # 9 | # Personal values (such as access and secret keys) should *not* be stored in 10 | # this file since they’re not shared among developers. 11 | # ----------------------------------------------------------------------------- 12 | 13 | # Automatic browser launch on server start 14 | BROWSER=false 15 | 16 | # Server bound hostname 17 | HOST=0.0.0.0 18 | 19 | # Server bound port 20 | PORT=3000 21 | 22 | # Server HTTPS mode 23 | HTTPS=false 24 | 25 | # Feature flags 26 | REACT_APP_SHOW_VERSION_NUMBER=true 27 | 28 | # API 29 | REACT_APP_API_URL=http://localhost:4000/graphql 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## A word about the project 4 | 5 | First of all, thank you for your interest in contributing to this project! 6 | 7 | This project is our vision of a well set up project and is the base we use to create all of our React applications at Mirego. We decided to make it public so that others can benefit from our experience and the lessons learned over the years of building several projects with different objectives; all fulfilled by this boilerplate. 8 | 9 | While we accept pull requests and suggestions, it is more of a project that we want to share so that you can build awesome things with it and maybe, base your own boilerplate off of it! 10 | 11 | ## Contributing 12 | 13 | We strongly suggest you open an issue before starting to work on code that you would like to see in this project. This will prevent you, for example, from implementing a feature that we, Mirego, already discussed and decided not to use. 14 | 15 | Bug and typo fixes are always welcomed, of course 🙂 16 | 17 | Thank you! ❤️ 18 | -------------------------------------------------------------------------------- /src/ui/application/application.tsx: -------------------------------------------------------------------------------- 1 | import React, {FunctionComponent, Suspense} from 'react'; 2 | import ErrorBoundary from 'react-boilerplate/ui/@components/error-boundary'; 3 | import Spinner from 'react-boilerplate/ui/@components/spinner'; 4 | import About from 'react-boilerplate/ui/about'; 5 | import Home from 'react-boilerplate/ui/home'; 6 | import {Route, Switch} from 'react-router-dom'; 7 | import Layout from './@components/layout'; 8 | import Version from './@components/version'; 9 | 10 | const Application: FunctionComponent = () => { 11 | return ( 12 | 13 | 14 | 15 | }> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Application; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowUnreachableCode": false, 5 | "allowSyntheticDefaultImports": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "preserve", 8 | "lib": [ 9 | "es7", 10 | "es2017", 11 | "dom", 12 | "esnext.asynciterable" 13 | ], 14 | "moduleResolution": "node", 15 | "module": "esnext", 16 | "noFallthroughCasesInSwitch": true, 17 | "noEmit": true, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "pretty": true, 21 | "rootDir": "src", 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "sourceMap": true, 25 | "strict": true, 26 | "suppressImplicitAnyIndexErrors": true, 27 | "target": "es2018", 28 | "esModuleInterop": true, 29 | "baseUrl": ".", 30 | "paths": { 31 | "react-boilerplate/*": [ 32 | "./src/*" 33 | ] 34 | }, 35 | "isolatedModules": true 36 | }, 37 | "include": [ 38 | "src", 39 | "typings" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/initializers/i18next/i18next.ts: -------------------------------------------------------------------------------- 1 | import I18next from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | import resources from 'react-boilerplate/locales'; 4 | import {initReactI18next} from 'react-i18next'; 5 | 6 | const i18NextConfig: I18next.InitOptions = { 7 | debug: process.env.NODE_ENV === 'development', 8 | defaultNS: 'common', 9 | fallbackLng: 'en', 10 | interpolation: { 11 | escapeValue: false 12 | }, 13 | load: 'languageOnly', 14 | resources, 15 | whitelist: ['fr', 'en'] 16 | }; 17 | 18 | export const LanguageSanitizer = { 19 | type: '3rdParty', 20 | 21 | init(i18n: I18next.i18n) { 22 | const language = i18n.language.split('-').shift(); 23 | 24 | if (language && language !== i18n.language) { 25 | i18n.changeLanguage(language); 26 | } 27 | } 28 | }; 29 | 30 | const initializeI18next = () => { 31 | I18next.use(initReactI18next) 32 | .use(LanguageDetector) 33 | .use(LanguageSanitizer) 34 | .init(i18NextConfig); 35 | 36 | return I18next; 37 | }; 38 | 39 | export default initializeI18next; 40 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/navigation/navigation.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled/macro'; 2 | import React, {FunctionComponent} from 'react'; 3 | import {useTranslation} from 'react-i18next'; 4 | import {Link as OriginalLink} from 'react-router-dom'; 5 | import LanguageSwitcher from './language-switcher'; 6 | 7 | const Container = styled.nav` 8 | display: flex; 9 | flex: 1 1 100%; 10 | justify-content: flex-end; 11 | align-items: center; 12 | padding: 20px; 13 | `; 14 | 15 | const Link = styled(OriginalLink)` 16 | margin: 15px; 17 | text-decoration: none; 18 | font-size: 18px; 19 | color: #61dafb; 20 | 21 | &:hover { 22 | text-decoration: underline; 23 | color: #fff; 24 | } 25 | `; 26 | 27 | const Navigation: FunctionComponent = () => { 28 | const {t} = useTranslation(); 29 | 30 | return ( 31 | 32 | {t('header.links.home')} 33 | {t('header.links.about')} 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Navigation; 40 | -------------------------------------------------------------------------------- /src/ui/@components/error-boundary/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | import React, {Component, ErrorInfo, FunctionComponent} from 'react'; 3 | import Alert from 'react-boilerplate/ui/@components/alert'; 4 | import {useTranslation} from 'react-i18next'; 5 | 6 | interface State { 7 | hasError: boolean; 8 | } 9 | 10 | const ErrorMessage: FunctionComponent = () => { 11 | const {t} = useTranslation(); 12 | 13 | return {t('fatalError')}; 14 | }; 15 | 16 | export class ErrorBoundary extends Component<{}, State> { 17 | static getDerivedStateFromError() { 18 | return {hasError: true}; 19 | } 20 | 21 | state: State = { 22 | hasError: false 23 | }; 24 | 25 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 26 | Sentry.withScope(scope => { 27 | Object.keys(errorInfo).forEach(key => { 28 | scope.setExtra(key, errorInfo[key]); 29 | }); 30 | Sentry.captureException(error); 31 | }); 32 | } 33 | 34 | render() { 35 | if (this.state.hasError) { 36 | return ; 37 | } 38 | 39 | return this.props.children; 40 | } 41 | } 42 | 43 | export default ErrorBoundary; 44 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/navigation/language-switcher/language-switcher.test.tsx: -------------------------------------------------------------------------------- 1 | import I18next from 'i18next'; 2 | import React from 'react'; 3 | import {fireEvent, render} from 'react-boilerplate/test-utils'; 4 | import {I18nextProvider} from 'react-i18next'; 5 | import LanguageSwitcher from './language-switcher'; 6 | 7 | jest.unmock('react-i18next'); 8 | 9 | it('toggles language between french and english', async () => { 10 | await I18next.init({lng: 'fr'}); 11 | 12 | const {getByText} = render( 13 | 14 | 15 | 16 | ); 17 | 18 | // Current language is french so the caption is `en` 19 | let languageSwitcherButton = getByText('en'); 20 | 21 | // Clicking will switch to english 22 | fireEvent.click(languageSwitcherButton); 23 | expect(I18next.language).toBe('en'); 24 | 25 | // Current language is english so the caption is `fr` 26 | languageSwitcherButton = getByText('fr'); 27 | 28 | // Clicking will switch to french 29 | fireEvent.click(languageSwitcherButton); 30 | expect(I18next.language).toBe('fr'); 31 | 32 | // Current language is back to french so the caption is `en` 33 | getByText('en'); 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/ci-check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | error_status=0 4 | 5 | RED='\033[0;31m' 6 | RED_BOLD='\033[1;31m' 7 | GREEN='\033[0;32m' 8 | GREEN_BOLD='\033[1;32m' 9 | YELLOW='\033[0;33m' 10 | NO_COLOR='\033[0m' 11 | 12 | header() { 13 | echo "\n\n${YELLOW}▶ $1${NO_COLOR}" 14 | } 15 | 16 | run() { 17 | eval "${@}" 18 | last_exit_status=${?} 19 | 20 | if [ ${last_exit_status} -ne 0 ]; then 21 | echo "\n${RED}${@}${NO_COLOR}" 22 | echo "${RED}↳ Something went wrong. Program exited with ${last_exit_status} ✘${NO_COLOR}" 23 | error_status=${last_exit_status} 24 | else 25 | echo "\n${GREEN}${@}${NO_COLOR}" 26 | echo "${GREEN}↳ Passed ✔${NO_COLOR}" 27 | fi 28 | } 29 | 30 | header "Lint files…" 31 | run make lint 32 | 33 | header "Check code format…" 34 | run make check-format 35 | 36 | header "Run tests and check test code coverage…" 37 | run make check-code-coverage 38 | 39 | header "Typecheck files…" 40 | run make check-types 41 | 42 | header "Build application…" 43 | run make build-app 44 | 45 | header "Build Docker image…" 46 | run make build 47 | 48 | if [ ${error_status} -ne 0 ]; then 49 | echo "\n\n${YELLOW}▶▶ One of the checks ${RED_BOLD}failed${YELLOW}. Please fix it before committing.${NO_COLOR}" 50 | else 51 | echo "\n\n${YELLOW}▶▶ All checks ${GREEN_BOLD}passed${YELLOW}!${NO_COLOR}" 52 | fi 53 | 54 | exit $error_status 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020, Mirego 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | - Neither the name of the Mirego nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React Boilerplate 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "version": "0.0.1", 4 | "private": true, 5 | "engines": { 6 | "node": "~12.14", 7 | "npm": "~6.13" 8 | }, 9 | "dependencies": { 10 | "@emotion/core": "^10.0.7", 11 | "@emotion/css": "^10.0.7", 12 | "@emotion/styled": "^10.0.7", 13 | "@mirego/react-scripts": "2.1.5", 14 | "@sentry/browser": "^4.6.1", 15 | "apollo-boost": "^0.1.28", 16 | "emotion": "^10.0.7", 17 | "graphql": "^14.1.1", 18 | "graphql-tag.macro": "^2.1.0", 19 | "i18next": "^15.0.4", 20 | "i18next-browser-languagedetector": "^3.0.1", 21 | "react": "^16.8.2", 22 | "react-apollo": "^2.4.1", 23 | "react-dom": "^16.8.2", 24 | "react-i18next": "^10.1.2", 25 | "react-router": "^4.3.1", 26 | "react-router-dom": "^4.3.1", 27 | "simple-css-reset": "^3.0.0" 28 | }, 29 | "scripts": { 30 | "start": "mirego-react-scripts start", 31 | "build": "mirego-react-scripts build", 32 | "test": "TZ=UTC mirego-react-scripts test" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^24.0.6", 39 | "@types/node": "^11.9.4", 40 | "@types/react": "^16.8.3", 41 | "@types/react-dom": "^16.8.2", 42 | "@types/react-router": "^4.4.4", 43 | "@types/react-router-dom": "^4.3.1", 44 | "jest-dom": "^3.1.2", 45 | "prettier": "^1.16.4", 46 | "react-testing-library": "^5.9.0", 47 | "stylelint": "^9.10.1", 48 | "stylelint-config-mirego": "^1.0.4", 49 | "stylelint-config-styled-components": "^0.1.1", 50 | "stylelint-processor-styled-components": "^1.5.2", 51 | "tslint": "^5.12.1", 52 | "tslint-config-prettier": "^1.18.0", 53 | "tslint-react": "^3.6.0", 54 | "typescript": "^3.3.3" 55 | }, 56 | "browserslist": [ 57 | ">0.2%", 58 | "not dead", 59 | "not ie <= 11", 60 | "not op_mini all" 61 | ], 62 | "jest": { 63 | "coverageReporters": [ 64 | "text-summary" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Contact: info@mirego.com 4 | 5 | ## Why have a Code of Conduct? 6 | 7 | As contributors and maintainers of this project, we are committed to providing a friendly, safe and welcoming environment for all, regardless of age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. 8 | 9 | The goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about the project effectively, productively, and respectfully, even in face of disagreements. The Code of Conduct also provides a mechanism for resolving conflicts in the community when they arise. 10 | 11 | ## Our Values 12 | 13 | These are the values React Boilerplate developers should aspire to: 14 | 15 | - Be friendly and welcoming 16 | - Be patient 17 | - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) 18 | - Be thoughtful 19 | - Productive communication requires effort. Think about how your words will be interpreted. 20 | - Remember that sometimes it is best to refrain entirely from commenting. 21 | - Be respectful 22 | - In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. 23 | - Avoid destructive behavior 24 | - Derailing: stay on topic; if you want to talk about something else, start a new conversation. 25 | - Unconstructive criticism: don't merely decry the current state of affairs; offer (or at least solicit) suggestions as to how things may be improved. 26 | - Snarking (pithy, unproductive, sniping comments). 27 | 28 | The following actions are explicitly forbidden: 29 | 30 | - Insulting, demeaning, hateful, or threatening remarks. 31 | - Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. 32 | - Bullying or systematic harassment. 33 | - Unwelcome sexual advances. 34 | - Incitement to any of these. 35 | 36 | ## Acknowledgements 37 | 38 | This document was based on the Code of Conduct from the Elixir project. 39 | -------------------------------------------------------------------------------- /src/ui/application/@components/layout/header/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier", "tslint-react"], 3 | "rules": { 4 | "adjacent-overload-signatures": true, 5 | "arrow-return-shorthand": true, 6 | "ban-comma-operator": true, 7 | "class-name": true, 8 | "comment-format": [true, "check-space"], 9 | "curly": [true, "ignore-same-line"], 10 | "encoding": true, 11 | "interface-name": false, 12 | "jsx-boolean-value": [true, "never"], 13 | "jsx-no-bind": false, 14 | "jsx-no-lambda": false, 15 | "jsx-no-multiline-js": false, 16 | "jsx-use-translation-function": [true, "allow-punctuation"], 17 | "jsx-self-close": false, 18 | "jsx-curly-spacing": false, 19 | "jsx-alignment": false, 20 | "max-classes-per-file": false, 21 | "member-access": [true, "no-public"], 22 | "newline-before-return": true, 23 | "no-arg": true, 24 | "no-conditional-assignment": true, 25 | "no-console": true, 26 | "no-consecutive-blank-lines": true, 27 | "no-debugger": true, 28 | "no-duplicate-imports": true, 29 | "no-duplicate-super": true, 30 | "no-duplicate-switch-case": true, 31 | "no-duplicate-variable": [true, "check-parameters"], 32 | "no-empty-interface": false, 33 | "no-empty": [true, "allow-empty-catch"], 34 | "no-eval": true, 35 | "no-implicit-dependencies": false, 36 | "no-non-null-assertion": true, 37 | "no-parameter-reassignment": true, 38 | "no-return-await": true, 39 | "no-shadowed-variable": true, 40 | "no-string-literal": true, 41 | "no-string-throw": true, 42 | "no-submodule-imports": false, 43 | "no-switch-case-fall-through": true, 44 | "no-this-assignment": [true, {"allow-destructuring": true}], 45 | "no-unsafe-finally": true, 46 | "no-unused-expression": [true, "allow-fast-null-checks"], 47 | "no-var-keyword": true, 48 | "number-literal-format": true, 49 | "object-literal-key-quotes": [true, "as-needed"], 50 | "one-variable-per-declaration": [true, "ignore-for-loop"], 51 | "prefer-conditional-expression": true, 52 | "prefer-const": true, 53 | "prefer-for-of": true, 54 | "prefer-object-spread": true, 55 | "prefer-template": true, 56 | "radix": true, 57 | "triple-equals": [true, "allow-undefined-check", "allow-null-check"], 58 | "typedef-whitespace": [ 59 | true, 60 | { 61 | "call-signature": "nospace", 62 | "index-signature": "nospace", 63 | "parameter": "nospace", 64 | "property-declaration": "nospace", 65 | "variable-declaration": "nospace" 66 | }, 67 | { 68 | "call-signature": "onespace", 69 | "index-signature": "onespace", 70 | "parameter": "onespace", 71 | "property-declaration": "onespace", 72 | "variable-declaration": "onespace" 73 | } 74 | ], 75 | "unified-signatures": true, 76 | "use-isnan": true, 77 | "variable-name": [ 78 | true, 79 | "ban-keywords", 80 | "check-format", 81 | "allow-leading-underscore", 82 | "allow-pascal-case" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |


This repository is the stable base upon which we build our React projects at Mirego.
We want to share it with the world so you can build awesome React applications too.

4 |
5 | 6 | ## Introduction 7 | 8 | To learn more about _why_ we created and maintain this boilerplate project, read our [blog post](https://shift.mirego.com/en/boilerplate-projects). 9 | 10 | ## Content 11 | 12 | This boilerplate comes with batteries included, you’ll find: 13 | 14 | - Tests with [jest](https://jestjs.io), with coverage 15 | - Linting with [tslint](https://palantir.github.io/tslint) and [stylelint](https://stylelint.io) 16 | - Formatting with [Prettier](https://prettier.io) 17 | - A [GraphQL](https://graphql.org) setup powered by [Apollo](https://www.apollographql.com) 18 | - Translations powered by [i18next](https://www.i18next.com) 19 | - [TypeScript](https://www.typescriptlang.org) 20 | - Styled components with [emotions](https://emotion.sh) 21 | - Routing with [react-router](https://reacttraining.com/react-router/) 22 | - A clean and useful `README.md` template (in both [english](./BOILERPLATE_README.md) and [french](./BOILERPLATE_README.fr.md)) 23 | 24 | ## Usage 25 | 26 | ### With GitHub template 27 | 28 | 1. Click on the [**Use this template**](https://github.com/mirego/react-boilerplate/generate) button to create a new repository 29 | 2. Clone your newly created project (`git clone https://github.com/you/repo.git`) 30 | 3. Run the boilerplate setup script (`./boilerplate-setup.sh YourProjectName`) 31 | 4. Commit the changes (`git commit -a -m "Rename react-boilerplate parts"`) 32 | 33 | ### Without GitHub template 34 | 35 | 1. Clone this project (`git clone https://github.com/mirego/react-boilerplate.git`) 36 | 2. Delete the internal Git directory (`rm -rf .git`) 37 | 3. Run the boilerplate setup script (`./boilerplate-setup.sh YourProjectName`) 38 | 4. Create a new Git repository (`git init`) 39 | 5. Create the initial Git commit (`git commit -a -m "Initial commit"`) 40 | 41 | ## License 42 | 43 | React Boilerplate is © 2018-2020 [Mirego](https://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/react-boilerplate/blob/master/LICENSE.md) file. 44 | 45 | The science logo is based on [this lovely icon by Igé Maulana](https://thenounproject.com/term/science/2089589), from The Noun Project. Used under a [Creative Commons BY 3.0](http://creativecommons.org/licenses/by/3.0/) license. 46 | 47 | ## About Mirego 48 | 49 | [Mirego](https://www.mirego.com) is a team of passionate people who believe that work is a place where you can innovate and have fun. We’re a team of [talented people](https://life.mirego.com) who imagine and build beautiful Web and mobile applications. We come together to share ideas and [change the world](http://www.mirego.org). 50 | 51 | We also [love open-source software](https://open.mirego.com) and we try to give back to the community as much as we can. 52 | -------------------------------------------------------------------------------- /boilerplate-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # ----------------------------------------------------------------------------- 4 | # Configuration 5 | # ----------------------------------------------------------------------------- 6 | 7 | pascalCaseBefore="ReactBoilerplate" 8 | snakeCaseBefore="react_boilerplate" 9 | kebabCaseBefore="react-boilerplate" 10 | 11 | # The identifiers above will be replaced in the content of the files found below 12 | content=$(find . -type f \( \ 13 | -name "*.sh" -or \ 14 | -name "*.json" -or \ 15 | -name "*.js" -or \ 16 | -name "*.jsx" -or \ 17 | -name "*.ts" -or \ 18 | -name "*.tsx" -or \ 19 | -name "*.yml" -or \ 20 | -name "*.md" -or \ 21 | -name "Dockerfile" -or \ 22 | -name "Makefile" \ 23 | \) \ 24 | -and ! -path "./boilerplate-setup.sh" \ 25 | -and ! -path "./assets/node_modules/*" \ 26 | ) 27 | 28 | # The identifiers above will be replaced in the path of the files and directories found here 29 | paths=$(find . -depth 2 \( \ 30 | -path "…" \ 31 | \)) 32 | 33 | # ----------------------------------------------------------------------------- 34 | # Validation 35 | # ----------------------------------------------------------------------------- 36 | 37 | if [[ -z "$1" ]] ; then 38 | echo 'You must specify your project name in PascalCase as first argument.' 39 | exit 0 40 | fi 41 | 42 | pascalCaseAfter=$1 43 | snakeCaseAfter=$(echo $pascalCaseAfter | /usr/bin/sed 's/\(.\)\([A-Z]\)/\1_\2/g' | tr '[:upper:]' '[:lower:]') 44 | kebabCaseAfter=$(echo $snakeCaseAfter | tr '_' '-') 45 | 46 | # ----------------------------------------------------------------------------- 47 | # Helper functions 48 | # ----------------------------------------------------------------------------- 49 | 50 | header() { 51 | echo "\033[0;33m▶ $1\033[0m" 52 | } 53 | 54 | success() { 55 | echo "\033[0;32m▶ $1\033[0m" 56 | } 57 | 58 | run() { 59 | echo ${@} 60 | eval "${@}" 61 | } 62 | 63 | # ----------------------------------------------------------------------------- 64 | # Execution 65 | # ----------------------------------------------------------------------------- 66 | 67 | header "Configuration" 68 | echo "${pascalCaseBefore} → ${pascalCaseAfter}" 69 | echo "${snakeCaseBefore} → ${snakeCaseAfter}" 70 | echo "${kebabCaseBefore} → ${kebabCaseAfter}" 71 | echo "" 72 | 73 | header "Replacing boilerplate identifiers in content" 74 | for file in $content; do 75 | run /usr/bin/sed -i "''" "s/$snakeCaseBefore/$snakeCaseAfter/g" $file 76 | run /usr/bin/sed -i "''" "s/$kebabCaseBefore/$kebabCaseAfter/g" $file 77 | run /usr/bin/sed -i "''" "s/$pascalCaseBefore/$pascalCaseAfter/g" $file 78 | done 79 | success "Done!\n" 80 | 81 | header "Replacing boilerplate identifiers in file and directory paths" 82 | for path in $paths; do 83 | run mv $path $(echo $path | /usr/bin/sed "s/$snakeCaseBefore/$snakeCaseAfter/g" | /usr/bin/sed "s/$kebabCaseBefore/$kebabCaseAfter/g" | /usr/bin/sed "s/$pascalCaseBefore/$pascalCaseAfter/g") 84 | done 85 | success "Done!\n" 86 | 87 | header "Importing project README.md and README.fr.md" 88 | run "rm -fr README.md && mv BOILERPLATE_README.md README.md && mv BOILERPLATE_README.fr.md README.fr.md" 89 | success "Done!\n" 90 | 91 | header "Removing boilerplate license → https://choosealicense.com" 92 | run rm -fr LICENSE.md 93 | success "Done!\n" 94 | 95 | header "Removing boilerplate changelog" 96 | run rm -fr CHANGELOG.md 97 | success "Done!\n" 98 | 99 | header "Removing boilerplate code of conduct and contribution information → https://help.github.com/articles/setting-guidelines-for-repository-contributors/" 100 | run rm -fr CODE_OF_CONDUCT.md CONTRIBUTING.md 101 | success "Done!\n" 102 | 103 | header "Removing boilerplate setup script" 104 | run rm -fr boilerplate-setup.sh 105 | success "Done!\n" 106 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Build configuration 2 | # ------------------- 3 | 4 | APP_NAME = `grep -m1 name package.json | awk -F: '{ print $$2 }' | sed 's/[ ",]//g'` 5 | APP_VERSION = `grep -m1 version package.json | awk -F: '{ print $$2 }' | sed 's/[ ",]//g'` 6 | GIT_REVISION = `git rev-parse HEAD` 7 | DOCKER_IMAGE_TAG ?= $(APP_VERSION) 8 | DOCKER_REGISTRY ?= 9 | DOCKER_LOCAL_IMAGE = $(APP_NAME):$(DOCKER_IMAGE_TAG) 10 | DOCKER_REMOTE_IMAGE = $(DOCKER_REGISTRY)/$(DOCKER_LOCAL_IMAGE) 11 | 12 | # Linter and formatter configuration 13 | # ---------------------------------- 14 | 15 | PRETTIER_FILES_PATTERN = '{src,typings,__mocks__,scripts}/**/*.{js,ts,tsx}' '**/*.md' 16 | SCRIPTS_PATTERN = '{src,typings,__mocks__}/**/*.{js,ts,tsx}' 17 | STYLES_PATTERN = 'src/**/*.css' 18 | 19 | # Introspection targets 20 | # --------------------- 21 | 22 | .PHONY: help 23 | help: header targets 24 | 25 | .PHONY: header 26 | header: 27 | @echo "\033[34mEnvironment\033[0m" 28 | @echo "\033[34m---------------------------------------------------------------\033[0m" 29 | @printf "\033[33m%-23s\033[0m" "APP_NAME" 30 | @printf "\033[35m%s\033[0m" $(APP_NAME) 31 | @echo "" 32 | @printf "\033[33m%-23s\033[0m" "APP_VERSION" 33 | @printf "\033[35m%s\033[0m" $(APP_VERSION) 34 | @echo "" 35 | @printf "\033[33m%-23s\033[0m" "GIT_REVISION" 36 | @printf "\033[35m%s\033[0m" $(GIT_REVISION) 37 | @echo "" 38 | @printf "\033[33m%-23s\033[0m" "DOCKER_IMAGE_TAG" 39 | @printf "\033[35m%s\033[0m" $(DOCKER_IMAGE_TAG) 40 | @echo "" 41 | @printf "\033[33m%-23s\033[0m" "DOCKER_REGISTRY" 42 | @printf "\033[35m%s\033[0m" $(DOCKER_REGISTRY) 43 | @echo "" 44 | @printf "\033[33m%-23s\033[0m" "DOCKER_LOCAL_IMAGE" 45 | @printf "\033[35m%s\033[0m" $(DOCKER_LOCAL_IMAGE) 46 | @echo "" 47 | @printf "\033[33m%-23s\033[0m" "DOCKER_REMOTE_IMAGE" 48 | @printf "\033[35m%s\033[0m" $(DOCKER_REMOTE_IMAGE) 49 | @echo "\n" 50 | 51 | .PHONY: targets 52 | targets: 53 | @echo "\033[34mTargets\033[0m" 54 | @echo "\033[34m---------------------------------------------------------------\033[0m" 55 | @perl -nle'print $& if m{^[a-zA-Z_-\d]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' 56 | 57 | # Build targets 58 | # ------------- 59 | 60 | .PHONY: build 61 | build: ## Build the Docker image 62 | docker build --build-arg APP_NAME=$(APP_NAME) --build-arg APP_VERSION=$(APP_VERSION) --rm --tag $(DOCKER_LOCAL_IMAGE) . 63 | 64 | .PHONY: push 65 | push: ## Push the Docker image to the registry 66 | docker tag $(DOCKER_LOCAL_IMAGE) $(DOCKER_REMOTE_IMAGE) 67 | docker push $(DOCKER_REMOTE_IMAGE) 68 | 69 | # Development targets 70 | # ------------------- 71 | 72 | .PHONY: dependencies 73 | dependencies: ## Install dependencies required by the application 74 | npm install 75 | 76 | .PHONY: build-app 77 | build-app: ## Build the application 78 | npm run build 79 | 80 | .PHONY: test 81 | test: ## Run the test suite 82 | npm test 83 | 84 | # Check, lint and format targets 85 | # ------------------------------ 86 | 87 | .PHONY: check 88 | check: check-format check-code-coverage check-types ## Run various checks on project files 89 | 90 | .PHONY: check-format 91 | check-format: 92 | npx prettier --check $(PRETTIER_FILES_PATTERN) 93 | 94 | .PHONY: check-code-coverage 95 | check-code-coverage: 96 | npm test -- --coverage 97 | 98 | .PHONY: check-types 99 | check-types: 100 | npx tsc 101 | 102 | .PHONY: format 103 | format: ## Format project files 104 | npx prettier --write $(PRETTIER_FILES_PATTERN) 105 | npx tslint -c tslint.json --fix $(SCRIPTS_PATTERN) 106 | 107 | .PHONY: lint 108 | lint: lint-scripts lint-styles ## Lint project files 109 | 110 | .PHONY: lint-scripts 111 | lint-scripts: 112 | npx tslint -c tslint.json $(SCRIPTS_PATTERN) 113 | 114 | .PHONY: lint-styles 115 | lint-styles: 116 | npx stylelint --config .stylelintrc.json $(STYLES_PATTERN) 117 | npx stylelint --config .stylelintrc-components.json $(SCRIPTS_PATTERN) 118 | -------------------------------------------------------------------------------- /BOILERPLATE_README.md: -------------------------------------------------------------------------------- 1 | # react-boilerplate 2 | 3 | | Section | Description | 4 | | ----------------------------------------------------- | --------------------------------------------------------------- | 5 | | [🎯 Objectives and context](#-objectives-and-context) | Project introduction and context | 6 | | [🚧 Dependencies](#-dependencies) | Technical dependencies and how to install them | 7 | | [🏎 Kickstart](#kickstart) | Details on how to kickstart development on the project | 8 | | [🏗 Code & architecture](#-code--architecture) | Details on the application modules and technical specifications | 9 | | [🔭 Possible improvements](#-possible-improvements) | Possible code refactors, improvements and ideas | 10 | | [🚑 Troubleshooting](#-troubleshooting) | Recurring problems and proven solutions | 11 | | [🚀 Deploy](#-deploy) | Deployment details for various enviroments | 12 | 13 | ## 🎯 Objectives and context 14 | 15 | … 16 | 17 | ### Browser support 18 | 19 | | Browser | OS | Constraint | 20 | | ------- | --- | ---------- | 21 | | … | … | … | 22 | 23 | ## 🚧 Dependencies 24 | 25 | - Node.js 26 | - NPM 27 | 28 | Canonical versions of dependencies are located in `Dockerfile` and `.tool-versions`. 29 | 30 | ## 🏎 Kickstart 31 | 32 | ### Environment variables 33 | 34 | All required environment variables are documented in [`.env.development`](./.env.development). 35 | 36 | When running scripts or `npm` commands, it is important that these variables are present in the environment. You can use `source`, [`nv`](https://github.com/jcouture/nv) or any custom script to achieve this. 37 | 38 | ### Initial setup 39 | 40 | 1. Create `.env.development.local` from empty values in [`.env.development`](./.env.development) 41 | 2. Install NPM dependencies with `make dependencies` 42 | 43 | ### Run the application in development mode 44 | 45 | To start a development server: 46 | 47 | ```bash 48 | $ npm start 49 | ``` 50 | 51 | ### Build the application for production 52 | 53 | To create a production-ready build: 54 | 55 | ```bash 56 | $ make build-app 57 | ``` 58 | 59 | ### Tests 60 | 61 | Tests can be ran with the following script and do not need any environment variables as they should not create side effects (eg. they should not make any network calls, they should not read cookies, etc.) 62 | 63 | ```bash 64 | $ make test 65 | ``` 66 | 67 | ### Code coverage 68 | 69 | Tests can also be ran while calculating test coverage level. 70 | 71 | ```bash 72 | $ make check-code-coverage 73 | ``` 74 | 75 | ### Linting 76 | 77 | Several linting and formatting tools can be ran to ensure coding style consistency: 78 | 79 | - `make lint-scripts` ensures TypeScript code follows our best practices 80 | - `make lint-styles` ensures CSS code follows our best practices 81 | - `make check-format` ensures all code is properly formatted 82 | - `make check-types` ensures types match 83 | - `make format` formats files using Prettier 84 | 85 | ### Continuous integration 86 | 87 | To ensure the project and its code are in a good state, tests and linting tools can be ran all at once: 88 | 89 | ```bash 90 | $ ./scripts/ci-check.sh 91 | ``` 92 | 93 | ## 🏗 Code & architecture 94 | 95 | … 96 | 97 | ## 🔭 Possible improvements 98 | 99 | | Description | Priority | Complexity | Ideas | 100 | | ----------- | -------- | ---------- | ----- | 101 | | … | … | … | … | 102 | 103 | ## 🚑 Troubleshooting 104 | 105 | … 106 | 107 | ## 🚀 Deploy 108 | 109 | ### Versions & branches 110 | 111 | Each deployment is made from a Git tag. The codebase version is managed with [`incr`](https://github.com/jcouture/incr). 112 | 113 | ### Container 114 | 115 | A Docker image running a Node.js server can be created with `make build`, tested with `docker-compose up application` and pushed to a registry with `make push`. 116 | -------------------------------------------------------------------------------- /BOILERPLATE_README.fr.md: -------------------------------------------------------------------------------- 1 | # react-boilerplate 2 | 3 | | Section | Description | 4 | | ------------------------------------------------------- | ------------------------------------------------------------------ | 5 | | [🎯 Objectifs et contexte](#-objectifs-et-contexte) | Introduction et contexte du projet | 6 | | [🚧 Dépendances](#-dépendances) | Dépendances techniques et comment les installer | 7 | | [🏎 Départ rapide](#-départ-rapide) | Détails sur comment démarrer rapidement le développement du projet | 8 | | [🏗 Code et architecture](#-code-et-architecture) | Détails sur les composantes techniques de l’application | 9 | | [🔭 Améliorations possibles](#-améliorations-possibles) | Améliorations, idées et _refactors_ potentiels | 10 | | [🚑 Problèmes et solutions](#-problèmes-et-solutions) | Problèmes récurrents et solutions éprouvées | 11 | | [🚀 Déploiement](#-deploiement) | Détails pour le déploiement dans différents environnements | 12 | 13 | ## 🎯 Objectifs et contexte 14 | 15 | … 16 | 17 | ### Support de navigateurs 18 | 19 | | Navigateur | OS | Contrainte | 20 | | ---------- | --- | ---------- | 21 | | … | … | … | 22 | 23 | ## 🚧 Dépendances 24 | 25 | - Node.js 26 | - NPM 27 | 28 | Les versions canoniques des dépendances sont spécifiées dans les fichiers `Dockerfile` et `.tool-versions`. 29 | 30 | ## 🏎 Départ rapide 31 | 32 | ### Variables d’environnement 33 | 34 | Toutes les variables d’environnement requises sont documentées dans [`.env.dev`](./.env.dev). 35 | 36 | Ces variables doivent être présentes dans l’environnement lorsque des commandes `npm` ou `make` sont exécutées. Plusieurs moyens sont à votre disposition pour ça, mais l’utilisation de [`nv`](https://github.com/jcouture/nv) est recommandée puisqu’elle fonctionne _out of the box_ avec les fichiers `.env.*`. 37 | 38 | ### Mise en place initiale 39 | 40 | 1. Créer `.env.development.local` à partir des valeurs vides de [`.env.development`](./.env.development) 41 | 2. Installer les avec `make dependencies` 42 | 43 | ### Lancer l’application en mode _développement_ 44 | 45 | Pour démarrer un serveur de développement : 46 | 47 | ```bash 48 | $ npm run start 49 | ``` 50 | 51 | ### Bâtir l’application pour la production 52 | 53 | Pour créer une _build_ prête pour la production : 54 | 55 | ```bash 56 | $ npm run build --prod 57 | ``` 58 | 59 | ### Tests 60 | 61 | La suite de tests peut être exécutée à l’aide de la commande suivante et ne nécessite pas de variables d’environnements puisqu’elle ne devrait pas créer d’effets de bord (eg. pas de requêtes _networking_, pas de lecture de cookies, etc.) 62 | 63 | ```bash 64 | $ make test 65 | ``` 66 | 67 | ### Couverture des tests 68 | 69 | La suite de tests peut aussi être exécutée en calculant le niveau de couverture. 70 | 71 | ```bash 72 | $ make check-code-coverage 73 | ``` 74 | 75 | ### _Linting_ et _formatting_ 76 | 77 | Plusieurs outils de _linting_ et de _formatting_ peuvent être exécutés pour s’assurer du respect des bonnes pratiques de code : 78 | 79 | - `make lint-scripts` s’assure que le code TypeScript respecte nos bonnes pratiques 80 | - `make lint-styles` s’assure que le code CSS respecte nos bonnes pratiques 81 | - `make check-format` valide que le code est bien formatté 82 | - `make format` formatte les fichiers en utilisant Prettier 83 | 84 | ### Intégration continue 85 | 86 | Pour s’assurer que le projet et son code sont en bon état, les outils de _linting_ et de tests peuvent être exécutés en une seule commande : 87 | 88 | ```bash 89 | $ ./scripts/ci-check.sh 90 | ``` 91 | 92 | ## 🏗 Code et architecture 93 | 94 | … 95 | 96 | ## 🔭 Améliorations possibles 97 | 98 | | Description | Priorité | Complexité | Idées | 99 | | ----------- | -------- | ---------- | ----- | 100 | | … | … | … | … | 101 | 102 | ## 🚑 Problèmes et solutions 103 | 104 | … 105 | 106 | ## 🚀 Deploiement 107 | 108 | ### Versions et branches 109 | 110 | Chaque déploiement est effectué à partir d’un tag Git. La version du _codebase_ est gérée avec [`incr`](https://github.com/jcouture/incr). 111 | 112 | ### _Container_ 113 | 114 | Un _container_ Docker exposant un serveur Node.js peut être créé avec `make build`, testé avec `docker-compose up application` et poussé dans un registre avec `make push`. 115 | --------------------------------------------------------------------------------