29 | );
30 | }
31 |
32 | export default {{ properCase name }};
33 |
--------------------------------------------------------------------------------
/internals/generators/component/styles.css.hbs:
--------------------------------------------------------------------------------
1 | .{{ camelCase name }} { /* stylelint-disable */
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/internals/generators/component/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import {{ properCase name }} from '../index';
2 |
3 | import expect from 'expect';
4 | // import { shallow } from 'enzyme';
5 | // import React from 'react';
6 |
7 | describe('<{{ properCase name }} />', () => {
8 | it('Expect to have unit tests specified', () => {
9 | expect(true).toEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/internals/generators/container/actions.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} actions
4 | *
5 | */
6 |
7 | import {
8 | DEFAULT_ACTION,
9 | } from './constants';
10 |
11 | export function defaultAction() {
12 | return {
13 | type: DEFAULT_ACTION,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/internals/generators/container/actions.test.js.hbs:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('{{ properCase name }} actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/internals/generators/container/constants.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} constants
4 | *
5 | */
6 |
7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION';
8 |
--------------------------------------------------------------------------------
/internals/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import { connect } from 'react-redux';
9 | {{#if wantHeaders}}
10 | import Helmet from 'react-helmet';
11 | {{/if}}
12 | {{#if wantActionsAndReducer}}
13 | import select{{properCase name}} from './selectors';
14 | {{/if}}
15 | {{#if wantMessages}}
16 | import { FormattedMessage } from 'react-intl';
17 | import messages from './messages';
18 | {{/if}}
19 | {{#if wantCSS}}
20 | import styles from './styles.css';
21 | {{/if}}
22 |
23 | export class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function
24 | render() {
25 | return (
26 | {{#if wantCSS}}
27 |
28 | {{else}}
29 |
30 | {{/if}}
31 | {{#if wantHeaders}}
32 |
38 | {{/if}}
39 | {{#if wantMessages}}
40 |
41 | {{/if}}
42 |
43 | );
44 | }
45 | }
46 |
47 | {{#if wantActionsAndReducer}}
48 | const mapStateToProps = select{{properCase name}}();
49 | {{/if}}
50 |
51 | function mapDispatchToProps(dispatch) {
52 | return {
53 | dispatch,
54 | };
55 | }
56 |
57 | {{#if wantActionsAndReducer}}
58 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
59 | {{else}}
60 | export default connect(null, mapDispatchToProps)({{ properCase name }});
61 | {{/if}}
62 |
--------------------------------------------------------------------------------
/internals/generators/container/messages.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | * {{properCase name }} Messages
3 | *
4 | * This contains all the text for the {{properCase name }} component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'app.containers.{{properCase name }}.header',
11 | defaultMessage: 'This is {{properCase name}} container !',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/internals/generators/container/reducer.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 | import {
9 | DEFAULT_ACTION,
10 | } from './constants';
11 |
12 | const initialState = fromJS({});
13 |
14 | function {{ camelCase name }}Reducer(state = initialState, action) {
15 | switch (action.type) {
16 | case DEFAULT_ACTION:
17 | return state;
18 | default:
19 | return state;
20 | }
21 | }
22 |
23 | export default {{ camelCase name }}Reducer;
24 |
--------------------------------------------------------------------------------
/internals/generators/container/reducer.test.js.hbs:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import {{ camelCase name }}Reducer from '../reducer';
3 | import { fromJS } from 'immutable';
4 |
5 | describe('{{ camelCase name }}Reducer', () => {
6 | it('returns the initial state', () => {
7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({}));
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/internals/generators/container/sagas.js.hbs:
--------------------------------------------------------------------------------
1 | // import { take, call, put, select } from 'redux-saga/effects';
2 |
3 | // Individual exports for testing
4 | export function* defaultSaga() {
5 | return;
6 | }
7 |
8 | // All sagas to be loaded
9 | export default [
10 | defaultSaga,
11 | ];
12 |
--------------------------------------------------------------------------------
/internals/generators/container/sagas.test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | import expect from 'expect';
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../sagas';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | expect(true).toEqual(false);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/internals/generators/container/selectors.js.hbs:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | /**
4 | * Direct selector to the {{ camelCase name }} state domain
5 | */
6 | const select{{ properCase name }}Domain = () => (state) => state.get('{{ camelCase name }}');
7 |
8 | /**
9 | * Other specific selectors
10 | */
11 |
12 |
13 | /**
14 | * Default selector used by {{ properCase name }}
15 | */
16 |
17 | const select{{ properCase name }} = () => createSelector(
18 | select{{ properCase name }}Domain(),
19 | (substate) => substate.toJS()
20 | );
21 |
22 | export default select{{ properCase name }};
23 | export {
24 | select{{ properCase name }}Domain,
25 | };
26 |
--------------------------------------------------------------------------------
/internals/generators/container/selectors.test.js.hbs:
--------------------------------------------------------------------------------
1 | // import { select{{ properCase name }}Domain } from '../selectors';
2 | // import { fromJS } from 'immutable';
3 | import expect from 'expect';
4 |
5 | // const selector = select{{ properCase name}}Domain();
6 |
7 | describe('select{{ properCase name }}Domain', () => {
8 | it('Expect to have unit tests specified', () => {
9 | expect('Test case').toEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/internals/generators/container/styles.css.hbs:
--------------------------------------------------------------------------------
1 | .{{ camelCase name }} { /* stylelint-disable */
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/internals/generators/container/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import {{ properCase name }} from '../index';
2 |
3 | import expect from 'expect';
4 | // import { shallow } from 'enzyme';
5 | // import React from 'react';
6 |
7 | describe('<{{ properCase name }} />', () => {
8 | it('Expect to have unit tests specified', () => {
9 | expect(true).toEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/internals/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 componentGenerator = require('./component/index.js');
9 | const containerGenerator = require('./container/index.js');
10 | const routeGenerator = require('./route/index.js');
11 | const languageGenerator = require('./language/index.js');
12 |
13 | module.exports = (plop) => {
14 | plop.setGenerator('component', componentGenerator);
15 | plop.setGenerator('container', containerGenerator);
16 | plop.setGenerator('route', routeGenerator);
17 | plop.setGenerator('language', languageGenerator);
18 | plop.addHelper('directory', (comp) => {
19 | try {
20 | fs.accessSync(`app/containers/${comp}`, fs.F_OK);
21 | return `containers/${comp}`;
22 | } catch (e) {
23 | return `components/${comp}`;
24 | }
25 | });
26 | plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
27 | };
28 |
--------------------------------------------------------------------------------
/internals/generators/language/add-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $1addLocaleData({{language}}LocaleData);
2 |
--------------------------------------------------------------------------------
/internals/generators/language/app-locale.hbs:
--------------------------------------------------------------------------------
1 | $1
2 | '{{language}}',
3 |
--------------------------------------------------------------------------------
/internals/generators/language/format-translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1 {{language}}: formatTranslationMessages({{language}}TranslationMessages),
2 |
--------------------------------------------------------------------------------
/internals/generators/language/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Language Generator
3 | */
4 | const exec = require('child_process').exec;
5 |
6 | module.exports = {
7 | description: 'Add a language',
8 | prompts: [{
9 | type: 'input',
10 | name: 'language',
11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?',
12 | default: 'fr',
13 | validate: (value) => {
14 | if ((/.+/).test(value) && value.length === 2) {
15 | return true;
16 | }
17 |
18 | return '2 character language specifier is required';
19 | },
20 | }],
21 |
22 | actions: () => {
23 | const actions = [];
24 | actions.push({
25 | type: 'modify',
26 | path: '../../app/i18n.js',
27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g,
28 | templateFile: './language/intl-locale-data.hbs',
29 | });
30 | actions.push({
31 | type: 'modify',
32 | path: '../../app/i18n.js',
33 | pattern: /([\n\s'[a-z]+',)(?!.*[\n\s'[a-z]+',)/g,
34 | templateFile: './language/app-locale.hbs',
35 | });
36 | actions.push({
37 | type: 'modify',
38 | path: '../../app/i18n.js',
39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g,
40 | templateFile: './language/translation-messages.hbs',
41 | });
42 | actions.push({
43 | type: 'modify',
44 | path: '../../app/i18n.js',
45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g,
46 | templateFile: './language/add-locale-data.hbs',
47 | });
48 | actions.push({
49 | type: 'modify',
50 | path: '../../app/i18n.js',
51 | pattern: /([a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),)/g,
52 | templateFile: './language/format-translation-messages.hbs',
53 | });
54 | actions.push({
55 | type: 'add',
56 | path: '../../app/translations/{{language}}.json',
57 | templateFile: './language/translations-json.hbs',
58 | abortOnFail: true,
59 | });
60 | actions.push({
61 | type: 'modify',
62 | path: '../../app/app.js',
63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g,
64 | templateFile: './language/polyfill-intl-locale.hbs',
65 | });
66 | actions.push(
67 | () => {
68 | const cmd = 'npm run extract-intl';
69 | exec(cmd, (err, result, stderr) => {
70 | if (err || stderr) {
71 | throw err || stderr;
72 | }
73 | process.stdout.write(result);
74 | });
75 | }
76 | );
77 |
78 | return actions;
79 | },
80 | };
81 |
--------------------------------------------------------------------------------
/internals/generators/language/intl-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}';
2 |
--------------------------------------------------------------------------------
/internals/generators/language/polyfill-intl-locale.hbs:
--------------------------------------------------------------------------------
1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'),
2 |
--------------------------------------------------------------------------------
/internals/generators/language/translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json';
2 |
--------------------------------------------------------------------------------
/internals/generators/language/translations-json.hbs:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/internals/generators/route/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Route Generator
3 | */
4 | const fs = require('fs');
5 | const componentExists = require('../utils/componentExists');
6 |
7 | function reducerExists(comp) {
8 | try {
9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK);
10 | return true;
11 | } catch (e) {
12 | return false;
13 | }
14 | }
15 |
16 | function sagasExists(comp) {
17 | try {
18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK);
19 | return true;
20 | } catch (e) {
21 | return false;
22 | }
23 | }
24 |
25 | function trimTemplateFile(template) {
26 | // Loads the template file and trims the whitespace and then returns the content as a string.
27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, '');
28 | }
29 |
30 | module.exports = {
31 | description: 'Add a route',
32 | prompts: [{
33 | type: 'input',
34 | name: 'component',
35 | message: 'Which component should the route show?',
36 | validate: (value) => {
37 | if ((/.+/).test(value)) {
38 | return componentExists(value) ? true : `"${value}" doesn't exist.`;
39 | }
40 |
41 | return 'The path is required';
42 | },
43 | }, {
44 | type: 'input',
45 | name: 'path',
46 | message: 'Enter the path of the route.',
47 | default: '/about',
48 | validate: (value) => {
49 | if ((/.+/).test(value)) {
50 | return true;
51 | }
52 |
53 | return 'path is required';
54 | },
55 | }],
56 |
57 | // Add the route to the routes.js file above the error route
58 | // TODO smarter route adding
59 | actions: (data) => {
60 | const actions = [];
61 | if (reducerExists(data.component)) {
62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign
63 | actions.push({
64 | type: 'modify',
65 | path: '../../app/routes.js',
66 | pattern: /(\s{\n\s{0,}path: '\*',)/g,
67 | template: trimTemplateFile('routeWithReducer.hbs'),
68 | });
69 | } else {
70 | actions.push({
71 | type: 'modify',
72 | path: '../../app/routes.js',
73 | pattern: /(\s{\n\s{0,}path: '\*',)/g,
74 | template: trimTemplateFile('route.hbs'),
75 | });
76 | }
77 |
78 | return actions;
79 | },
80 | };
81 |
--------------------------------------------------------------------------------
/internals/generators/route/route.hbs:
--------------------------------------------------------------------------------
1 | {
2 | path: '{{ path }}',
3 | name: '{{ camelCase component }}',
4 | getComponent(location, cb) {
5 | System.import('{{{directory (properCase component)}}}')
6 | .then(loadModule(cb))
7 | .catch(errorLoading);
8 | },
9 | },$1
10 |
--------------------------------------------------------------------------------
/internals/generators/route/routeWithReducer.hbs:
--------------------------------------------------------------------------------
1 | {
2 | path: '{{ path }}',
3 | name: '{{ camelCase component }}',
4 | getComponent(nextState, cb) {
5 | const importModules = Promise.all([
6 | System.import('containers/{{ properCase component }}/reducer'),
7 | {{#if useSagas}}
8 | System.import('containers/{{ properCase component }}/sagas'),
9 | {{/if}}
10 | System.import('containers/{{ properCase component }}'),
11 | ]);
12 |
13 | const renderRoute = loadModule(cb);
14 |
15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => {
16 | injectReducer('{{ camelCase component }}', reducer.default);
17 | {{#if useSagas}}
18 | injectSagas(sagas.default);
19 | {{/if}}
20 | renderRoute(component);
21 | });
22 |
23 | importModules.catch(errorLoading);
24 | },
25 | },$1
26 |
--------------------------------------------------------------------------------
/internals/generators/utils/componentExists.js:
--------------------------------------------------------------------------------
1 | /**
2 | * componentExists
3 | *
4 | * Check whether the given component exist in either the components or containers directory
5 | */
6 |
7 | const fs = require('fs');
8 | const pageComponents = fs.readdirSync('app/components');
9 | const pageContainers = fs.readdirSync('app/containers');
10 | const components = pageComponents.concat(pageContainers);
11 |
12 | function componentExists(comp) {
13 | return components.indexOf(comp) >= 0;
14 | }
15 |
16 | module.exports = componentExists;
17 |
--------------------------------------------------------------------------------
/internals/scripts/analyze.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var shelljs = require('shelljs');
4 | var animateProgress = require('./helpers/progress');
5 | var chalk = require('chalk');
6 | var addCheckMark = require('./helpers/checkmark');
7 |
8 | var progress = animateProgress('Generating stats');
9 |
10 | // Generate stats.json file with webpack
11 | shelljs.exec(
12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json',
13 | addCheckMark.bind(null, callback) // Output a checkmark on completion
14 | );
15 |
16 | // Called after webpack has finished generating the stats.json file
17 | function callback() {
18 | clearInterval(progress);
19 | process.stdout.write(
20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' +
21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n')
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/internals/scripts/clean.js:
--------------------------------------------------------------------------------
1 | require('shelljs/global');
2 |
3 | /**
4 | * Adds mark check symbol
5 | */
6 | function addCheckMark(callback) {
7 | process.stdout.write(' ✓');
8 | callback();
9 | }
10 |
11 | if (!which('git')) {
12 | echo('Sorry, this script requires git');
13 | exit(1);
14 | }
15 |
16 | if (!test('-e', 'internals/templates')) {
17 | echo('The example is deleted already.');
18 | exit(1);
19 | }
20 |
21 | process.stdout.write('Cleanup started...');
22 |
23 | // Cleanup components folder
24 | rm('-rf', 'app/components/*');
25 |
26 | // Cleanup containers folder
27 | rm('-rf', 'app/containers/*');
28 | mkdir('-p', 'app/containers/App');
29 | mkdir('-p', 'app/containers/NotFoundPage');
30 | mkdir('-p', 'app/containers/HomePage');
31 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js');
32 | cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js');
33 | cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js');
34 | cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js');
35 | cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js');
36 |
37 | // Handle Translations
38 | mkdir('-p', 'app/translations');
39 | cp('internals/templates/translations/en.json',
40 | 'app/translations/en.json');
41 |
42 | // move i18n file
43 | cp('internals/templates/i18n.js',
44 | 'app/i18n.js');
45 |
46 | // Copy LanguageProvider
47 | mkdir('-p', 'app/containers/LanguageProvider');
48 | mkdir('-p', 'app/containers/LanguageProvider/tests');
49 | cp('internals/templates/languageProvider/actions.js',
50 | 'app/containers/LanguageProvider/actions.js');
51 | cp('internals/templates/languageProvider/constants.js',
52 | 'app/containers/LanguageProvider/constants.js');
53 | cp('internals/templates/languageProvider/languageProvider.js',
54 | 'app/containers/LanguageProvider/index.js');
55 | cp('internals/templates/languageProvider/reducer.js',
56 | 'app/containers/LanguageProvider/reducer.js');
57 | cp('internals/templates/languageProvider/selectors.js',
58 | 'app/containers/LanguageProvider/selectors.js');
59 | cp('internals/templates/styles.css', 'app/containers/App/styles.css');
60 |
61 | // Copy selectors
62 | mkdir('app/containers/App/tests');
63 | cp('internals/templates/selectors.js',
64 | 'app/containers/App/selectors.js');
65 | cp('internals/templates/selectors.test.js',
66 | 'app/containers/App/tests/selectors.test.js');
67 |
68 | // Utils
69 | rm('-rf', 'app/utils');
70 | mkdir('app/utils');
71 | mkdir('app/utils/tests');
72 | cp('internals/templates/asyncInjectors.js',
73 | 'app/utils/asyncInjectors.js');
74 | cp('internals/templates/asyncInjectors.test.js',
75 | 'app/utils/tests/asyncInjectors.test.js');
76 |
77 | // Replace the files in the root app/ folder
78 | cp('internals/templates/app.js', 'app/app.js');
79 | cp('internals/templates/index.html', 'app/index.html');
80 | cp('internals/templates/reducers.js', 'app/reducers.js');
81 | cp('internals/templates/routes.js', 'app/routes.js');
82 | cp('internals/templates/store.js', 'app/store.js');
83 | cp('internals/templates/store.test.js', 'app/tests/store.test.js');
84 |
85 | // Remove the templates folder
86 | rm('-rf', 'internals/templates');
87 |
88 | process.stdout.write(' ✓');
89 |
90 | // Commit the changes
91 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) {
92 | echo('\nError: Git commit failed');
93 | exit(1);
94 | }
95 |
96 | echo('\nCleanup done. Happy Coding!!!');
97 |
--------------------------------------------------------------------------------
/internals/scripts/dependencies.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable*/
2 |
3 | // No need to build the DLL in production
4 | if (process.env.NODE_ENV === 'production') {
5 | process.exit(0)
6 | }
7 |
8 | require('shelljs/global')
9 |
10 | const path = require('path')
11 | const fs = require('fs')
12 | const exists = fs.existsSync
13 | const writeFile = fs.writeFileSync
14 |
15 | const defaults = require('lodash/defaultsDeep')
16 | const pkg = require(path.join(process.cwd(), 'package.json'))
17 | const config = require('../config')
18 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults)
19 | const outputPath = path.join(process.cwd(), dllConfig.path)
20 | const dllManifestPath = path.join(outputPath, 'package.json')
21 |
22 | /**
23 | * I use node_modules/react-boilerplate-dlls by default just because
24 | * it isn't going to be version controlled and babel wont try to parse it.
25 | */
26 | mkdir('-p', outputPath)
27 |
28 | echo('Building the Webpack DLL...')
29 |
30 | /**
31 | * Create a manifest so npm install doesn't warn us
32 | */
33 | if (!exists(dllManifestPath)) {
34 | writeFile(
35 | dllManifestPath,
36 | JSON.stringify(defaults({
37 | name: 'react-boilerplate-dlls',
38 | private: true,
39 | author: pkg.author,
40 | repository: pkg.repository,
41 | version: pkg.version
42 | }), null, 2),
43 |
44 | 'utf8'
45 | )
46 | }
47 |
48 | // the BUILDING_DLL env var is set to avoid confusing the development environment
49 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js')
50 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/checkmark.js:
--------------------------------------------------------------------------------
1 | var chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark check symbol
5 | */
6 | function addCheckMark(callback) {
7 | process.stdout.write(chalk.green(' ✓'));
8 | callback();
9 | }
10 |
11 | module.exports = addCheckMark;
12 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/progress.js:
--------------------------------------------------------------------------------
1 | var readline = require('readline');
2 |
3 | /**
4 | * Adds an animated progress indicator
5 | *
6 | * @param {string} message The message to write next to the indicator
7 | * @param {number} amountOfDots The amount of dots you want to animate
8 | */
9 | function animateProgress(message, amountOfDots) {
10 | if (typeof amountOfDots !== 'number') {
11 | amountOfDots = 3;
12 | }
13 |
14 | var i = 0;
15 | return setInterval(function () {
16 | readline.cursorTo(process.stdout, 0);
17 | i = (i + 1) % (amountOfDots + 1);
18 | var dots = new Array(i + 1).join('.');
19 | process.stdout.write(message + dots);
20 | }, 500);
21 | }
22 |
23 | module.exports = animateProgress;
24 |
--------------------------------------------------------------------------------
/internals/scripts/npmcheckversion.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var exec = require('child_process').exec;
3 | exec('npm -v', function (err, stdout, stderr) {
4 | if (err) throw err;
5 | if (parseFloat(stdout) < 3) {
6 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3');
7 | process.exit(1);
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/internals/scripts/pagespeed.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | process.stdin.resume();
4 | process.stdin.setEncoding('utf8');
5 |
6 | var ngrok = require('ngrok');
7 | var psi = require('psi');
8 | var chalk = require('chalk');
9 |
10 | log('\nStarting ngrok tunnel');
11 |
12 | startTunnel(runPsi);
13 |
14 | function runPsi(url) {
15 | log('\nStarting PageSpeed Insights');
16 | psi.output(url).then(function (err) {
17 | process.exit(0);
18 | });
19 | }
20 |
21 | function startTunnel(cb) {
22 | ngrok.connect(3000, function (err, url) {
23 | if (err) {
24 | log(chalk.red('\nERROR\n' + err));
25 | process.exit(0);
26 | }
27 |
28 | log('\nServing tunnel from: ' + chalk.magenta(url));
29 | cb(url);
30 | });
31 | }
32 |
33 | function log(string) {
34 | process.stdout.write(string);
35 | }
36 |
--------------------------------------------------------------------------------
/internals/scripts/setup.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var shell = require('shelljs');
4 | var exec = require('child_process').exec;
5 | var path = require('path');
6 | var fs = require('fs');
7 | var animateProgress = require('./helpers/progress');
8 | var addCheckMark = require('./helpers/checkmark');
9 | var readline = require('readline');
10 |
11 | process.stdin.resume();
12 | process.stdin.setEncoding('utf8');
13 |
14 | process.stdout.write('\n');
15 | var interval = animateProgress('Cleaning old repository');
16 | process.stdout.write('Cleaning old repository');
17 |
18 | cleanRepo(function () {
19 | clearInterval(interval);
20 | process.stdout.write('\nInstalling dependencies... (This might take a while)');
21 | setTimeout(function () {
22 | readline.cursorTo(process.stdout, 0);
23 | interval = animateProgress('Installing dependencies');
24 | }, 500);
25 |
26 | process.stdout.write('Installing dependencies');
27 | installDeps(function (error) {
28 | clearInterval(interval);
29 | if (error) {
30 | process.stdout.write(error);
31 | }
32 |
33 | deleteFileInCurrentDir('setup.js', function () {
34 | process.stdout.write('\n');
35 | interval = animateProgress('Initialising new repository');
36 | process.stdout.write('Initialising new repository');
37 | initGit(function () {
38 | clearInterval(interval);
39 | process.stdout.write('\nDone!');
40 | process.exit(0);
41 | });
42 | });
43 | });
44 | });
45 |
46 | /**
47 | * Deletes the .git folder in dir
48 | */
49 | function cleanRepo(callback) {
50 | shell.rm('-rf', '.git/');
51 | addCheckMark(callback);
52 | }
53 |
54 | /**
55 | * Initializes git again
56 | */
57 | function initGit(callback) {
58 | exec('git init && git add . && git commit -m "Initial commit"', addCheckMark.bind(null, callback));
59 | }
60 |
61 | /**
62 | * Deletes a file in the current directory
63 | */
64 | function deleteFileInCurrentDir(file, callback) {
65 | fs.unlink(path.join(__dirname, file), callback);
66 | }
67 |
68 | /**
69 | * Installs dependencies
70 | */
71 | function installDeps(callback) {
72 | exec('npm install', addCheckMark.bind(null, callback));
73 | }
74 |
--------------------------------------------------------------------------------
/internals/templates/appContainer.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * App.react.js
4 | *
5 | * This component is the skeleton around the actual pages, and should only
6 | * contain code that should be seen on all pages. (e.g. navigation bar)
7 | *
8 | * NOTE: while this component should technically be a stateless functional
9 | * component (SFC), hot reloading does not currently support SFCs. If hot
10 | * reloading is not a necessity for you then you can refactor it and remove
11 | * the linting exception.
12 | */
13 |
14 | import React from 'react';
15 |
16 | import styles from './styles.css';
17 |
18 | export default class App extends React.Component { // eslint-disable-line react/prefer-stateless-function
19 |
20 | static propTypes = {
21 | children: React.PropTypes.node,
22 | };
23 |
24 | render() {
25 | return (
26 |
27 | {React.Children.toArray(this.props.children)}
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/internals/templates/asyncInjectors.js:
--------------------------------------------------------------------------------
1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
2 | import invariant from 'invariant';
3 | import warning from 'warning';
4 | import createReducer from './reducers';
5 |
6 | /**
7 | * Validate the shape of redux store
8 | */
9 | export function checkStore(store) {
10 | const shape = {
11 | dispatch: isFunction,
12 | subscribe: isFunction,
13 | getState: isFunction,
14 | replaceReducer: isFunction,
15 | runSaga: isFunction,
16 | asyncReducers: isObject,
17 | };
18 | invariant(
19 | conformsTo(store, shape),
20 | '(app/utils...) asyncInjectors: Expected a valid redux store'
21 | );
22 | }
23 |
24 | /**
25 | * Inject an asynchronously loaded reducer
26 | */
27 | export function injectAsyncReducer(store, isValid) {
28 | return function injectReducer(name, asyncReducer) {
29 | if (!isValid) checkStore(store);
30 |
31 | invariant(
32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer),
33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
34 | );
35 |
36 | if (Reflect.has(store.asyncReducers, name)) return;
37 |
38 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
39 | store.replaceReducer(createReducer(store.asyncReducers));
40 | };
41 | }
42 |
43 | /**
44 | * Inject an asynchronously loaded saga
45 | */
46 | export function injectAsyncSagas(store, isValid) {
47 | return function injectSagas(sagas) {
48 | if (!isValid) checkStore(store);
49 |
50 | invariant(
51 | Array.isArray(sagas),
52 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
53 | );
54 |
55 | warning(
56 | !isEmpty(sagas),
57 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
58 | );
59 |
60 | sagas.map(store.runSaga);
61 | };
62 | }
63 |
64 | /**
65 | * Helper for creating injectors
66 | */
67 | export function getAsyncInjectors(store) {
68 | checkStore(store);
69 |
70 | return {
71 | injectReducer: injectAsyncReducer(store, true),
72 | injectSagas: injectAsyncSagas(store, true),
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/internals/templates/homePage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * HomePage
3 | *
4 | * This is the first thing users see of our App, at the '/' route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a neccessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 |
14 | export default class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
15 |
16 | render() {
17 | return (
18 |
This is the Homepage!
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internals/templates/homePage/homePage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * HomePage
3 | *
4 | * This is the first thing users see of our App, at the '/' route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a necessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 | import { FormattedMessage } from 'react-intl';
14 | import messages from './messages';
15 |
16 | export default class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/internals/templates/homePage/messages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * HomePage Messages
3 | *
4 | * This contains all the text for the HomePage component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'app.components.HomePage.header',
11 | defaultMessage: 'This is HomePage components !',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/internals/templates/i18n.js:
--------------------------------------------------------------------------------
1 | /**
2 | * i18n.js
3 | *
4 | * This will setup the i18n language files and locale data for your app.
5 | *
6 | */
7 | import { addLocaleData } from 'react-intl';
8 |
9 | import enLocaleData from 'react-intl/locale-data/en';
10 |
11 | export const appLocales = [
12 | 'en',
13 | ];
14 |
15 | import enTranslationMessages from './translations/en.json';
16 |
17 | addLocaleData(enLocaleData);
18 |
19 | const formatTranslationMessages = (messages) => {
20 | const formattedMessages = {};
21 | for (const message of messages) {
22 | formattedMessages[message.id] = message.message || message.defaultMessage;
23 | }
24 |
25 | return formattedMessages;
26 | };
27 |
28 | export const translationMessages = {
29 | en: formatTranslationMessages(enTranslationMessages),
30 | };
31 |
--------------------------------------------------------------------------------
/internals/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
React.js Boilerplate
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/internals/templates/languageProvider/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider actions
4 | *
5 | */
6 |
7 | import {
8 | CHANGE_LOCALE,
9 | } from './constants';
10 |
11 | export function changeLocale(languageLocale) {
12 | return {
13 | type: CHANGE_LOCALE,
14 | locale: languageLocale,
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/internals/templates/languageProvider/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider constants
4 | *
5 | */
6 |
7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
8 |
--------------------------------------------------------------------------------
/internals/templates/languageProvider/languageProvider.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider
4 | *
5 | * this component connects the redux state language locale to the
6 | * IntlProvider component and i18n messages (loaded from `app/translations`)
7 | */
8 |
9 | import React from 'react';
10 | import { connect } from 'react-redux';
11 | import { createSelector } from 'reselect';
12 | import { IntlProvider } from 'react-intl';
13 | import { selectLocale } from './selectors';
14 |
15 | export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function
16 | render() {
17 | return (
18 |
19 | {React.Children.only(this.props.children)}
20 |
21 | );
22 | }
23 | }
24 |
25 | LanguageProvider.propTypes = {
26 | locale: React.PropTypes.string,
27 | messages: React.PropTypes.object,
28 | children: React.PropTypes.element.isRequired,
29 | };
30 |
31 |
32 | const mapStateToProps = createSelector(
33 | selectLocale(),
34 | (locale) => ({ locale })
35 | );
36 |
37 | function mapDispatchToProps(dispatch) {
38 | return {
39 | dispatch,
40 | };
41 | }
42 |
43 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider);
44 |
--------------------------------------------------------------------------------
/internals/templates/languageProvider/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 | import {
9 | CHANGE_LOCALE,
10 | } from './constants';
11 |
12 | const initialState = fromJS({
13 | locale: 'en',
14 | });
15 |
16 | function languageProviderReducer(state = initialState, action) {
17 | switch (action.type) {
18 | case CHANGE_LOCALE:
19 | return state
20 | .set('locale', action.locale);
21 | default:
22 | return state;
23 | }
24 | }
25 |
26 | export default languageProviderReducer;
27 |
--------------------------------------------------------------------------------
/internals/templates/languageProvider/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | /**
4 | * Direct selector to the languageToggle state domain
5 | */
6 | const selectLanguage = () => (state) => state.get('language');
7 |
8 | /**
9 | * Select the language locale
10 | */
11 |
12 | const selectLocale = () => createSelector(
13 | selectLanguage(),
14 | (languageState) => languageState.get('locale')
15 | );
16 |
17 | export {
18 | selectLanguage,
19 | selectLocale,
20 | };
21 |
--------------------------------------------------------------------------------
/internals/templates/notFoundPage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a neccessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 |
14 | export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function
15 |
16 | render() {
17 | return (
18 |
Page Not Found
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internals/templates/notFoundPage/messages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * NotFoundPage Messages
3 | *
4 | * This contains all the text for the NotFoundPage component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'app.components.NotFoundPage.header',
11 | defaultMessage: 'This is NotFoundPage component !',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/internals/templates/notFoundPage/notFoundPage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a necessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 | import { FormattedMessage } from 'react-intl';
14 | import messages from './messages';
15 |
16 | export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/internals/templates/reducers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Combine all reducers in this file and export the combined reducers.
3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable.
4 | */
5 |
6 | import { combineReducers } from 'redux-immutable';
7 | import { fromJS } from 'immutable';
8 | import { LOCATION_CHANGE } from 'react-router-redux';
9 | import languageProviderReducer from 'containers/LanguageProvider/reducer';
10 |
11 | /*
12 | * routeReducer
13 | *
14 | * The reducer merges route location changes into our immutable state.
15 | * The change is necessitated by moving to react-router-redux@4
16 | *
17 | */
18 |
19 | // Initial routing state
20 | const routeInitialState = fromJS({
21 | locationBeforeTransitions: null,
22 | });
23 |
24 | /**
25 | * Merge route into the global application state
26 | */
27 | function routeReducer(state = routeInitialState, action) {
28 | switch (action.type) {
29 | /* istanbul ignore next */
30 | case LOCATION_CHANGE:
31 | return state.merge({
32 | locationBeforeTransitions: action.payload,
33 | });
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | /**
40 | * Creates the main reducer with the asynchronously loaded ones
41 | */
42 | export default function createReducer(asyncReducers) {
43 | return combineReducers({
44 | route: routeReducer,
45 | language: languageProviderReducer,
46 | ...asyncReducers,
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/internals/templates/routes.js:
--------------------------------------------------------------------------------
1 | // These are the pages you can go to.
2 | // They are all wrapped in the App component, which should contain the navbar etc
3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information
4 | // about the code splitting business
5 | import { getAsyncInjectors } from 'utils/asyncInjectors';
6 |
7 | const errorLoading = (err) => {
8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console
9 | };
10 |
11 | const loadModule = (cb) => (componentModule) => {
12 | cb(null, componentModule.default);
13 | };
14 |
15 | export default function createRoutes(store) {
16 | // Create reusable async injectors using getAsyncInjectors factory
17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars
18 |
19 | return [
20 | {
21 | path: '/',
22 | name: 'home',
23 | getComponent(nextState, cb) {
24 | const importModules = Promise.all([
25 | System.import('containers/HomePage'),
26 | ]);
27 |
28 | const renderRoute = loadModule(cb);
29 |
30 | importModules.then(([component]) => {
31 | renderRoute(component);
32 | });
33 |
34 | importModules.catch(errorLoading);
35 | },
36 | }, {
37 | path: '*',
38 | name: 'notfound',
39 | getComponent(nextState, cb) {
40 | System.import('containers/NotFoundPage')
41 | .then(loadModule(cb))
42 | .catch(errorLoading);
43 | },
44 | },
45 | ];
46 | }
47 |
--------------------------------------------------------------------------------
/internals/templates/selectors.js:
--------------------------------------------------------------------------------
1 | // selectLocationState expects a plain JS object for the routing state
2 | const selectLocationState = () => {
3 | let prevRoutingState;
4 | let prevRoutingStateJS;
5 |
6 | return (state) => {
7 | const routingState = state.get('route'); // or state.route
8 |
9 | if (!routingState.equals(prevRoutingState)) {
10 | prevRoutingState = routingState;
11 | prevRoutingStateJS = routingState.toJS();
12 | }
13 |
14 | return prevRoutingStateJS;
15 | };
16 | };
17 |
18 | export {
19 | selectLocationState,
20 | };
21 |
--------------------------------------------------------------------------------
/internals/templates/selectors.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import expect from 'expect';
3 |
4 | import { selectLocationState } from 'containers/App/selectors';
5 |
6 | describe('selectLocationState', () => {
7 | it('should select the route as a plain JS object', () => {
8 | const route = fromJS({
9 | locationBeforeTransitions: null,
10 | });
11 | const mockedState = fromJS({
12 | route,
13 | });
14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS());
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/internals/templates/store.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create the store with asynchronously loaded reducers
3 | */
4 |
5 | import { createStore, applyMiddleware, compose } from 'redux';
6 | import { fromJS } from 'immutable';
7 | import { routerMiddleware } from 'react-router-redux';
8 | import createSagaMiddleware from 'redux-saga';
9 | import createReducer from './reducers';
10 |
11 | const sagaMiddleware = createSagaMiddleware();
12 | const devtools = window.devToolsExtension || (() => (noop) => noop);
13 |
14 | export default function configureStore(initialState = {}, history) {
15 | // Create the store with two middlewares
16 | // 1. sagaMiddleware: Makes redux-sagas work
17 | // 2. routerMiddleware: Syncs the location/URL path to the state
18 | const middlewares = [
19 | sagaMiddleware,
20 | routerMiddleware(history),
21 | ];
22 |
23 | const enhancers = [
24 | applyMiddleware(...middlewares),
25 | devtools(),
26 | ];
27 |
28 | const store = createStore(
29 | createReducer(),
30 | fromJS(initialState),
31 | compose(...enhancers)
32 | );
33 |
34 | // Create hook for async sagas
35 | store.runSaga = sagaMiddleware.run;
36 |
37 | // Make reducers hot reloadable, see http://mxs.is/googmo
38 | /* istanbul ignore next */
39 | if (module.hot) {
40 | System.import('./reducers').then((reducerModule) => {
41 | const createReducers = reducerModule.default;
42 | const nextReducers = createReducers(store.asyncReducers);
43 |
44 | store.replaceReducer(nextReducers);
45 | });
46 | }
47 |
48 | // Initialize it with no other reducers
49 | store.asyncReducers = {};
50 | return store;
51 | }
52 |
--------------------------------------------------------------------------------
/internals/templates/store.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test store addons
3 | */
4 |
5 | import expect from 'expect';
6 | import configureStore from '../store'; // eslint-disable-line
7 | import { browserHistory } from 'react-router';
8 |
9 | describe('configureStore', () => {
10 | let store;
11 |
12 | before(() => {
13 | store = configureStore({}, browserHistory);
14 | });
15 |
16 | describe('asyncReducers', () => {
17 | it('should contain an object for async reducers', () => {
18 | expect(typeof store.asyncReducers).toEqual('object');
19 | });
20 | });
21 |
22 | describe('runSaga', () => {
23 | it('should contain a hook for `sagaMiddleware.run`', () => {
24 | expect(typeof store.runSaga).toEqual('function');
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/internals/templates/styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * styles.css
3 | *
4 | * App container styles
5 | */
6 |
7 | .container {
8 | display: block;
9 | }
10 |
--------------------------------------------------------------------------------
/internals/templates/translations/en.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/internals/testing/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpackConfig = require('../webpack/webpack.test.babel');
2 | const argv = require('minimist')(process.argv.slice(2));
3 | const path = require('path');
4 |
5 | module.exports = (config) => {
6 | config.set({
7 | frameworks: ['mocha'],
8 | reporters: ['coverage', 'mocha'],
9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary
10 | ? ['ChromeTravis']
11 | : process.env.APPVEYOR
12 | ? ['IE'] : ['Chrome'],
13 |
14 | autoWatch: false,
15 | singleRun: true,
16 |
17 | client: {
18 | mocha: {
19 | grep: argv.grep,
20 | },
21 | },
22 |
23 | files: [
24 | {
25 | pattern: './test-bundler.js',
26 | watched: false,
27 | served: true,
28 | included: true,
29 | },
30 | ],
31 |
32 | exclude: [
33 | '**/*.d.ts',
34 | ],
35 |
36 | preprocessors: {
37 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key
38 | },
39 |
40 | webpack: webpackConfig,
41 |
42 | // make Webpack bundle generation quiet
43 | webpackMiddleware: {
44 | noInfo: true,
45 | stats: 'errors-only',
46 | },
47 |
48 | customLaunchers: {
49 | ChromeTravis: {
50 | base: 'Chrome',
51 | flags: ['--no-sandbox'],
52 | },
53 | },
54 |
55 | coverageReporter: {
56 | dir: path.join(process.cwd(), 'coverage'),
57 | reporters: [
58 | { type: 'lcov', subdir: 'lcov' },
59 | { type: 'html', subdir: 'html' },
60 | { type: 'text-summary' },
61 | ],
62 | },
63 |
64 | });
65 | };
66 |
--------------------------------------------------------------------------------
/internals/testing/test-bundler.js:
--------------------------------------------------------------------------------
1 | // needed for regenerator-runtime
2 | // (ES7 generator support is required by redux-saga)
3 | import 'babel-polyfill';
4 |
5 | // If we need to use Chai, we'll have already chaiEnzyme loaded
6 | import chai from 'chai';
7 | import chaiEnzyme from 'chai-enzyme';
8 | chai.use(chaiEnzyme());
9 |
10 | // Include all .(j|t)sx? files under `app`, except app.ts, reducers.ts, and routes.ts.
11 | // This is for code coverage
12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)|.*\.d).)*\.(j|t)sx?$/);
13 | context.keys().forEach(context);
14 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WEBPACK DLL GENERATOR
3 | *
4 | * This profile is used to cache webpack's module
5 | * contexts for external library and framework type
6 | * dependencies which will usually not change often enough
7 | * to warrant building them from scratch every time we use
8 | * the webpack process.
9 | */
10 |
11 | const { join } = require('path');
12 | const defaults = require('lodash/defaultsDeep');
13 | const webpack = require('webpack');
14 | const pkg = require(join(process.cwd(), 'package.json'));
15 | const dllPlugin = require('../config').dllPlugin;
16 |
17 | if (!pkg.dllPlugin) { process.exit(0); }
18 |
19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
20 | const outputPath = join(process.cwd(), dllConfig.path);
21 |
22 | module.exports = require('./webpack.base.babel')({
23 | context: process.cwd(),
24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
25 | devtool: 'eval',
26 | output: {
27 | filename: '[name].dll.js',
28 | path: outputPath,
29 | library: '[name]',
30 | },
31 | plugins: [
32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new
33 | ],
34 | });
35 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint consistent-return:0 */
2 |
3 | import * as Express from 'express';
4 | import logger from 'server/logger';
5 |
6 | const argv = require('minimist')(process.argv.slice(2)) as {
7 | port?: number;
8 | tunnel?: boolean;
9 | };
10 | import setup from 'server/middlewares/frontendMiddleware';
11 | const isDev = process.env.NODE_ENV !== 'production';
12 | interface INgrok { connect(port: number, callback: (err: Error, url: string) => void); }
13 | const ngrok: INgrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false;
14 | import { resolve } from 'path';
15 | const app = Express();
16 |
17 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here
18 | // app.use('/api', myApi);
19 |
20 | // In production we need to pass these values in instead of relying on webpack
21 | setup(app, {
22 | outputPath: resolve(process.cwd(), 'build'),
23 | publicPath: '/',
24 | });
25 |
26 | // get the intended port number, use port 3000 if not provided
27 | const port = argv.port || process.env.PORT || 3000;
28 |
29 | // Start your app.
30 | app.listen(port, (err) => {
31 | if (err) {
32 | return logger.error(err.message);
33 | }
34 |
35 | // Connect to ngrok in dev mode
36 | if (ngrok) {
37 | ngrok.connect(port, (innerErr, url) => {
38 | if (innerErr) {
39 | return logger.error(innerErr);
40 | }
41 |
42 | logger.appStarted(port, url);
43 | });
44 | } else {
45 | logger.appStarted(port);
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/server/logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import * as chalk from 'chalk';
4 | import * as ip from 'ip';
5 |
6 | const divider = chalk.gray('\n-----------------------------------');
7 |
8 | /**
9 | * Logger middleware, you can customize it to make messages more personal
10 | */
11 | export class Logger {
12 |
13 | // Called whenever there's an error on the server we want to print
14 | public static error(err) {
15 | console.error(chalk.red(err));
16 | }
17 |
18 | // Called when express.js app starts on given port w/o errors
19 | public static appStarted(port: number, tunnelStarted?: string) {
20 | console.log(`Server started ${chalk.green('✓')}`);
21 |
22 | // If the tunnel started, log that and the URL it's available at
23 | if (tunnelStarted) {
24 | console.log(`Tunnel initialised ${chalk.green('✓')}`);
25 | }
26 |
27 | console.log(`
28 | ${chalk.bold('Access URLs:')}${divider}
29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)}
30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) +
31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider}
32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}
33 | `);
34 | }
35 | }
36 |
37 | export default Logger;
38 |
--------------------------------------------------------------------------------
/server/middlewares/frontendMiddleware.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import * as Express from 'express';
3 | import * as Path from 'path';
4 | import * as Compression from 'compression';
5 | import * as Webpack from 'webpack';
6 | import * as WebpackDevMiddleware from 'webpack-dev-middleware';
7 | import * as WebpackHotMiddleware from 'webpack-hot-middleware';
8 | const Pkg = require(Path.resolve(process.cwd(), 'package.json'));
9 |
10 | export interface IOptions {
11 | publicPath: string;
12 | outputPath: string;
13 | }
14 |
15 | // Dev middleware
16 | const addDevMiddlewares = (app: Express.Application, webpackConfig: Webpack.Configuration) => {
17 | const compiler = Webpack(webpackConfig);
18 | const middleware = WebpackDevMiddleware(compiler, {
19 | noInfo: true,
20 | publicPath: webpackConfig.output.publicPath,
21 | silent: true,
22 | stats: 'errors-only',
23 | });
24 |
25 | app.use(middleware);
26 | app.use(WebpackHotMiddleware(compiler));
27 |
28 | // Since webpackDevMiddleware uses memory-fs internally to store build
29 | // artifacts, we use it instead
30 | const fs = middleware.fileSystem;
31 |
32 | if (Pkg.dllPlugin) {
33 | app.get(/\.dll\.js$/, (req, res) => {
34 | const filename = req.path.replace(/^\//, '');
35 | res.sendFile(Path.join(process.cwd(), Pkg.dllPlugin.path, filename));
36 | });
37 | }
38 |
39 | app.get('*', (req, res) => {
40 | fs.readFile(Path.join(compiler.options.output.path, 'index.html'), (err, file) => {
41 | if (err) {
42 | res.sendStatus(404);
43 | } else {
44 | res.send(file.toString());
45 | }
46 | });
47 | });
48 | };
49 |
50 | // Production middlewares
51 | const addProdMiddlewares = (app: Express.Application, options: IOptions) => {
52 | const publicPath = options.publicPath || '/';
53 | const outputPath = options.outputPath || Path.resolve(process.cwd(), 'build');
54 |
55 | // compression middleware compresses your server responses which makes them
56 | // smaller (applies also to assets). You can read more about that technique
57 | // and other good practices on official Express.js docs http://mxs.is/googmy
58 | app.use(Compression());
59 | app.use(publicPath, Express.static(outputPath));
60 |
61 | app.get('*', (req, res) => res.sendFile(Path.resolve(outputPath, 'index.html')));
62 | };
63 |
64 | /**
65 | * Front-end middleware
66 | */
67 | export default (app: Express.Application, options: IOptions) => {
68 | const isProd = process.env.NODE_ENV === 'production';
69 |
70 | if (isProd) {
71 | addProdMiddlewares(app, options);
72 | } else {
73 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel') as Webpack.Configuration;
74 | addDevMiddlewares(app, webpackConfig);
75 | }
76 |
77 | return app;
78 | };
79 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "target": "es6",
5 | "module": "commonjs",
6 | "noImplicitAny": false,
7 | "sourceMap": true,
8 | "preserveConstEnums": true,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "jsx": "preserve",
12 | "lib": ["dom","es5","es6","es7","es2017"],
13 | "allowSyntheticDefaultImports": true,
14 | "typeRoots": ["node_modules/@types"],
15 | "baseUrl": ".",
16 | "paths": {
17 | "*": [
18 | "*"
19 | ]
20 | }
21 | },
22 | "exclude": [
23 | "node_modules"
24 | ],
25 | "awesomeTypescriptLoaderOptions": {
26 | "useBabel": true,
27 | "forkChecker": true,
28 | "useCache": false
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react"],
3 | "rules": {
4 | "indent": [ true, "spaces" ],
5 | "quotemark": [ true, "single", "jsx-double" ],
6 | "no-var-requires": false,
7 | "ordered-imports": false,
8 | "no-unused-variable": [false, "react"],
9 | "member-ordering": [false],
10 | "object-literal-sort-keys": false,
11 | "no-shadowed-variable": false,
12 | "no-console": [false],
13 | "max-line-length": [true, 180],
14 | "no-consecutive-blank-lines": [false],
15 | "no-string-literal": false,
16 | "jsx-no-multiline-js": false,
17 | "jsx-boolean-value": false
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/typings/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "note": "dummy file to suppress errors on intellij products"
3 | }
4 |
--------------------------------------------------------------------------------
/typings/untyped/index.d.ts:
--------------------------------------------------------------------------------
1 | // TODO: remove when offline-plugin@5.0.0 is released
2 | declare module 'offline-plugin/runtime' {
3 | export interface InstallOptions {
4 | /**
5 | * Event called exactly once when ServiceWorker or AppCache is installed.
6 | * Can be useful to display "App is ready for offline usage" message.
7 | *
8 | * @memberOf InstallOptions
9 | */
10 | onInstalled?: () => void;
11 |
12 | /**
13 | * Not supported for AppCache.
14 | * Event called when update is found and browsers started updating process.
15 | * At this moment, some assets are downloading.
16 | *
17 | * @memberOf InstallOptions
18 | */
19 | onUpdating?: () => void;
20 |
21 | /**
22 | * Event called when onUpdating phase finished.
23 | * All required assets are downloaded at this moment and are ready to be updated.
24 | * Call runtime.applyUpdate() to apply update.
25 | *
26 | * @memberOf InstallOptions
27 | */
28 | onUpdateReady?: () => void;
29 |
30 | /**
31 | * Event called when upUpdating phase failed by some reason.
32 | * Nothing is downloaded at this moment and current update process
33 | * in your code should be canceled or ignored.
34 | *
35 | * @memberOf InstallOptions
36 | */
37 | onUpdateFailed?: () => void;
38 |
39 | /**
40 | * Event called when update is applied,
41 | * either by calling runtime.applyUpdate() or
42 | * some other way by a browser itself.
43 | *
44 | * @memberOf InstallOptions
45 | */
46 | onUpdated?: () => void;
47 | }
48 |
49 | /**
50 | * Starts installation flow for ServiceWorker/AppCache
51 | * it's safe and must be called each time your page loads
52 | * (i.e. do not wrap it into any conditions).
53 | *
54 | * @param {InstallOptions} [options] The install options.
55 | *
56 | * @memberOf RuntimeStatic
57 | */
58 | export function install(options?: InstallOptions): void;
59 |
60 | /**
61 | * Used to apply update for existing installation.
62 | * See InstallOptions.
63 | *
64 | * @memberOf RuntimeStatic
65 | */
66 | export function applyUpdate(): void;
67 |
68 | /**
69 | * Performs check for updates of new ServiceWorker/AppCache.
70 | *
71 | * @memberOf RuntimeStatic
72 | */
73 | export function update(): void;
74 | }
75 |
76 | declare module "warning" {
77 | const warning: (boolean, string) => void;
78 | export = warning;
79 | }
80 |
81 | declare module Expect {
82 | import EventHandler = React.EventHandler;
83 | import SyntheticEvent = React.SyntheticEvent;
84 | interface Spy extends EventHandler
> {
85 | (): EventHandler>;
86 | }
87 | }
88 |
89 | declare module _ {
90 | export interface LoDashStatic {
91 | conformsTo: (a: any, b: any) => boolean;
92 | }
93 | }
94 |
95 |
96 | declare module "react-router-scroll" {
97 | export const useScroll: Function; //TODO: needs type definition
98 | }
99 |
100 | interface Window {
101 | Intl: any;
102 | swUpdate: boolean;
103 | }
104 |
--------------------------------------------------------------------------------