├── .nvmrc
├── screenshots
├── gcom.png
└── gcon.png
├── generators
├── container
│ ├── Loadable.js.hbs
│ ├── saga.js.hbs
│ ├── selectors.js.hbs
│ ├── saga.test.js.hbs
│ ├── test.js.hbs
│ ├── reducer.js.hbs
│ ├── selectors.test.js.hbs
│ ├── reducer.test.js.hbs
│ ├── index.js.hbs
│ ├── existing
│ │ └── index.js
│ └── new
│ │ └── index.js
├── loadable
│ ├── loadable.js.hbs
│ └── index.js
├── component
│ ├── stories.js.hbs
│ ├── index.js.hbs
│ ├── test.js.hbs
│ ├── new
│ │ └── index.js
│ └── existing
│ │ └── index.js
├── webpack
│ └── base
│ │ └── babel
│ │ ├── index.js
│ │ └── babel.js.hbs
├── testUtil
│ ├── index.js
│ └── testUtils.js.hbs
└── index.js
├── .prettierignore
├── testing
└── test-bundler.js
├── generated-files
├── container
│ └── HomeContainer
│ │ ├── Loadable.js
│ │ ├── saga.js
│ │ ├── tests
│ │ ├── selectors.test.js
│ │ ├── saga.test.js
│ │ ├── index.test.js
│ │ └── reducer.test.js
│ │ ├── selectors.js
│ │ ├── reducer.js
│ │ └── index.js
├── loadable
│ └── loadable.js
├── component
│ └── Button
│ │ ├── stories
│ │ └── Button.stories.js
│ │ ├── index.js
│ │ └── tests
│ │ └── index.test.js
└── test-util
│ └── testUtils.js
├── .prettierrc
├── .eslintignore
├── .stylelintrc
├── .gitignore
├── .github
├── pull_request_template.md
└── workflows
│ └── npm-publish.yml
├── .eslintrc.js
├── babel.config.js
├── jest.config.js
├── LICENSE
├── package.json
├── README.md
├── react_floki_github.svg
├── floki.svg
└── bin
└── react-generate.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/dubnium
2 |
--------------------------------------------------------------------------------
/screenshots/gcom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wednesday-solutions/react-floki/HEAD/screenshots/gcom.png
--------------------------------------------------------------------------------
/screenshots/gcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wednesday-solutions/react-floki/HEAD/screenshots/gcon.png
--------------------------------------------------------------------------------
/generators/container/Loadable.js.hbs:
--------------------------------------------------------------------------------
1 | import loadable from '@utils/loadable';
2 |
3 | export default loadable(() => import('./index'));
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | internals/generators/
4 | internals/scripts/
5 | package-lock.json
6 | yarn.lock
7 | package.json
8 |
--------------------------------------------------------------------------------
/testing/test-bundler.js:
--------------------------------------------------------------------------------
1 | // needed for regenerator-runtime
2 | // (ES7 generator support is required by redux-saga)
3 | import '@babel/polyfill';
4 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/Loadable.js:
--------------------------------------------------------------------------------
1 | import loadable from '@utils/loadable';
2 |
3 | export default loadable(() => import('./index'));
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Don't check auto-generated stuff into git
2 | coverage
3 | build
4 | node_modules
5 | stats.json
6 |
7 | # Cruft
8 | .DS_Store
9 | npm-debug.log
10 | .idea
11 | generated-files
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "processors": ["stylelint-processor-styled-components"],
3 | "extends": [
4 | "stylelint-config-recommended",
5 | "stylelint-config-styled-components"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't check auto-generated stuff into git
2 | coverage
3 | build
4 | node_modules
5 | stats.json
6 |
7 | # Cruft
8 | .DS_Store
9 | npm-debug.log
10 | .idea
11 | .vscode/**
12 | app/**
13 |
--------------------------------------------------------------------------------
/generators/loadable/loadable.js.hbs:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from 'react'
2 |
3 | const loadable = (importFunc, { fallback = null } = { fallback: null }) => {
4 | const LazyComponent = lazy(importFunc)
5 |
6 | return props => (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default loadable
14 |
--------------------------------------------------------------------------------
/generated-files/loadable/loadable.js:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from 'react';
2 |
3 | const loadable = (importFunc, { fallback = null } = { fallback: null }) => {
4 | const LazyComponent = lazy(importFunc);
5 |
6 | return props => (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default loadable;
14 |
--------------------------------------------------------------------------------
/generators/component/stories.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Stories for {{ properCase name }}
4 | *
5 | * @see https://github.com/storybookjs/storybook
6 | *
7 | */
8 |
9 | import React from 'react';
10 | import { storiesOf } from '@storybook/react';
11 | import { {{ properCase name }} } from '../index';
12 |
13 |
14 | storiesOf('{{properCase name}}').add('simple', () => <{{properCase name}} />);
15 |
--------------------------------------------------------------------------------
/generated-files/component/Button/stories/Button.stories.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Stories for Button
4 | *
5 | * @see https://github.com/storybookjs/storybook
6 | *
7 | */
8 |
9 | import React from 'react';
10 | import { storiesOf } from '@storybook/react';
11 | import { text } from '@storybook/addon-knobs';
12 | import Button from '../index';
13 |
14 | storiesOf('Button').add('simple', () => );
15 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### Ticket Link
2 | ---------------------------------------------------
3 |
4 |
5 | ### Related Links
6 | ---------------------------------------------------
7 |
8 |
9 | ### Description
10 | ---------------------------------------------------
11 |
12 |
13 | ### Steps to Reproduce / Test
14 | ---------------------------------------------------
15 |
16 |
17 | ### GIF's
18 | ---------------------------------------------------
19 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/saga.js:
--------------------------------------------------------------------------------
1 | import { takeLatest } from 'redux-saga/effects';
2 | import { homeContainerTypes } from './reducer';
3 | // Individual exports for testing
4 | const { DEFAULT_ACTION } = homeContainerTypes;
5 |
6 | export function* defaultFunction(/* action */) {
7 | // console.log('Do something here')
8 | }
9 |
10 | export default function* homeContainerSaga() {
11 | yield takeLatest(DEFAULT_ACTION, defaultFunction);
12 | }
13 |
--------------------------------------------------------------------------------
/generators/container/saga.js.hbs:
--------------------------------------------------------------------------------
1 | import { takeLatest } from 'redux-saga/effects';
2 | import { {{ camelCase name}}Types } from './reducer';
3 |
4 | // Individual exports for testing
5 | const { DEFAULT_ACTION } = {{ camelCase name }}Types;
6 |
7 | export function *defaultFunction (/* action */) {
8 | // console.log('Do something here')
9 | };
10 |
11 | export default function* {{ camelCase name }}Saga() {
12 | yield takeLatest(DEFAULT_ACTION, defaultFunction);
13 | };
14 |
--------------------------------------------------------------------------------
/generated-files/component/Button/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Button
4 | *
5 | */
6 |
7 | import React, { memo } from 'react';
8 | // import PropTypes from 'prop-types'
9 | // import styled from 'styled-components'
10 |
11 | import { FormattedMessage as T } from 'react-intl';
12 |
13 | function Button() {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | Button.propTypes = {};
22 |
23 | export default memo(Button);
24 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/tests/selectors.test.js:
--------------------------------------------------------------------------------
1 | import { selectHomeContainer } from '../selectors';
2 |
3 | describe('HomeContainer selector tests', () => {
4 | let mockedState;
5 |
6 | beforeEach(() => {
7 | mockedState = {
8 | homeContainer: {},
9 | };
10 | });
11 |
12 | it('should select the user state', () => {
13 | const homeContainerSelector = selectHomeContainer();
14 | expect(homeContainerSelector(mockedState)).toEqual(
15 | mockedState.homeContainer,
16 | );
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/generators/container/selectors.js.hbs:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { initialState } from './reducer';
3 |
4 | /**
5 | * Direct selector to the {{ camelCase name }} state domain
6 | */
7 |
8 | const select{{ properCase name }}Domain = state => state.{{ camelCase name }} || initialState;
9 |
10 | export const select{{ properCase name }} = () =>
11 | createSelector(select{{ properCase name }}Domain, substate => substate);
12 |
13 | export const selectSomePayLoad = () =>
14 | createSelector(select{{properCase name}}Domain, substate => substate.somePayLoad);
15 |
--------------------------------------------------------------------------------
/generators/container/saga.test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * Test {{ camelCase name }} sagas
3 | */
4 |
5 | import { takeLatest } from 'redux-saga/effects'
6 | import {{ camelCase name}}Saga, { defaultFunction } from '../saga'
7 | import { {{ camelCase name}}Types } from '../reducer'
8 |
9 | describe('{{ properCase name }} saga tests', () => {
10 | const generator = {{ camelCase name }}Saga();
11 |
12 | it('should start task to watch for DEFAULT_ACTION action', () => {
13 | expect(generator.next().value).toEqual(
14 | takeLatest({{ camelCase name }}Types.DEFAULT_ACTION, defaultFunction)
15 | );
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/generators/webpack/base/babel/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TestUtil Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | 'use strict';
8 |
9 | const cwd = process.cwd();
10 | module.exports = {
11 | prompts: [],
12 | actions: () => {
13 | const actions = [
14 | {
15 | type: 'add',
16 | path: `${cwd}/internals/webpack/webpack.base.babel.js`,
17 | templateFile: './webpack/base/babel/babel.js.hbs',
18 | abortOnFail: true,
19 | },
20 | ];
21 |
22 | return actions;
23 | },
24 | prettier: () => ({
25 | type: 'prettify',
26 | path: `${cwd}/`,
27 | }),
28 | };
29 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/tests/saga.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test homeContainer sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | import { takeLatest } from 'redux-saga/effects';
7 | import homeContainerSaga, { defaultFunction } from '../saga';
8 | import { homeContainerTypes } from '../reducer';
9 |
10 | describe('HomeContainer saga tests', () => {
11 | const generator = homeContainerSaga();
12 |
13 | it('should start task to watch for DEFAULT_ACTION action', () => {
14 | expect(generator.next().value).toEqual(
15 | takeLatest(homeContainerTypes.DEFAULT_ACTION, defaultFunction),
16 | );
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Tests for HomeContainer
4 | *
5 | *
6 | */
7 |
8 | import React from 'react';
9 | import { renderProvider } from '@utils/testUtils';
10 | // import { fireEvent } from '@testing-library/dom'
11 | import { HomeContainerTest as HomeContainer } from '../index';
12 |
13 | describe(' container tests', () => {
14 | // let submitSpy
15 |
16 | beforeEach(() => {
17 | // submitSpy = jest.fn()
18 | });
19 | it('should render and match the snapshot', () => {
20 | const { baseElement } = renderProvider( );
21 | expect(baseElement).toMatchSnapshot();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/generated-files/component/Button/tests/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Tests for Button
4 | *
5 | */
6 |
7 | import React from 'react';
8 | // import { fireEvent } from '@testing-library/dom'
9 | import { renderWithIntl } from '@utils/testUtils';
10 | import Button from '../index';
11 |
12 | describe(' ', () => {
13 | it('should render and match the snapshot', () => {
14 | const { baseElement } = renderWithIntl( );
15 | expect(baseElement).toMatchSnapshot();
16 | });
17 |
18 | it('should contain 1 Button component', () => {
19 | const { getAllByTestId } = renderWithIntl( );
20 | expect(getAllByTestId('button').length).toBe(1);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/generators/component/index.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{ properCase name }}
4 | *
5 | */
6 |
7 | {{#if memo}}
8 | import React, { memo } from 'react'
9 | {{else}}
10 | import React from 'react'
11 | {{/if}}
12 | // import PropTypes from 'prop-types'
13 | // import styled from 'styled-components'
14 | import T from '@components/T';
15 |
16 | export function {{ properCase name }}() {
17 | return (
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | {{ properCase name }}.propTypes = {}
25 |
26 | {{#if memo}}
27 | export default memo({{ properCase name }})
28 | {{else}}
29 | export default {{ properCase name }}
30 | {{/if}}
31 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const prettierOptions = JSON.parse(
5 | fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8'),
6 | );
7 |
8 | module.exports = {
9 | parser: 'babel-eslint',
10 | env: {
11 | browser: true,
12 | es6: true,
13 | amd: true,
14 | },
15 |
16 | extends: ['prettier', 'prettier-standard'],
17 | rules: {
18 | 'prettier/prettier': ['error', prettierOptions],
19 | 'import/no-webpack-loader-syntax': 0,
20 | curly: ['error', 'all'],
21 | 'no-console': ['error', { allow: ['error'] }],
22 | },
23 | globals: {
24 | GLOBAL: false,
25 | it: false,
26 | expect: false,
27 | describe: false,
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/generators/container/test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Tests for {{ properCase name }} container
4 | *
5 | *
6 | */
7 |
8 |
9 | import React from 'react';
10 | // import { fireEvent } from '@testing-library/dom';
11 | import { renderProvider } from '@utils/testUtils';
12 | import { {{ properCase name }}Test as {{ properCase name }} } from '../index';
13 |
14 | describe('<{{ properCase name }} /> container tests', () => {
15 | // let submitSpy
16 |
17 | beforeEach(() => {
18 | // submitSpy = jest.fn();
19 | });
20 |
21 | it('should render and match the snapshot', () => {
22 | const { baseElement } = renderProvider(
23 | <{{ properCase name }} />
24 | );
25 | expect(baseElement).toMatchSnapshot();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/generators/container/reducer.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} reducer
4 | *
5 | */
6 | import produce from 'immer'
7 | import { createActions } from 'reduxsauce'
8 |
9 | export const initialState = {
10 | somePayLoad: null
11 | }
12 |
13 | export const { Types: {{ camelCase name }}Types, Creators: {{ camelCase name }}Creators } = createActions({
14 | defaultAction: ['somePayLoad']
15 | })
16 |
17 | export const {{ camelCase name }}Reducer = (state = initialState, action) =>
18 | produce(state, draft => {
19 | switch (action.type) {
20 | case {{ camelCase name}}Types.DEFAULT_ACTION:
21 | draft.somePayLoad = action.somePayLoad;
22 | default:
23 | }
24 | })
25 |
26 | export default {{ camelCase name }}Reducer
27 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { initialState } from './reducer';
3 |
4 | /**
5 | * Direct selector to the homeContainer state domain
6 | */
7 |
8 | const selectHomeContainerDomain = state => state.homeContainer || initialState;
9 |
10 | /**
11 | * use createSelector if you are doing something with the returned state.
12 | * https://redux.js.org/usage/deriving-data-selectors#createselector-overview
13 | * e.g: const makeSelectHomeContainer = () =>
14 | * createSelector(selectHomeContainerDomain, substate => get(substate, 'somevalue'))
15 | */
16 | export const selectHomeContainer = () =>
17 | createSelector(
18 | selectHomeContainerDomain,
19 | substate => substate,
20 | );
21 |
--------------------------------------------------------------------------------
/generators/component/test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Tests for {{ properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react'
8 | // import { fireEvent } from '@testing-library/dom'
9 | import { renderWithIntl } from '@utils/testUtils'
10 | import {{ properCase name }} from '../index'
11 |
12 | describe('<{{ properCase name }} />', () => {
13 |
14 | it('should render and match the snapshot', () => {
15 | const { baseElement } = renderWithIntl(<{{ properCase name }} />)
16 | expect(baseElement).toMatchSnapshot()
17 | })
18 |
19 | it('should contain 1 {{ properCase name }} component', () => {
20 | const { getAllByTestId } = renderWithIntl(<{{ properCase name }} />)
21 | expect(getAllByTestId('{{ kebabCase name }}').length).toBe(1)
22 | })
23 | })
--------------------------------------------------------------------------------
/generators/container/selectors.test.js.hbs:
--------------------------------------------------------------------------------
1 | import { select{{ properCase name }}, selectSomePayLoad } from '../selectors';
2 |
3 | describe('{{ properCase name }} selector tests', () => {
4 | const mockedState = {
5 | {{ camelCase name }}: {
6 | somePayLoad: "W.S"
7 | }
8 | }
9 |
10 | it('should select the {{ camelCase name }} state', () => {
11 | const {{ camelCase name }}Selector = select{{ properCase name }}();
12 | expect({{ camelCase name }}Selector(mockedState)).toEqual(mockedState.{{ camelCase name }});
13 | });
14 |
15 | it('should select the somePayLoad state', () => {
16 | const somePayLoadSelector = selectSomePayLoad();
17 | expect(somePayLoadSelector(mockedState)).toEqual(mockedState.{{ camelCase name }}.somePayLoad);
18 | });
19 | })
--------------------------------------------------------------------------------
/generators/container/reducer.test.js.hbs:
--------------------------------------------------------------------------------
1 | import { {{ camelCase name }}Reducer, {{ camelCase name }}Types, initialState } from '../reducer';
2 |
3 | describe('{{ properCase name }} reducer tests', () => {
4 | it('should return the initial state by default', () => {
5 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(initialState);
6 | });
7 |
8 | it('should return the updated state when an action of type DEFAULT is dispatched', () => {
9 | const expectedResult = {...initialState, somePayLoad: 'Mohammed Ali Chherawalla'};
10 | expect(
11 | {{ camelCase name }}Reducer(initialState, {
12 | type: {{ camelCase name}}Types.DEFAULT_ACTION,
13 | somePayLoad: 'Mohammed Ali Chherawalla'
14 | })
15 | ).toEqual(expectedResult);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/generators/loadable/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Loadable Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | 'use strict';
8 |
9 | const cwd = process.cwd();
10 | module.exports = {
11 | description: 'Create a loadable util file ',
12 | prompts: [
13 | {
14 | type: 'input',
15 | name: 'path',
16 | message: 'What is the utils directory? (app/utils)',
17 | default: 'app/utils',
18 | },
19 | ],
20 | actions: () => {
21 | const actions = [
22 | {
23 | type: 'add',
24 | path: `${cwd}/{{path}}/loadable.js`,
25 | templateFile: './loadable/loadable.js.hbs',
26 | abortOnFail: true,
27 | },
28 | ];
29 |
30 | return actions;
31 | },
32 | prettier: () => ({
33 | type: 'prettify',
34 | path: `{{path}}/`,
35 | }),
36 | };
37 |
--------------------------------------------------------------------------------
/generators/testUtil/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TestUtil Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | 'use strict';
8 |
9 | const cwd = process.cwd();
10 | module.exports = {
11 | description: 'Create a test util file',
12 | prompts: [
13 | {
14 | type: 'input',
15 | name: 'path',
16 | message: 'What is the utils directory? (app/utils)',
17 | default: 'app/utils',
18 | },
19 | ],
20 | actions: () => {
21 | const actions = [
22 | {
23 | type: 'add',
24 | path: `${cwd}/{{path}}/testUtils.js`,
25 | templateFile: './testUtil/testUtils.js.hbs',
26 | abortOnFail: true,
27 | },
28 | ];
29 |
30 | return actions;
31 | },
32 | prettier: () => ({
33 | type: 'prettify',
34 | path: `{{path}}/`,
35 | }),
36 | };
37 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | modules: false,
7 | },
8 | ],
9 | '@babel/preset-react',
10 | ],
11 | plugins: [
12 | 'styled-components',
13 | '@babel/plugin-proposal-class-properties',
14 | '@babel/plugin-syntax-dynamic-import',
15 | ],
16 | env: {
17 | production: {
18 | only: ['app'],
19 | plugins: [
20 | 'lodash',
21 | 'transform-react-remove-prop-types',
22 | '@babel/plugin-transform-react-inline-elements',
23 | '@babel/plugin-transform-react-constant-elements',
24 | ],
25 | },
26 | test: {
27 | plugins: [
28 | '@babel/plugin-transform-modules-commonjs',
29 | 'dynamic-import-node',
30 | ],
31 | },
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * HomeContainer reducer
4 | *
5 | */
6 | import produce from 'immer';
7 | import { createActions } from 'reduxsauce';
8 |
9 | export const initialState = {};
10 |
11 | export const {
12 | Types: homeContainerTypes,
13 | Creators: homeContainerCreators,
14 | } = createActions({
15 | defaultAction: ['somePayload'],
16 | });
17 |
18 | /* eslint-disable default-case, no-param-reassign */
19 | export const homeContainerReducer = (state = initialState, action) =>
20 | produce(state, (/* draft */) => {
21 | switch (action.type) {
22 | case homeContainerTypes.DEFAULT_ACTION:
23 | return { ...state, somePayload: action.somePayload };
24 | default:
25 | return state;
26 | }
27 | });
28 |
29 | export default homeContainerReducer;
30 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: react-floki
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | publish-npm:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v2
14 | with:
15 | node-version: 14
16 | registry-url: https://registry.npmjs.org/
17 | - name: bump version
18 | run: |
19 | git config --global user.email "git@wednesday.is"
20 | git config --global user.name "Git"
21 | npm version patch -m 'Bump up'
22 | - name: Push changes
23 | uses: ad-m/github-push-action@master
24 | with:
25 | github_token: ${{ secrets.GITHUB_TOKEN }}
26 | branch: ${{ github.ref }}
27 | - name: Publish
28 | run: yarn publish
29 | env:
30 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
31 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/tests/reducer.test.js:
--------------------------------------------------------------------------------
1 | // import produce from 'immer'
2 | import {
3 | homeContainerReducer,
4 | homeContainerTypes,
5 | initialState,
6 | } from '../reducer';
7 |
8 | /* eslint-disable default-case, no-param-reassign */
9 | describe('HomeContainer reducer tests', () => {
10 | let state;
11 | beforeEach(() => {
12 | state = initialState;
13 | });
14 |
15 | it('should return the initial state', () => {
16 | expect(homeContainerReducer(undefined, {})).toEqual(state);
17 | });
18 |
19 | it('should return the update the state when an action of type DEFAULT is dispatched', () => {
20 | const expectedResult = {
21 | ...state,
22 | somePayload: 'Mohammed Ali Chherawalla',
23 | };
24 | expect(
25 | homeContainerReducer(state, {
26 | type: homeContainerTypes.DEFAULT_ACTION,
27 | somePayload: 'Mohammed Ali Chherawalla',
28 | }),
29 | ).toEqual(expectedResult);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverageFrom: [
3 | 'app/**/*.{js,jsx}',
4 | '!app/**/*.test.{js,jsx}',
5 | '!app/*/RbGenerated*/*.{js,jsx}',
6 | '!app/app.js',
7 | '!app/global-styles.js',
8 | '!app/*/*/Loadable.{js,jsx}',
9 | ],
10 | coverageThreshold: {
11 | global: {
12 | statements: 98,
13 | branches: 91,
14 | functions: 98,
15 | lines: 98,
16 | },
17 | },
18 | moduleDirectories: ['node_modules', 'app'],
19 | moduleNameMapper: {
20 | '.*\\.(css|less|styl|scss|sass)$': '/internals/mocks/cssModule.js',
21 | '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
22 | '/internals/mocks/image.js',
23 | },
24 | setupFilesAfterEnv: [
25 | '/testing/test-bundler.js',
26 | 'react-testing-library/cleanup-after-each',
27 | ],
28 | setupFiles: ['raf/polyfill'],
29 | testRegex: 'tests/.*\\.test\\.js$',
30 | snapshotSerializers: [],
31 | };
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-Present Mohammed Ali Chherawalla
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/generators/component/new/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Component Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | ('use strict');
8 |
9 | const existing = require('../existing');
10 | const cwd = process.cwd();
11 |
12 | const prompts = [
13 | {
14 | type: 'input',
15 | name: 'name',
16 | message: 'What should it be called?',
17 | default: 'Button',
18 | },
19 | {
20 | type: 'confirm',
21 | name: 'memo',
22 | default: false,
23 | message: 'Do you want to wrap your component in React.memo?',
24 | },
25 | ];
26 | prompts.unshift(existing.pathPrompt);
27 | prompts.push(existing.storyPrompt);
28 |
29 | module.exports = {
30 | description: 'Add an unconnected component',
31 | prompts,
32 | actions: data => {
33 | // Generate index.js and index.test.js
34 | const actions = [
35 | {
36 | type: 'add',
37 | path: `${cwd}/{{path}}/{{properCase name}}/index.js`,
38 | templateFile: './component/index.js.hbs',
39 | abortOnFail: true,
40 | },
41 | ];
42 |
43 | actions.push(...existing.actions(data));
44 | actions.push(existing.prettier());
45 |
46 | return actions;
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/generated-files/container/HomeContainer/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * HomeContainer
4 | *
5 | */
6 |
7 | import React, { memo } from 'react';
8 | // import PropTypes from 'prop-types'
9 | import { connect } from 'react-redux';
10 | import { injectIntl } from 'react-intl';
11 | import { Helmet } from 'react-helmet';
12 | import { FormattedMessage as T } from 'react-intl';
13 | import { createStructuredSelector } from 'reselect';
14 | import { compose } from 'redux';
15 | import { injectSaga } from 'redux-injectors';
16 | import { selectHomeContainer } from './selectors';
17 | import saga from './saga';
18 |
19 | export function HomeContainer() {
20 | return (
21 |
22 |
23 | HomeContainer
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | HomeContainer.propTypes = {};
32 |
33 | const mapStateToProps = createStructuredSelector({
34 | homeContainer: selectHomeContainer(),
35 | });
36 |
37 | function mapDispatchToProps(dispatch) {
38 | return {
39 | dispatch,
40 | };
41 | }
42 |
43 | const withConnect = connect(
44 | mapStateToProps,
45 | mapDispatchToProps,
46 | );
47 |
48 | export default compose(
49 | withConnect,
50 | memo,
51 | injectSaga({ key: 'homeContainer', saga }),
52 | )(HomeContainer);
53 |
54 | export const HomeContainerTest = compose(injectIntl)(HomeContainer);
55 |
--------------------------------------------------------------------------------
/generators/component/existing/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Component Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | 'use strict';
8 |
9 | const cwd = process.cwd();
10 |
11 | const storyPrompt = {
12 | type: 'confirm',
13 | name: 'wantStories',
14 | default: true,
15 | message: 'Do you want stories for your component?',
16 | };
17 | const pathPrompt = {
18 | type: 'input',
19 | name: 'path',
20 | message: 'What is the component directory? (app/components)',
21 | default: 'app/components',
22 | };
23 |
24 | const prompts = [
25 | {
26 | type: 'input',
27 | name: 'name',
28 | message: 'What is the name of the component you want to add tests for?',
29 | default: 'Button',
30 | },
31 | ];
32 | prompts.push(storyPrompt);
33 | prompts.push(pathPrompt);
34 |
35 | module.exports = {
36 | description: 'Add tests for an existing component',
37 | storyPrompt,
38 | pathPrompt,
39 | prompts,
40 | actions: data => {
41 | // index.test.js
42 | const actions = [
43 | {
44 | type: 'add',
45 | path: `${cwd}/{{path}}/{{properCase name}}/tests/index.test.js`,
46 | templateFile: './component/test.js.hbs',
47 | abortOnFail: true,
48 | },
49 | ];
50 |
51 | if (data.wantStories) {
52 | actions.push({
53 | type: 'add',
54 | path: `${cwd}/{{path}}/{{properCase name}}/stories/{{properCase name}}.stories.js`,
55 | templateFile: './component/stories.js.hbs',
56 | abortOnFail: true,
57 | });
58 | }
59 |
60 | return actions;
61 | },
62 | prettier: () => ({
63 | type: 'prettify',
64 | path: `{{path}}/`,
65 | }),
66 | };
67 |
--------------------------------------------------------------------------------
/generated-files/test-util/testUtils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IntlProvider } from 'react-intl';
3 | import { render } from '@testing-library/react';
4 | import { Provider } from 'react-redux';
5 | import configureStore from '@app/configureStore';
6 | import { DEFAULT_LOCALE, translationMessages } from '@app/i18n';
7 | import ConnectedLanguageProvider from '@containers/LanguageProvider';
8 | import { BrowserRouter as Router, browserHistory } from 'react-router-dom';
9 | import { ThemeProvider } from 'styled-components';
10 |
11 | export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms));
12 |
13 | export const apiResponseGenerator = (ok, data) => ({
14 | ok,
15 | data,
16 | });
17 |
18 | export const renderProvider = children => {
19 | const store = configureStore({}, browserHistory);
20 | const theme = { main: 'violet' };
21 | return render(
22 |
23 |
24 | {children}
25 |
26 | ,
27 | );
28 | };
29 |
30 | export const renderWithIntl = children =>
31 | render(
32 |
36 | {children}
37 | ,
38 | );
39 |
40 | export const getComponentStyles = (Component, props = {}) => {
41 | renderWithIntl(Component(props));
42 | const { styledComponentId } = Component(props).type;
43 | const componentRoots = document.getElementsByClassName(styledComponentId);
44 | // eslint-disable-next-line no-underscore-dangle
45 | return window.getComputedStyle(componentRoots[0])._values;
46 | };
47 |
48 | export const renderWithRouterAndIntl = children =>
49 | renderWithIntl({children} );
50 |
--------------------------------------------------------------------------------
/generators/testUtil/testUtils.js.hbs:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IntlProvider } from 'react-intl'
3 | import { render } from '@testing-library/react'
4 | import { Provider } from 'react-redux'
5 | import configureStore from '@app/configureStore'
6 | import { DEFAULT_LOCALE, translationMessages } from '@app/i18n'
7 | import ConnectedLanguageProvider from '@containers/LanguageProvider'
8 | import { BrowserRouter as Router, browserHistory } from 'react-router-dom'
9 | import { ThemeProvider } from 'styled-components'
10 |
11 | export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
12 |
13 | export const apiResponseGenerator = (ok, data) => ({
14 | ok,
15 | data
16 | })
17 |
18 | export const renderProvider = children => {
19 | const store = configureStore({}, browserHistory)
20 | const theme = {main: 'violet'}
21 | return render(
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export const renderWithIntl = children =>
33 | render(
34 |
38 | {children}
39 |
40 | )
41 |
42 | export const getComponentStyles = (Component, props = {}) => {
43 | renderWithIntl(Component(props))
44 | const { styledComponentId } = Component(props).type
45 | const componentRoots = document.getElementsByClassName(styledComponentId)
46 | // eslint-disable-next-line no-underscore-dangle
47 | return window.getComputedStyle(componentRoots[0])._values
48 | }
49 |
50 | export const renderWithRouterAndIntl = children =>
51 | renderWithIntl({children} );
52 |
--------------------------------------------------------------------------------
/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{properCase name }} Container
4 | *
5 | */
6 |
7 | {{#if memo}}
8 | import React, { memo } from 'react';
9 | {{else}}
10 | import React from 'react';
11 | {{/if}}
12 | import PropTypes from 'prop-types';
13 | import { connect } from 'react-redux';
14 | import { injectIntl } from 'react-intl';
15 | {{#if wantHeaders}}
16 | import { Helmet } from 'react-helmet';
17 | {{/if}}
18 | import { FormattedMessage as T } from 'react-intl';
19 | {{#if wantActionsAndReducer}}
20 | import { createStructuredSelector } from 'reselect';
21 | {{/if}}
22 | import { compose } from 'redux';
23 | {{#if wantSaga}}
24 | import { injectSaga } from 'redux-injectors';
25 | {{/if}}
26 | {{#if wantActionsAndReducer}}
27 | import { selectSomePayLoad } from './selectors';
28 | {{/if}}
29 | {{#if wantSaga}}
30 | import saga from './saga';
31 | {{/if}}
32 |
33 | export function {{ properCase name }}() {
34 | return (
35 |
36 | {{#if wantHeaders}}
37 |
38 | {{properCase name}}
39 |
40 |
41 | {{/if}}
42 |
43 |
44 | )
45 | };
46 |
47 | {{ properCase name }}.propTypes = {
48 | somePayLoad: PropTypes.any,
49 | };
50 |
51 | {{#if wantActionsAndReducer}}
52 | const mapStateToProps = createStructuredSelector({
53 | somePayLoad: selectSomePayLoad(),
54 | })
55 | {{/if}}
56 |
57 | function mapDispatchToProps(dispatch) {
58 | return {
59 | dispatch,
60 | };
61 | };
62 |
63 | {{#if wantActionsAndReducer}}
64 | const withConnect = connect(mapStateToProps, mapDispatchToProps);
65 | {{else}}
66 | const withConnect = connect(null, mapDispatchToProps);
67 | {{/if}}
68 |
69 | export default compose(
70 | withConnect,
71 | {{#if memo}}
72 | memo,
73 | {{/if}}
74 | {{#if wantSaga}}
75 | injectSaga({ key: '{{ camelCase name }}', saga })
76 | {{/if}}
77 | )({{ properCase name }});
78 |
79 | export const {{ properCase name }}Test = compose(injectIntl)({{ properCase name }});
80 |
--------------------------------------------------------------------------------
/generators/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * generator/index.js
3 | *
4 | * Exports the generators so plop knows them
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const shell = require('shelljs');
10 | const { execSync } = require('child_process');
11 | const prettier = path.join(__dirname, '../node_modules/.bin/prettier');
12 | const componentGenerator = require(`./component/${
13 | shell.env.GENERATOR_TYPE
14 | }/index.js`);
15 | const containerGenerator = require(`./container/${
16 | shell.env.GENERATOR_TYPE
17 | }/index.js`);
18 | const testUtilGenerator = require(`./testUtil/index.js`);
19 | const loadableUtilGenerator = require(`./loadable/index.js`);
20 | const webpackBaseBabelGenerator = require(`./webpack/base/babel/index.js`);
21 |
22 | /**
23 | * Every generated backup file gets this extension
24 | * @type {string}
25 | */
26 | const BACKUPFILE_EXTENSION = 'rbgen';
27 |
28 | module.exports = plop => {
29 | plop.setGenerator('component', componentGenerator);
30 | plop.setGenerator('container', containerGenerator);
31 | plop.setGenerator('tUtil', testUtilGenerator);
32 | plop.setGenerator('loadable', loadableUtilGenerator);
33 | plop.setGenerator('webpackBaseBabel', webpackBaseBabelGenerator);
34 |
35 | plop.addHelper('directory', comp => {
36 | try {
37 | fs.accessSync(
38 | path.join(__dirname, `../../app/containers/${comp}`),
39 | fs.F_OK,
40 | );
41 | return `containers/${comp}`;
42 | } catch (e) {
43 | return `components/${comp}`;
44 | }
45 | });
46 | plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
47 | plop.setActionType('prettify', answers => {
48 | const folderPath = `${path.join(
49 | `${process.cwd()}/${answers.path}/`,
50 | plop.getHelper('properCase')(answers.name),
51 | '**/*.*.js',
52 | )}`;
53 |
54 | try {
55 | execSync(`${prettier} --write -- "${folderPath}"`);
56 | return folderPath;
57 | } catch (err) {
58 | throw new Error(`Prettier failed`);
59 | }
60 | });
61 | };
62 |
63 | module.exports.BACKUPFILE_EXTENSION = BACKUPFILE_EXTENSION;
64 |
--------------------------------------------------------------------------------
/generators/container/existing/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Container Generator
3 | */
4 |
5 | const cwd = process.cwd();
6 | module.exports = {
7 | description: 'Add test for an existing container component',
8 | prompts: [
9 | {
10 | type: 'input',
11 | name: 'path',
12 | message: 'What is the container directory? (app/containers)',
13 | default: 'app/containers',
14 | },
15 | {
16 | type: 'input',
17 | name: 'name',
18 | message: 'Which container do you want to add tests for?',
19 | default: 'Form',
20 | },
21 | {
22 | type: 'confirm',
23 | name: 'wantActionsAndReducer',
24 | default: true,
25 | message:
26 | 'Do you want add tests for actions, selectors & reducer tuple for this container?',
27 | },
28 | {
29 | type: 'confirm',
30 | name: 'wantSaga',
31 | default: true,
32 | message: 'Do you want to add tests for sagas?',
33 | },
34 | ],
35 | actions: data => {
36 | // Generate index.js and index.test.js
37 | const actions = [
38 | {
39 | type: 'add',
40 | path: `${cwd}/{{path}}/{{properCase name}}/tests/index.test.js`,
41 | templateFile: './container/test.js.hbs',
42 | abortOnFail: true,
43 | },
44 | ];
45 |
46 | // If they want actions and a reducer, generate reducer.js,
47 | // and the corresponding tests for actions and the reducer
48 | if (data.wantActionsAndReducer) {
49 | // Selectors
50 | actions.push({
51 | type: 'add',
52 | path: `${cwd}/{{path}}/{{properCase name}}/tests/selectors.test.js`,
53 | templateFile: './container/selectors.test.js.hbs',
54 | abortOnFail: true,
55 | });
56 |
57 | // Reducer
58 | actions.push({
59 | type: 'add',
60 | path: `${cwd}/{{path}}/{{properCase name}}/tests/reducer.test.js`,
61 | templateFile: './container/reducer.test.js.hbs',
62 | abortOnFail: true,
63 | });
64 | }
65 |
66 | // Sagas
67 | if (data.wantSaga) {
68 | actions.push({
69 | type: 'add',
70 | path: `${cwd}/{{path}}/{{properCase name}}/tests/saga.test.js`,
71 | templateFile: './container/saga.test.js.hbs',
72 | abortOnFail: true,
73 | });
74 | }
75 | return actions;
76 | },
77 | prettier: () => ({
78 | type: 'prettify',
79 | path: `{{path}}/`,
80 | }),
81 | };
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-floki",
3 | "version": "1.0.96",
4 | "description": "A React component, container and test generation library",
5 | "repository": {
6 | "type": "git",
7 | "url": "git://github.com/wednesday-solutions/react-floki.git"
8 | },
9 | "engines": {
10 | "npm": ">=5",
11 | "node": ">=8.15.1"
12 | },
13 | "author": "Wednesday Solutions",
14 | "license": "MIT",
15 | "scripts": {
16 | "lint": "npm run lint:js && npm run lint:css",
17 | "lint:css": "stylelint app/**/*.js",
18 | "lint:eslint": "eslint --ignore-path .eslintignore --ignore-pattern internals/scripts",
19 | "lint:eslint:fix": "eslint --ignore-path .eslintignore --ignore-pattern internals/scripts --fix",
20 | "lint:js": "npm run lint:eslint -- . ",
21 | "lint:staged": "lint-staged",
22 | "pretest": "#npm run test:clean && npm run lint",
23 | "test:clean": "rimraf ./coverage",
24 | "test": "jest --coverage",
25 | "test:watch": "jest --watchAll",
26 | "coveralls": "cat ./coverage/lcov.info | coveralls",
27 | "prettify": "prettier --write",
28 | "initialize": "git checkout --orphan temp-branch && git add -A && git commit -m 'Initial commit' && git branch -D master && git branch -m master"
29 | },
30 | "lint-staged": {
31 | "*.js": [
32 | "npm run lint:eslint:fix",
33 | "git add --force"
34 | ],
35 | "*.json": [
36 | "prettier --write",
37 | "git add --force"
38 | ]
39 | },
40 | "keywords": [
41 | "react",
42 | "test",
43 | "generator",
44 | "cli",
45 | "jest",
46 | "container",
47 | "component"
48 | ],
49 | "pre-commit": "lint:staged",
50 | "resolutions": {
51 | "babel-core": "7.0.0-bridge.0"
52 | },
53 | "dependencies": {
54 | "babel-eslint": "10.0.1",
55 | "circular-dependency-plugin": "5.0.2",
56 | "coveralls": "3.0.3",
57 | "eslint": "5.16.0",
58 | "eslint-config-airbnb": "17.1.0",
59 | "eslint-config-airbnb-base": "13.1.0",
60 | "eslint-config-prettier": "4.1.0",
61 | "eslint-config-prettier-standard": "^3.0.1",
62 | "eslint-config-standard": "^14.1.1",
63 | "eslint-import-resolver-webpack": "0.11.1",
64 | "eslint-plugin-import": "2.17.2",
65 | "eslint-plugin-jsx-a11y": "6.2.1",
66 | "eslint-plugin-node": "^11.1.0",
67 | "eslint-plugin-prettier": "3.0.1",
68 | "eslint-plugin-promise": "^4.2.1",
69 | "eslint-plugin-standard": "^4.0.1",
70 | "json": "^9.0.6",
71 | "lint-staged": "8.1.5",
72 | "minimist": "1.2.3",
73 | "node-plop": "0.18.0",
74 | "plop": "^2.5.2",
75 | "pre-commit": "1.2.2",
76 | "prettier": "1.17.0",
77 | "prettier-config-standard": "^1.0.1",
78 | "redux-saga": "^1.1.1",
79 | "shelljs": "^0.8.3"
80 | },
81 | "@babel/cli": "7.4.3",
82 | "bin": {
83 | "react-generate": "bin/react-generate.js"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/generators/container/new/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Container Generator
3 | */
4 | const existing = require('../existing');
5 |
6 | const cwd = process.cwd();
7 | module.exports = {
8 | description: 'Add a container component',
9 | prompts: [
10 | {
11 | type: 'input',
12 | name: 'path',
13 | message: 'What is the container directory? (app/containers)',
14 | default: 'app/containers',
15 | },
16 | {
17 | type: 'input',
18 | name: 'name',
19 | message: 'What should it be called?',
20 | default: 'Form',
21 | },
22 | {
23 | type: 'confirm',
24 | name: 'memo',
25 | default: false,
26 | message: 'Do you want to wrap your component in React.memo?',
27 | },
28 | {
29 | type: 'confirm',
30 | name: 'wantHeaders',
31 | default: false,
32 | message: 'Do you want headers?',
33 | },
34 | {
35 | type: 'confirm',
36 | name: 'wantActionsAndReducer',
37 | default: true,
38 | message:
39 | 'Do you want an actions/constants/selectors/reducer tuple for this container?',
40 | },
41 | {
42 | type: 'confirm',
43 | name: 'wantSaga',
44 | default: true,
45 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)',
46 | },
47 | ],
48 | actions: data => {
49 | // Generate index.js and index.test.js
50 | const actions = [
51 | {
52 | type: 'add',
53 | path: `${cwd}/{{path}}/{{properCase name}}/index.js`,
54 | templateFile: './container/index.js.hbs',
55 | abortOnFail: true,
56 | },
57 | ];
58 |
59 | // If they want actions and a reducer, generate reducer.js and the
60 | // corresponding tests for actions and the reducer
61 | if (data.wantActionsAndReducer) {
62 | // Selectors
63 | actions.push({
64 | type: 'add',
65 | path: `${cwd}/{{path}}/{{properCase name}}/selectors.js`,
66 | templateFile: './container/selectors.js.hbs',
67 | abortOnFail: true,
68 | });
69 |
70 | // Reducer
71 | actions.push({
72 | type: 'add',
73 | path: `${cwd}/{{path}}/{{properCase name}}/reducer.js`,
74 | templateFile: './container/reducer.js.hbs',
75 | abortOnFail: true,
76 | });
77 | }
78 |
79 | // Sagas
80 | if (data.wantSaga) {
81 | actions.push({
82 | type: 'add',
83 | path: `${cwd}/{{path}}/{{properCase name}}/saga.js`,
84 | templateFile: './container/saga.js.hbs',
85 | abortOnFail: true,
86 | });
87 | }
88 |
89 | // Loadable
90 | actions.push({
91 | type: 'add',
92 | path: `${cwd}/{{path}}/{{properCase name}}/Loadable.js`,
93 | templateFile: './container/Loadable.js.hbs',
94 | abortOnFail: true,
95 | });
96 |
97 | actions.push(...existing.actions(data));
98 | actions.push(existing.prettier());
99 |
100 | return actions;
101 | },
102 | };
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
React Floki
9 |
10 |
11 |
12 |
13 | A CLI tool that works with the react template and allows you to scaffold tests, containers, components and stitches them all together preventing wastage of time in setup and boilerplate code.
14 |
15 |
16 | ___
17 |
18 |
19 |
20 |
21 | Expert teams of digital product strategists, developers, and designers.
22 |
23 |
24 |
25 |
33 |
34 | ___
35 |
36 |
We’re always looking for people who value their work, so come and join us. We are hiring!
37 |
38 |
39 | ## Installation
40 |
41 | ### Globally install react-generate
42 |
43 | `npm install -g react-floki`
44 |
45 | **OR**
46 |
47 | `yarn global add react-floki`
48 |
49 | ## Examples of generated files
50 |
51 | - [Container](generated-files/container)
52 | - [Component](generated-files/component)
53 | - [Loadable](generated-files/loadable)
54 | - [Test-util](generated-files/test-util)
55 |
56 | ## Generating containers with tests and stories
57 |
58 | 
59 |
60 | ## Generate components with tests and stories
61 |
62 | 
63 |
64 | ## Documentation
65 |
66 | ### Help
67 |
68 | To get a list of commands and usage hints use
69 |
70 | ```
71 | react-generate --help
72 | ```
73 |
74 | ### Creating a new React Application
75 |
76 | ```
77 | react-generate init movie-rating
78 | ```
79 |
80 | ### Generating tests for all existing components and containers
81 |
82 | **Creating a test for a container or component:** `react-generate gt`
83 |
84 | **Creating a test for an existing component:** `react-generate gtcom`
85 |
86 | **Creating a test for an existing container:** `react-generate gtcon`
87 |
88 | ### Forcefully generating tests for all existing components and containers
89 |
90 | **Forcefully creating a test for a container or component:** `react-generate gtf`
91 |
92 | **Forcefully creating a test for an existing component:** `react-generate gtcomf`
93 |
94 | **Forcefully creating a test for an existing container:** `react-generate gtconf`
95 |
96 | ### Generating components and containers
97 |
98 | **Creating a container or component:** `react-generate g`
99 |
100 | **Creating a component:** `react-generate gcom`
101 |
102 | **Creating a container:** `react-generate gcon`
103 |
104 | ### Forcefully generating components and containers
105 |
106 | **Forcefully creating a container or component:** `react-generate gf`
107 |
108 | **Forcefully creating a component:** `react-generate gcomf`
109 |
110 | **Forcefully creating a container:** `react-generate gconf`
111 |
112 | ### Generating tests for all existing components and containers
113 |
114 | **Generate test for all components in directory:** `react-generate --all component `
115 |
116 | **Generate test for all containers in directory:** `react-generate --all containers `
117 |
118 | ### Generating a testUtils file with some utility functions for tests
119 |
120 | **Generate a test util file:** `react-generate gtutil`
121 |
122 | ### Generating a utility for a loadable file using React 16 lazy and Suspense
123 |
124 | **Generating a utility for a loadable file :** `react-generate gloadable`
125 |
126 | # Advanced
127 |
128 | ## Example Usages
129 |
130 | **Creating a test by specifying type, path and name:** `react-generate gt component src/app Button`
131 |
132 | **Creating a test for an existing component by specifying path and name:** `react-generate gtcom src/app Button`
133 |
134 | **Creating a test for an existing container by specifying path and name:** `react-generate gtcon src/app HomePage`
135 |
136 | **Creating a component/container by specifying type, path and name:** `react-generate g component src/app Button`
137 |
138 | **Creating a component by specifying path and name:** `react-generate gcom src/app Button`
139 |
140 | **Creating a container by specifying path and name:** `react-generate gcon src/app HomePage`
141 |
142 | **Generate test for all components in directory:** `react-generate --all component src/app/components`
143 |
144 | **Generate test for all containers in directory:** `react-generate --all container src/app/containers`
145 |
146 | # Projects using it
147 |
148 | - [React Template](https://github.com/wednesday-solutions/react-template)
149 |
--------------------------------------------------------------------------------
/generators/webpack/base/babel/babel.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * COMMON WEBPACK CONFIGURATION
3 | */
4 | const path = require('path');
5 | const webpack = require('webpack');
6 | const dotenv = require('dotenv');
7 | const colors = require('../../app/themes/colors');
8 |
9 | const dotEnvFile =
10 | process.env.NODE_ENV === 'production'
11 | ? `.env`
12 | : `.env.${process.env.NODE_ENV}`;
13 | const env = dotenv.config({ path: dotEnvFile }).parsed;
14 | const envKeys = {
15 | ...Object.keys(process.env).reduce((prev, next) => {
16 | prev[`process.env.${next}`] = JSON.stringify(process.env[next]);
17 | return prev;
18 | }, {}),
19 | ...Object.keys(env).reduce((prev, next) => {
20 | prev[`process.env.${next}`] = JSON.stringify(env[next]);
21 | return prev;
22 | }, {})
23 | };
24 |
25 | module.exports = options => ({
26 | mode: options.mode,
27 | entry: options.entry,
28 | output: Object.assign(
29 | {
30 | // Compile into js/build.js
31 | path: path.resolve(process.cwd(), 'build'),
32 | publicPath: '/'
33 | },
34 | options.output
35 | ), // Merge with env dependent settings
36 | optimization: options.optimization,
37 | module: {
38 | rules: [
39 | {
40 | test: /\.jsx?$/, // Transform all .js and .jsx files required somewhere with Babel
41 | exclude: /node_modules/,
42 | use: {
43 | loader: 'babel-loader',
44 | options: options.babelQuery
45 | }
46 | },
47 | {
48 | // Preprocess our own .css files
49 | // This is the place to add your own loaders (e.g. sass/less etc.)
50 | // for a list of loaders, see https://webpack.js.org/loaders/#styling
51 | test: /\.css$/,
52 | exclude: /node_modules/,
53 | use: ['style-loader', 'css-loader']
54 | },
55 | {
56 | test: /\.less$/,
57 | use: [
58 | {
59 | loader: 'style-loader'
60 | },
61 | {
62 | loader: 'css-loader'
63 | },
64 | {
65 | loader: 'less-loader',
66 | options: {
67 | lessOptions: {
68 | javascriptEnabled: true,
69 | modifyVars: {
70 | 'primary-color': colors.secondary
71 | }
72 | }
73 | }
74 | }
75 | ]
76 | },
77 | {
78 | // Preprocess 3rd party .css files located in node_modules
79 | test: /\.css$/,
80 | include: /node_modules/,
81 | use: ['style-loader', 'css-loader']
82 | },
83 | {
84 | test: /\.(eot|otf|ttf|woff|woff2)$/,
85 | use: 'file-loader'
86 | },
87 | {
88 | test: /\.svg$/,
89 | use: [
90 | {
91 | loader: 'svg-url-loader',
92 | options: {
93 | // Inline files smaller than 10 kB
94 | limit: 10 * 1024,
95 | noquotes: true
96 | }
97 | }
98 | ]
99 | },
100 | {
101 | test: /\.(jpg|png|gif)$/,
102 | use: [
103 | {
104 | loader: 'url-loader',
105 | options: {
106 | // Inline files smaller than 10 kB
107 | limit: 10 * 1024
108 | }
109 | },
110 | {
111 | loader: 'image-webpack-loader',
112 | options: {
113 | mozjpeg: {
114 | enabled: false
115 | // NOTE: mozjpeg is disabled as it causes errors in some Linux environments
116 | // Try enabling it in your environment by switching the config to:
117 | // enabled: true,
118 | // progressive: true,
119 | },
120 | gifsicle: {
121 | interlaced: false
122 | },
123 | optipng: {
124 | optimizationLevel: 7
125 | },
126 | pngquant: {
127 | quality: '65-90',
128 | speed: 4
129 | }
130 | }
131 | }
132 | ]
133 | },
134 | {
135 | test: /\.html$/,
136 | use: 'html-loader'
137 | },
138 | {
139 | test: /\.(mp4|webm)$/,
140 | use: {
141 | loader: 'url-loader',
142 | options: {
143 | limit: 10000
144 | }
145 | }
146 | }
147 | ]
148 | },
149 | plugins: options.plugins.concat([
150 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
151 | // inside your code for any environment checks; Terser will automatically
152 | // drop any unreachable code.
153 | new webpack.EnvironmentPlugin({
154 | NODE_ENV: 'development'
155 | }),
156 | new webpack.DefinePlugin(envKeys)
157 | ]),
158 | resolve: {
159 | modules: ['node_modules', 'app'],
160 | alias: {
161 | '@app': path.resolve(__dirname, '../../app'),
162 | '@components': path.resolve(__dirname, '../../app/components'),
163 | '@containers': path.resolve(__dirname, '../../app/containers'),
164 | '@utils': path.resolve(__dirname, '../../app/utils'),
165 | '@services': path.resolve(__dirname, '../../app/services'),
166 | '@themes': path.resolve(__dirname, '../../app/themes'),
167 | '@images': path.resolve(__dirname, '../../app/images'),
168 | '@hooks': path.resolve(__dirname, '../../app/hooks'),
169 | moment$: path.resolve(__dirname, '../../node_modules/moment/moment.js')
170 | },
171 | extensions: ['.js', '.jsx', '.react.js'],
172 | mainFields: ['browser', 'jsnext:main', 'main']
173 | },
174 | devtool: options.devtool,
175 | target: 'web', // Make web variables accessible to webpack, e.g. window
176 | performance: options.performance || {}
177 | });
--------------------------------------------------------------------------------
/react_floki_github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/floki.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/bin/react-generate.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | const shell = require('shelljs');
3 | const fs = require('fs');
4 | const childProcess = require('child_process');
5 | const process = require('process');
6 | const _ = require('lodash');
7 | const path = require('path');
8 |
9 | const COMPONENT = 'component';
10 | const CONTAINER = 'container';
11 | const WEBPACK_BASE_BABEL = 'webpackBaseBabel';
12 | const TEST_UTIL = 'tUtil';
13 | const LOADABLE = 'loadable';
14 |
15 | const generator = path.join(__dirname, '../generators/index.js');
16 | const plopGen = ['--plopfile', generator];
17 |
18 | const [, , ...args] = process.argv;
19 | let commandLineArgs = args.toString().split(',');
20 | const stdioInherit = { stdio: 'inherit' };
21 |
22 | function execShell(commandArray) {
23 | childProcess.execFileSync(
24 | path.join(__dirname, '../node_modules/.bin/plop'),
25 | commandArray,
26 | { ...stdioInherit },
27 | );
28 | }
29 |
30 | // validate input
31 | if (!commandLineArgs[0]) {
32 | shell.exec(
33 | `echo Sorry! react-generate requires an argument to be passed. Run react-generate --help for more details`,
34 | );
35 | return;
36 | }
37 |
38 | // get the type of generator
39 | shell.env.GENERATOR_TYPE = _.includes(commandLineArgs[0], 't')
40 | ? 'existing'
41 | : 'new';
42 | let directoryName = 'react-template';
43 | switch (commandLineArgs[0]) {
44 | case 'init':
45 | shell.exec(
46 | `git clone https://github.com/wednesday-solutions/react-template`,
47 | );
48 | shell.cd(process.cwd());
49 | if (commandLineArgs[1]) {
50 | shell.exec(`mkdir ${commandLineArgs[1]}`);
51 | fs.rename(`react-template`, commandLineArgs[1], err => {
52 | if (err) {
53 | throw new Error('Error while renaming');
54 | }
55 | });
56 | directoryName = commandLineArgs[1];
57 | const json = path.join(__dirname, '../node_modules/.bin/json');
58 |
59 | shell.exec(
60 | `${json} -I -f ${directoryName}/package.json -e "this.name='${directoryName}'"`,
61 | );
62 | shell.exec(
63 | `${json} -I -f ${directoryName}/package.json -e "this.homepage='""'"`,
64 | );
65 | shell.exec(
66 | `${json} -I -f ${directoryName}/package.json -e "this.author='""'"`,
67 | );
68 | shell.exec(
69 | `${json} -I -f ${directoryName}/package.json -e "this.repository='{}'"`,
70 | );
71 |
72 | commandLineArgs = _.drop(commandLineArgs);
73 | }
74 | shell.cd(directoryName);
75 | shell.exec(`git remote remove origin`);
76 | execShell([
77 | '-f',
78 | ...plopGen,
79 | WEBPACK_BASE_BABEL,
80 | ..._.drop(commandLineArgs),
81 | ]);
82 | shell.exec(`npm run initialize`);
83 | break;
84 | case 'gt':
85 | execShell(plopGen);
86 | break;
87 | case 'gtcom':
88 | execShell([...plopGen, COMPONENT, ..._.drop(commandLineArgs)]);
89 | break;
90 | case 'gtcon':
91 | execShell([...plopGen, CONTAINER, ..._.drop(commandLineArgs)]);
92 | break;
93 | case 'gtf':
94 | execShell(['-f', ...plopGen, ..._.drop(commandLineArgs)]);
95 | break;
96 | case 'gtcomf':
97 | execShell(['-f', ...plopGen, COMPONENT, ..._.drop(commandLineArgs)]);
98 | break;
99 | case 'gtconf':
100 | execShell(['-f', ...plopGen, CONTAINER, ..._.drop(commandLineArgs)]);
101 | break;
102 | case 'g':
103 | execShell([...plopGen, ..._.drop(commandLineArgs)]);
104 | break;
105 | case 'gcom':
106 | execShell([...plopGen, COMPONENT, ..._.drop(commandLineArgs)]);
107 | break;
108 | case 'gcon':
109 | execShell([...plopGen, CONTAINER, ..._.drop(commandLineArgs)]);
110 | break;
111 | case 'gf':
112 | execShell(['-f', ...plopGen, ..._.drop(commandLineArgs)]);
113 | break;
114 | case 'gcomf':
115 | execShell(['-f', ...plopGen, COMPONENT, ..._.drop(commandLineArgs)]);
116 | break;
117 | case 'gconf':
118 | execShell(['-f', ...plopGen, CONTAINER, ..._.drop(commandLineArgs)]);
119 | break;
120 | case 'gtutil':
121 | execShell(['-f', ...plopGen, TEST_UTIL, ..._.drop(commandLineArgs)]);
122 | break;
123 | case 'gloadable':
124 | execShell(['-f', ...plopGen, LOADABLE, ..._.drop(commandLineArgs)]);
125 | break;
126 | case '--help':
127 | shell.echo(
128 | `Generate tests for existing and new react components\n\n` +
129 | `init: Create a new react application\n` +
130 | `gt: Creating a test for a container or component\n` +
131 | `gtf: Forcefully creating a test for a container or component\n` +
132 | `gtcom: Creating a test for an existing component\n` +
133 | `gtcomf: Forcefully creating a test for an existing component\n` +
134 | `gtcon: Creating a test for an existing container\n` +
135 | `gtconf : Forcefully creating a test for an existing component\n` +
136 | `g: Creating a container or component\n` +
137 | `gf: Forcefully creating a container or component\n` +
138 | `gcom: Creating a component\n` +
139 | `gcomf: Forcefully creating a component\n` +
140 | `gcon: Creating a container\n` +
141 | `gconf: Forcefully creating a container\n` +
142 | `--all: Adding tests for all existing containers or components.\n` +
143 | `gtutil: Create a test util file with some test utility functions.\n` +
144 | `gloadable: Create a loadable utility file that uses lazy and Suspense from React to lazyload your containers.\n` +
145 | `-------\n\n` +
146 | `Creating a test by specifying type, path and name: react-generate gt component src/app Button\n` +
147 | `Creating a test for an existing component by specifying path and name: react-generate gtcon src/app Button\n` +
148 | `Creating a test for an existing container by specifying path and name: react-generate gtcom src/app HomePage\n` +
149 | `Creating a component/container by specifying type, path and name: react-generate g component src/app Button\n` +
150 | `Creating a component by specifying path and name: react-generate gcon src/app Button\n` +
151 | `Creating a container by specifying path and name: react-generate gcom src/app HomePage\n` +
152 | `Generate test for all components in directory: react-generate --all component src/app/components\n` +
153 | `Generate test for all containers in directory: react-generate --all containers src/app/containers`,
154 | );
155 | break;
156 | case '--all':
157 | {
158 | commandLineArgs = _.drop(commandLineArgs);
159 | if (!commandLineArgs[0] || !commandLineArgs[1] || commandLineArgs[2]) {
160 | shell.exec(
161 | `echo Sorry! react-generate --all requires 2 commandLineArgs to be passed. Run react-generate --help for more details`,
162 | );
163 | return;
164 | }
165 | let cwd;
166 | let directories;
167 | switch (commandLineArgs[0]) {
168 | case COMPONENT:
169 | cwd = shell.exec('pwd').stdout;
170 | shell.cd(`./${commandLineArgs[1]}`);
171 | directories = shell.ls();
172 | shell.cd(cwd);
173 | directories.forEach(component => {
174 | if (!_.includes(component, '.')) {
175 | shell.exec(
176 | `react-generate gtcomf ${_.drop(commandLineArgs)} ${component}`,
177 | );
178 | }
179 | });
180 | break;
181 | case CONTAINER:
182 | cwd = shell.exec('pwd').stdout;
183 | shell.cd(`./${commandLineArgs[1]}`);
184 | directories = shell.ls();
185 | shell.cd(cwd);
186 | directories.forEach(component => {
187 | if (!_.includes(component, '.')) {
188 | shell.echo(`Component name: ${component}`);
189 | childProcess.execSync(
190 | `react-generate gtconf ${_.drop(commandLineArgs)} ${component}`,
191 | {
192 | ...stdioInherit,
193 | },
194 | );
195 | }
196 | });
197 | break;
198 | default:
199 | shell.exec(`echo ${commandLineArgs[0]} is not a valid argument`);
200 | }
201 | }
202 | break;
203 | default:
204 | shell.exec(`echo Sorry ${commandLineArgs[0]} is not a valid command`);
205 | break;
206 | }
207 |
--------------------------------------------------------------------------------