27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/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:
--------------------------------------------------------------------------------
1 | /**
2 | * Container Generator
3 | */
4 |
5 | const componentExists = require('../utils/componentExists');
6 |
7 | module.exports = {
8 | description: 'Add a container component',
9 | prompts: [{
10 | type: 'input',
11 | name: 'name',
12 | message: 'What should it be called?',
13 | default: 'Form',
14 | validate: (value) => {
15 | if ((/.+/).test(value)) {
16 | return componentExists(value) ? 'A component or container with this name already exists' : true;
17 | }
18 |
19 | return 'The name is required';
20 | },
21 | }, {
22 | type: 'list',
23 | name: 'component',
24 | message: 'Select a base component:',
25 | default: 'PureComponent',
26 | choices: () => ['PureComponent', 'Component'],
27 | }, {
28 | type: 'confirm',
29 | name: 'wantHeaders',
30 | default: false,
31 | message: 'Do you want headers?',
32 | }, {
33 | type: 'confirm',
34 | name: 'wantActionsAndReducer',
35 | default: true,
36 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?',
37 | }, {
38 | type: 'confirm',
39 | name: 'wantSagas',
40 | default: true,
41 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)',
42 | }, {
43 | type: 'confirm',
44 | name: 'wantMessages',
45 | default: true,
46 | message: 'Do you want i18n messages (i.e. will this component use text)?',
47 | }],
48 | actions: (data) => {
49 | // Generate index.js and index.test.js
50 | const actions = [{
51 | type: 'add',
52 | path: '../../app/containers/{{properCase name}}/index.js',
53 | templateFile: './container/index.js.hbs',
54 | abortOnFail: true,
55 | }, {
56 | type: 'add',
57 | path: '../../app/containers/{{properCase name}}/view.js',
58 | templateFile: './container/view.js.hbs',
59 | abortOnFail: true,
60 | }, {
61 | type: 'add',
62 | path: '../../app/containers/{{properCase name}}/tests/index.test.js',
63 | templateFile: './container/test.js.hbs',
64 | abortOnFail: true,
65 | }];
66 |
67 | // If component wants messages
68 | if (data.wantMessages) {
69 | actions.push({
70 | type: 'add',
71 | path: '../../app/containers/{{properCase name}}/messages.js',
72 | templateFile: './container/messages.js.hbs',
73 | abortOnFail: true,
74 | });
75 | }
76 |
77 | // If they want actions and a reducer, generate actions.js, constants.js,
78 | // reducer.js and the corresponding tests for actions and the reducer
79 | if (data.wantActionsAndReducer) {
80 | // Actions
81 | actions.push({
82 | type: 'add',
83 | path: '../../app/containers/{{properCase name}}/actions.js',
84 | templateFile: './container/actions.js.hbs',
85 | abortOnFail: true,
86 | });
87 | actions.push({
88 | type: 'add',
89 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js',
90 | templateFile: './container/actions.test.js.hbs',
91 | abortOnFail: true,
92 | });
93 |
94 | // Constants
95 | actions.push({
96 | type: 'add',
97 | path: '../../app/containers/{{properCase name}}/constants.js',
98 | templateFile: './container/constants.js.hbs',
99 | abortOnFail: true,
100 | });
101 |
102 | // Selectors
103 | actions.push({
104 | type: 'add',
105 | path: '../../app/containers/{{properCase name}}/selectors.js',
106 | templateFile: './container/selectors.js.hbs',
107 | abortOnFail: true,
108 | });
109 | actions.push({
110 | type: 'add',
111 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js',
112 | templateFile: './container/selectors.test.js.hbs',
113 | abortOnFail: true,
114 | });
115 |
116 | // Reducer
117 | actions.push({
118 | type: 'add',
119 | path: '../../app/containers/{{properCase name}}/reducer.js',
120 | templateFile: './container/reducer.js.hbs',
121 | abortOnFail: true,
122 | });
123 | actions.push({
124 | type: 'add',
125 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js',
126 | templateFile: './container/reducer.test.js.hbs',
127 | abortOnFail: true,
128 | });
129 | }
130 |
131 | // Sagas
132 | if (data.wantSagas) {
133 | actions.push({
134 | type: 'add',
135 | path: '../../app/containers/{{properCase name}}/sagas.js',
136 | templateFile: './container/sagas.js.hbs',
137 | abortOnFail: true,
138 | });
139 | actions.push({
140 | type: 'add',
141 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js',
142 | templateFile: './container/sagas.test.js.hbs',
143 | abortOnFail: true,
144 | });
145 | }
146 |
147 | return actions;
148 | },
149 | };
150 |
--------------------------------------------------------------------------------
/internals/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import { {{{ component }}} } from 'react';
8 | import { connect } from 'react-redux';
9 | import view from './view';
10 | {{#if wantActionsAndReducer}}
11 | import select{{properCase name}} from './selectors';
12 | {{/if}}
13 |
14 | class {{ properCase name }} extends {{{ component }}} {}
15 |
16 | {{ properCase name }}.prototype.render = view;
17 |
18 | {{#if wantActionsAndReducer}}
19 | const mapStateToProps = select{{properCase name}}();
20 | {{/if}}
21 |
22 | function mapDispatchToProps(dispatch) {
23 | return {
24 | dispatch,
25 | };
26 | }
27 |
28 | {{#if wantActionsAndReducer}}
29 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
30 | {{else}}
31 | export default connect(null, mapDispatchToProps)({{ properCase name }});
32 | {{/if}}
33 |
--------------------------------------------------------------------------------
/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/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/view.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }} view
4 | *
5 | */
6 |
7 | import React from 'react';
8 | {{#if wantHeaders}}
9 | import Helmet from 'react-helmet';
10 | {{/if}}
11 | {{#if wantMessages}}
12 | import { FormattedMessage } from 'react-intl';
13 | import messages from './messages';
14 | {{/if}}
15 | {{#if wantCSS}}
16 | import styles from './styles.css';
17 | {{/if}}
18 |
19 | export default function render() {
20 | return (
21 | {{#if wantCSS}}
22 |
23 | {{else}}
24 |
25 | {{/if}}
26 | {{#if wantHeaders}}
27 |
33 | {{/if}}
34 | {{#if wantMessages}}
35 |
36 | {{/if}}
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/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 '{{language}}',
2 |
--------------------------------------------------------------------------------
/internals/generators/language/format-translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{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: /(\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]+',\s[a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\('[a-z]+',\s[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 | const shelljs = require('shelljs');
4 | const animateProgress = require('./helpers/progress');
5 | const chalk = require('chalk');
6 | const addCheckMark = require('./helpers/checkmark');
7 |
8 | const 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/dependencies.js:
--------------------------------------------------------------------------------
1 | // No need to build the DLL in production
2 | if (process.env.NODE_ENV === 'production') {
3 | process.exit(0);
4 | }
5 |
6 | require('shelljs/global');
7 |
8 | const path = require('path');
9 | const fs = require('fs');
10 | const exists = fs.existsSync;
11 | const writeFile = fs.writeFileSync;
12 |
13 | const defaults = require('lodash/defaultsDeep');
14 | const pkg = require(path.join(process.cwd(), 'package.json'));
15 | const config = require('../config');
16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
17 | const outputPath = path.join(process.cwd(), dllConfig.path);
18 | const dllManifestPath = path.join(outputPath, 'package.json');
19 |
20 | /**
21 | * I use node_modules/react-boilerplate-dlls by default just because
22 | * it isn't going to be version controlled and babel wont try to parse it.
23 | */
24 | mkdir('-p', outputPath);
25 |
26 | echo('Building the Webpack DLL...');
27 |
28 | /**
29 | * Create a manifest so npm install doesn't warn us
30 | */
31 | if (!exists(dllManifestPath)) {
32 | writeFile(
33 | dllManifestPath,
34 | JSON.stringify(defaults({
35 | name: 'react-boilerplate-dlls',
36 | private: true,
37 | author: pkg.author,
38 | repository: pkg.repository,
39 | version: pkg.version,
40 | }), null, 2),
41 | 'utf8'
42 | );
43 | }
44 |
45 | // the BUILDING_DLL env var is set to avoid confusing the development environment
46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js');
47 |
--------------------------------------------------------------------------------
/internals/scripts/extract-intl.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * This script will extract the internationalization messages from all components
4 | and package them in the translation json files in the translations file.
5 | */
6 | const fs = require('fs');
7 | const nodeGlob = require('glob');
8 | const transform = require('babel-core').transform;
9 |
10 | const animateProgress = require('./helpers/progress');
11 | const addCheckmark = require('./helpers/checkmark');
12 |
13 | const pkg = require('../../package.json');
14 | const i18n = require('../../app/i18n');
15 | import { DEFAULT_LOCALE } from '../../app/containers/App/constants';
16 |
17 | require('shelljs/global');
18 |
19 | // Glob to match all js files except test files
20 | const FILES_TO_PARSE = 'app/**/!(*.test).js';
21 | const locales = i18n.appLocales;
22 |
23 | const newLine = () => process.stdout.write('\n');
24 |
25 | // Progress Logger
26 | let progress;
27 | const task = (message) => {
28 | progress = animateProgress(message);
29 | process.stdout.write(message);
30 |
31 | return (error) => {
32 | if (error) {
33 | process.stderr.write(error);
34 | }
35 | clearTimeout(progress);
36 | return addCheckmark(() => newLine());
37 | }
38 | }
39 |
40 | // Wrap async functions below into a promise
41 | const glob = (pattern) => new Promise((resolve, reject) => {
42 | nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value)));
43 | });
44 |
45 | const readFile = (fileName) => new Promise((resolve, reject) => {
46 | fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value)));
47 | });
48 |
49 | const writeFile = (fileName, data) => new Promise((resolve, reject) => {
50 | fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value)));
51 | });
52 |
53 | // Store existing translations into memory
54 | const oldLocaleMappings = [];
55 | const localeMappings = [];
56 | // Loop to run once per locale
57 | for (const locale of locales) {
58 | oldLocaleMappings[locale] = {};
59 | localeMappings[locale] = {};
60 | // File to store translation messages into
61 | const translationFileName = `app/translations/${locale}.json`;
62 | try {
63 | // Parse the old translation message JSON files
64 | const messages = JSON.parse(fs.readFileSync(translationFileName));
65 | const messageKeys = Object.keys(messages);
66 | for (const messageKey of messageKeys) {
67 | oldLocaleMappings[locale][messageKey] = messages[messageKey];
68 | }
69 | } catch (error) {
70 | if (error.code !== 'ENOENT') {
71 | process.stderr.write(
72 | `There was an error loading this translation file: ${translationFileName}
73 | \n${error}`
74 | );
75 | }
76 | }
77 | }
78 |
79 | const extractFromFile = async (fileName) => {
80 | try {
81 | const code = await readFile(fileName);
82 | // Use babel plugin to extract instances where react-intl is used
83 | const { metadata: result } = await transform(code, {
84 | presets: pkg.babel.presets,
85 | plugins: [
86 | ['react-intl'],
87 | ],
88 | });
89 | for (const message of result['react-intl'].messages) {
90 | for (const locale of locales) {
91 | const oldLocaleMapping = oldLocaleMappings[locale][message.id];
92 | // Merge old translations into the babel extracted instances where react-intl is used
93 | const newMsg = ( locale === DEFAULT_LOCALE) ? message.defaultMessage : '';
94 | localeMappings[locale][message.id] = (oldLocaleMapping)
95 | ? oldLocaleMapping
96 | : newMsg;
97 | }
98 | }
99 | } catch (error) {
100 | process.stderr.write(`Error transforming file: ${fileName}\n${error}`);
101 | }
102 | };
103 |
104 | (async function main() {
105 | const memoryTaskDone = task('Storing language files in memory');
106 | const files = await glob(FILES_TO_PARSE);
107 | memoryTaskDone()
108 |
109 | const extractTaskDone = task('Run extraction on all files');
110 | // Run extraction on all files that match the glob on line 16
111 | await Promise.all(files.map((fileName) => extractFromFile(fileName)));
112 | extractTaskDone()
113 |
114 | // Make the directory if it doesn't exist, especially for first run
115 | mkdir('-p', 'app/translations');
116 | for (const locale of locales) {
117 | const translationFileName = `app/translations/${locale}.json`;
118 |
119 | try {
120 | const localeTaskDone = task(
121 | `Writing translation messages for ${locale} to: ${translationFileName}`
122 | );
123 |
124 | // Sort the translation JSON file so that git diffing is easier
125 | // Otherwise the translation messages will jump around every time we extract
126 | let messages = {};
127 | Object.keys(localeMappings[locale]).sort().forEach(function(key) {
128 | messages[key] = localeMappings[locale][key];
129 | });
130 |
131 | // Write to file the JSON representation of the translation messages
132 | const prettified = `${JSON.stringify(messages, null, 2)}\n`;
133 |
134 | await writeFile(translationFileName, prettified);
135 |
136 | localeTaskDone();
137 | } catch (error) {
138 | localeTaskDone(
139 | `There was an error saving this translation file: ${translationFileName}
140 | \n${error}`
141 | );
142 | }
143 | }
144 |
145 | process.exit()
146 | }());
147 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/checkmark.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark check symbol
5 | */
6 | function addCheckMark(callback) {
7 | process.stdout.write(chalk.green(' ✓'));
8 | if (callback) callback();
9 | }
10 |
11 | module.exports = addCheckMark;
12 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/progress.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const readline = require('readline');
4 |
5 | /**
6 | * Adds an animated progress indicator
7 | *
8 | * @param {string} message The message to write next to the indicator
9 | * @param {number} amountOfDots The amount of dots you want to animate
10 | */
11 | function animateProgress(message, amountOfDots) {
12 | if (typeof amountOfDots !== 'number') {
13 | amountOfDots = 3;
14 | }
15 |
16 | let i = 0;
17 | return setInterval(function() {
18 | readline.cursorTo(process.stdout, 0);
19 | i = (i + 1) % (amountOfDots + 1);
20 | const dots = new Array(i + 1).join('.');
21 | process.stdout.write(message + dots);
22 | }, 500);
23 | }
24 |
25 | module.exports = animateProgress;
26 |
--------------------------------------------------------------------------------
/internals/scripts/npmcheckversion.js:
--------------------------------------------------------------------------------
1 | const exec = require('child_process').exec;
2 | exec('npm -v', function (err, stdout, stderr) {
3 | if (err) throw err;
4 | if (parseFloat(stdout) < 3) {
5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3');
6 | process.exit(1);
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/internals/scripts/pagespeed.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | process.stdin.resume();
4 | process.stdin.setEncoding('utf8');
5 |
6 | const ngrok = require('ngrok');
7 | const psi = require('psi');
8 | const 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/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 | preprocessors: {
33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key
34 | },
35 |
36 | webpack: webpackConfig,
37 |
38 | // make Webpack bundle generation quiet
39 | webpackMiddleware: {
40 | noInfo: true,
41 | stats: 'errors-only',
42 | },
43 |
44 | customLaunchers: {
45 | ChromeTravis: {
46 | base: 'Chrome',
47 | flags: ['--no-sandbox'],
48 | },
49 | },
50 |
51 | coverageReporter: {
52 | dir: path.join(process.cwd(), 'coverage'),
53 | reporters: [
54 | { type: 'lcov', subdir: 'lcov' },
55 | { type: 'html', subdir: 'html' },
56 | { type: 'text-summary' },
57 | ],
58 | },
59 |
60 | });
61 | };
62 |
--------------------------------------------------------------------------------
/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 .js files under `app`, except app.js, reducers.js, and routes.js.
11 | // This is for code coverage
12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/);
13 | context.keys().forEach(context);
14 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.base.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * COMMON WEBPACK CONFIGURATION
3 | */
4 |
5 | const path = require('path');
6 | const webpack = require('webpack');
7 |
8 | module.exports = (options) => ({
9 | entry: options.entry,
10 | output: Object.assign({ // Compile into js/build.js
11 | path: path.resolve(process.cwd(), 'build'),
12 | publicPath: '/',
13 | }, options.output), // Merge with env dependent settings
14 | module: {
15 | loaders: [{
16 | test: /\.js$/, // Transform all .js files required somewhere with Babel
17 | loader: 'babel',
18 | exclude: /node_modules/,
19 | query: options.babelQuery,
20 | }, {
21 | // Do not transform vendor's CSS with CSS-modules
22 | // The point is that they remain in global scope.
23 | // Since we require these CSS files in our JS or CSS files,
24 | // they will be a part of our compilation either way.
25 | // So, no need for ExtractTextPlugin here.
26 | test: /\.css$/,
27 | include: /node_modules/,
28 | loaders: ['style-loader', 'css-loader'],
29 | }, {
30 | test: /\.(eot|svg|ttf|woff|woff2)$/,
31 | loader: 'file-loader',
32 | }, {
33 | test: /\.(jpg|png|gif)$/,
34 | loaders: [
35 | 'file-loader',
36 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}',
37 | ],
38 | }, {
39 | test: /\.html$/,
40 | loader: 'html-loader',
41 | }, {
42 | test: /\.json$/,
43 | loader: 'json-loader',
44 | }, {
45 | test: /\.(mp4|webm)$/,
46 | loader: 'url-loader?limit=10000',
47 | }],
48 | },
49 | plugins: options.plugins.concat([
50 | new webpack.ProvidePlugin({
51 | // make fetch available
52 | fetch: 'exports?self.fetch!whatwg-fetch',
53 | }),
54 |
55 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
56 | // inside your code for any environment checks; UglifyJS will automatically
57 | // drop any unreachable code.
58 | new webpack.DefinePlugin({
59 | 'process.env': {
60 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
61 | },
62 | }),
63 | new webpack.NamedModulesPlugin(),
64 | ]),
65 | resolve: {
66 | modules: ['app', 'node_modules'],
67 | extensions: [
68 | '.js',
69 | '.jsx',
70 | '.react.js',
71 | ],
72 | mainFields: [
73 | 'browser',
74 | 'jsnext:main',
75 | 'main',
76 | ],
77 | },
78 | devtool: options.devtool,
79 | target: 'web', // Make web variables accessible to webpack, e.g. window
80 | });
81 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dev.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * DEVELOPMENT WEBPACK CONFIGURATION
3 | */
4 |
5 | const path = require('path');
6 | const fs = require('fs');
7 | const webpack = require('webpack');
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 | const logger = require('../../server/logger');
10 | const cheerio = require('cheerio');
11 | const pkg = require(path.resolve(process.cwd(), 'package.json'));
12 | const dllPlugin = pkg.dllPlugin;
13 |
14 | const plugins = [
15 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading
16 | new webpack.NoErrorsPlugin(),
17 | new HtmlWebpackPlugin({
18 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
19 | templateContent: templateContent(), // eslint-disable-line no-use-before-define
20 | }),
21 | ];
22 |
23 | module.exports = require('./webpack.base.babel')({
24 | // Add hot reloading in development
25 | entry: [
26 | 'eventsource-polyfill', // Necessary for hot reloading with IE
27 | 'webpack-hot-middleware/client',
28 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js
29 | ],
30 |
31 | // Don't use hashes in dev mode for better performance
32 | output: {
33 | filename: '[name].js',
34 | chunkFilename: '[name].chunk.js',
35 | },
36 |
37 | // Add development plugins
38 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define
39 |
40 | // Tell babel that we want to hot-reload
41 | babelQuery: {
42 | presets: ['react-hmre'],
43 | },
44 |
45 | // Emit a source map for easier debugging
46 | devtool: 'cheap-module-eval-source-map',
47 | });
48 |
49 | /**
50 | * Select which plugins to use to optimize the bundle's handling of
51 | * third party dependencies.
52 | *
53 | * If there is a dllPlugin key on the project's package.json, the
54 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin
55 | * will be used.
56 | *
57 | */
58 | function dependencyHandlers() {
59 | // Don't do anything during the DLL Build step
60 | if (process.env.BUILDING_DLL) { return []; }
61 |
62 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin
63 | if (!dllPlugin) {
64 | return [
65 | new webpack.optimize.CommonsChunkPlugin({
66 | name: 'vendor',
67 | children: true,
68 | minChunks: 2,
69 | async: true,
70 | }),
71 | ];
72 | }
73 |
74 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls');
75 |
76 | /**
77 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json
78 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude
79 | *
80 | * @see https://github.com/kelsonic/react-redux-material-ui-boilerplate/tree/master/docs
81 | */
82 | if (!dllPlugin.dlls) {
83 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json');
84 |
85 | if (!fs.existsSync(manifestPath)) {
86 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`');
87 | process.exit(0);
88 | }
89 |
90 | return [
91 | new webpack.DllReferencePlugin({
92 | context: process.cwd(),
93 | manifest: require(manifestPath), // eslint-disable-line global-require
94 | }),
95 | ];
96 | }
97 |
98 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them.
99 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`));
100 |
101 | return dllManifests.map((manifestPath) => {
102 | if (!fs.existsSync(path)) {
103 | if (!fs.existsSync(manifestPath)) {
104 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`);
105 | logger.error(`Expected to find it in ${dllPath}`);
106 | logger.error('Please run: npm run build:dll');
107 |
108 | process.exit(0);
109 | }
110 | }
111 |
112 | return new webpack.DllReferencePlugin({
113 | context: process.cwd(),
114 | manifest: require(manifestPath), // eslint-disable-line global-require
115 | });
116 | });
117 | }
118 |
119 | /**
120 | * We dynamically generate the HTML content in development so that the different
121 | * DLL Javascript files are loaded in script tags and available to our application.
122 | */
123 | function templateContent() {
124 | const html = fs.readFileSync(
125 | path.resolve(process.cwd(), 'app/index.html')
126 | ).toString();
127 |
128 | if (!dllPlugin) { return html; }
129 |
130 | const doc = cheerio(html);
131 | const body = doc.find('body');
132 | const dllNames = !dllPlugin.dlls ? ['reactBoilerplateDeps'] : Object.keys(dllPlugin.dlls);
133 |
134 | dllNames.forEach((dllName) => body.append(``));
135 |
136 | return doc.toString();
137 | }
138 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.prod.babel.js:
--------------------------------------------------------------------------------
1 | // Important modules this config uses
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const OfflinePlugin = require('offline-plugin');
6 |
7 | module.exports = require('./webpack.base.babel')({
8 | // In production, we skip all hot-reloading stuff
9 | entry: [
10 | path.join(process.cwd(), 'app/app.js'),
11 | ],
12 |
13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets
14 | output: {
15 | filename: '[name].[chunkhash].js',
16 | chunkFilename: '[name].[chunkhash].chunk.js',
17 | },
18 |
19 | plugins: [
20 | new webpack.optimize.CommonsChunkPlugin({
21 | name: 'vendor',
22 | children: true,
23 | minChunks: 2,
24 | async: true,
25 | }),
26 |
27 | // Minify and optimize the index.html
28 | new HtmlWebpackPlugin({
29 | template: 'app/index.html',
30 | minify: {
31 | removeComments: true,
32 | collapseWhitespace: true,
33 | removeRedundantAttributes: true,
34 | useShortDoctype: true,
35 | removeEmptyAttributes: true,
36 | removeStyleLinkTypeAttributes: true,
37 | keepClosingSlash: true,
38 | minifyJS: true,
39 | minifyCSS: true,
40 | minifyURLs: true,
41 | },
42 | inject: true,
43 | }),
44 |
45 | // Put it in the end to capture all the HtmlWebpackPlugin's
46 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin
47 | new OfflinePlugin({
48 | relativePaths: false,
49 | publicPath: '/',
50 |
51 | // No need to cache .htaccess. See http://mxs.is/googmp,
52 | // this is applied before any match in `caches` section
53 | excludes: ['.htaccess'],
54 |
55 | caches: {
56 | main: [':rest:'],
57 |
58 | // All chunks marked as `additional`, loaded after main section
59 | // and do not prevent SW to install. Change to `optional` if
60 | // do not want them to be preloaded at all (cached only when first loaded)
61 | additional: ['*.chunk.js'],
62 | },
63 |
64 | // Removes warning for about `additional` section usage
65 | safeToUseOptionalCaches: true,
66 |
67 | AppCache: false,
68 | }),
69 | ],
70 | });
71 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.test.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TEST WEBPACK CONFIGURATION
3 | */
4 |
5 | const webpack = require('webpack');
6 | const modules = [
7 | 'app',
8 | 'node_modules',
9 | ];
10 |
11 | module.exports = {
12 | devtool: 'inline-source-map',
13 | module: {
14 | // Some libraries don't like being run through babel.
15 | // If they gripe, put them here.
16 | noParse: [
17 | /node_modules(\\|\/)sinon/,
18 | /node_modules(\\|\/)acorn/,
19 | ],
20 | loaders: [
21 | { test: /\.json$/, loader: 'json-loader' },
22 | { test: /\.css$/, loader: 'null-loader' },
23 |
24 | // sinon.js--aliased for enzyme--expects/requires global vars.
25 | // imports-loader allows for global vars to be injected into the module.
26 | // See https://github.com/webpack/webpack/issues/304
27 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/,
28 | loader: 'imports?define=>false,require=>false',
29 | },
30 | { test: /\.js$/,
31 | loader: 'babel',
32 | exclude: [/node_modules/],
33 | },
34 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i,
35 | loader: 'null-loader',
36 | },
37 | ],
38 | },
39 |
40 | plugins: [
41 |
42 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
43 | // inside your code for any environment checks; UglifyJS will automatically
44 | // drop any unreachable code.
45 | new webpack.DefinePlugin({
46 | 'process.env': {
47 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
48 | },
49 | })],
50 |
51 | // Some node_modules pull in Node-specific dependencies.
52 | // Since we're running in a browser we have to stub them out. See:
53 | // https://webpack.github.io/docs/configuration.html#node
54 | // https://github.com/webpack/node-libs-browser/tree/master/mock
55 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520
56 | node: {
57 | fs: 'empty',
58 | child_process: 'empty',
59 | net: 'empty',
60 | tls: 'empty',
61 | },
62 |
63 | // required for enzyme to work properly
64 | externals: {
65 | jsdom: 'window',
66 | 'react/addons': true,
67 | 'react/lib/ExecutionEnvironment': true,
68 | 'react/lib/ReactContext': 'window',
69 | },
70 | resolve: {
71 | modules,
72 | alias: {
73 | // required for enzyme to work properly
74 | sinon: 'sinon/pkg/sinon',
75 | },
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-material-ui-boilerplate",
3 | "version": "0.0.1",
4 | "description": "A highly scalable, offline-first foundation with material-ui wrapper components",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/kelsonic/react-redux-material-ui-boilerplate.git"
8 | },
9 | "engines": {
10 | "npm": ">=3"
11 | },
12 | "author": "Kelson Adams",
13 | "license": "MIT",
14 | "scripts": {
15 | "analyze": "node ./internals/scripts/analyze.js",
16 | "analyze:clean": "rimraf stats.json",
17 | "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress",
18 | "build:clean": "npm run test:clean && rimraf ./build",
19 | "build:dll": "node ./internals/scripts/dependencies.js",
20 | "clean:all": "npm run analyze:clean && npm run test:clean && npm run build:clean",
21 | "coveralls": "cat ./coverage/lcov/lcov.info | coveralls",
22 | "extract-intl": "babel-node --presets latest,stage-0 -- ./internals/scripts/extract-intl.js",
23 | "generate": "plop --plopfile internals/generators/index.js",
24 | "heroku-postbuild": "npm run build",
25 | "lint": "npm run lint:js",
26 | "lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts",
27 | "lint:js": "npm run lint:eslint -- . ",
28 | "lint:staged": "lint-staged",
29 | "npmcheckversion": "node ./internals/scripts/npmcheckversion.js",
30 | "pagespeed": "node ./internals/scripts/pagespeed.js",
31 | "postinstall": "npm run build:dll",
32 | "postsetup": "npm run build:dll",
33 | "preanalyze": "npm run analyze:clean",
34 | "prebuild": "npm run build:clean",
35 | "preinstall": "npm run npmcheckversion",
36 | "presetup": "npm i chalk shelljs",
37 | "pretest": "npm run test:clean && npm run lint",
38 | "setup": "node ./internals/scripts/setup.js",
39 | "start": "cross-env NODE_ENV=development node server",
40 | "start:prod": "cross-env NODE_ENV=production node server",
41 | "start:production": "npm run build && npm run start:prod",
42 | "start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server",
43 | "test": "cross-env NODE_ENV=test karma start internals/testing/karma.conf.js --single-run",
44 | "test:clean": "rimraf ./coverage",
45 | "test:firefox": "npm run test -- --browsers Firefox",
46 | "test:ie": "npm run test -- --browsers IE",
47 | "test:safari": "npm run test -- --browsers Safari",
48 | "test:watch": "npm run test -- --auto-watch --no-single-run"
49 | },
50 | "lint-staged": {
51 | "*.js": "lint:eslint"
52 | },
53 | "pre-commit": "lint:staged",
54 | "dllPlugin": {
55 | "path": "node_modules/react-boilerplate-dlls",
56 | "exclude": [
57 | "chalk",
58 | "compression",
59 | "cross-env",
60 | "express",
61 | "ip",
62 | "minimist",
63 | "sanitize.css"
64 | ],
65 | "include": [
66 | "core-js",
67 | "lodash",
68 | "eventsource-polyfill"
69 | ]
70 | },
71 | "dependencies": {
72 | "babel-polyfill": "6.16.0",
73 | "chalk": "1.1.3",
74 | "compression": "1.6.2",
75 | "cross-env": "3.1.3",
76 | "express": "4.14.0",
77 | "fontfaceobserver": "2.0.5",
78 | "immutable": "3.8.1",
79 | "intl": "1.2.5",
80 | "invariant": "2.2.1",
81 | "ip": "1.1.3",
82 | "lodash": "4.16.4",
83 | "material-ui": "^0.16.6",
84 | "minimist": "1.2.0",
85 | "react": "^15.3.2",
86 | "react-dom": "^15.3.2",
87 | "react-helmet": "3.1.0",
88 | "react-intl": "2.1.5",
89 | "react-redux": "4.4.5",
90 | "react-router": "3.0.0",
91 | "react-router-redux": "4.0.6",
92 | "react-router-scroll": "0.3.3",
93 | "react-tap-event-plugin": "^2.0.1",
94 | "redux": "3.6.0",
95 | "redux-immutable": "3.0.8",
96 | "redux-saga": "0.12.0",
97 | "reselect": "2.5.4",
98 | "sanitize.css": "4.1.0",
99 | "styled-components": "^1.0.3",
100 | "warning": "3.0.0",
101 | "whatwg-fetch": "1.0.0"
102 | },
103 | "devDependencies": {
104 | "babel-cli": "6.18.0",
105 | "babel-core": "6.18.0",
106 | "babel-eslint": "7.1.0",
107 | "babel-loader": "6.2.7",
108 | "babel-plugin-istanbul": "2.0.3",
109 | "babel-plugin-react-intl": "2.2.0",
110 | "babel-plugin-react-transform": "2.0.2",
111 | "babel-plugin-transform-react-constant-elements": "6.9.1",
112 | "babel-plugin-transform-react-inline-elements": "6.8.0",
113 | "babel-plugin-transform-react-remove-prop-types": "0.2.10",
114 | "babel-preset-latest": "6.16.0",
115 | "babel-preset-react": "6.16.0",
116 | "babel-preset-react-hmre": "1.1.1",
117 | "babel-preset-stage-0": "6.16.0",
118 | "chai": "3.5.0",
119 | "chai-enzyme": "0.5.2",
120 | "cheerio": "0.22.0",
121 | "coveralls": "2.11.14",
122 | "css-loader": "0.25.0",
123 | "enzyme": "2.5.1",
124 | "eslint": "3.9.0",
125 | "eslint-config-airbnb": "12.0.0",
126 | "eslint-config-airbnb-base": "9.0.0",
127 | "eslint-import-resolver-webpack": "0.6.0",
128 | "eslint-plugin-import": "2.0.1",
129 | "eslint-plugin-jsx-a11y": "2.2.3",
130 | "eslint-plugin-react": "6.4.1",
131 | "eslint-plugin-redux-saga": "0.1.5",
132 | "eventsource-polyfill": "0.9.6",
133 | "expect": "1.20.2",
134 | "expect-jsx": "2.6.0",
135 | "exports-loader": "0.6.3",
136 | "file-loader": "0.9.0",
137 | "html-loader": "0.4.4",
138 | "html-webpack-plugin": "2.24.0",
139 | "image-webpack-loader": "2.0.0",
140 | "imports-loader": "0.6.5",
141 | "json-loader": "0.5.4",
142 | "karma": "1.3.0",
143 | "karma-chrome-launcher": "2.0.0",
144 | "karma-coverage": "1.1.1",
145 | "karma-firefox-launcher": "1.0.0",
146 | "karma-ie-launcher": "1.0.0",
147 | "karma-mocha": "1.2.0",
148 | "karma-mocha-reporter": "2.2.0",
149 | "karma-safari-launcher": "1.0.0",
150 | "karma-sourcemap-loader": "0.3.7",
151 | "karma-webpack": "1.8.0",
152 | "lint-staged": "3.2.0",
153 | "mocha": "3.1.2",
154 | "ngrok": "2.2.3",
155 | "null-loader": "0.1.1",
156 | "offline-plugin": "3.4.2",
157 | "plop": "1.5.0",
158 | "pre-commit": "1.1.3",
159 | "psi": "2.0.4",
160 | "react-addons-test-utils": "15.3.2",
161 | "rimraf": "2.5.4",
162 | "shelljs": "0.7.5",
163 | "sinon": "2.0.0-pre",
164 | "style-loader": "0.13.1",
165 | "url-loader": "0.5.7",
166 | "webpack": "2.1.0-beta.25",
167 | "webpack-dev-middleware": "1.8.4",
168 | "webpack-hot-middleware": "2.13.1"
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint consistent-return:0 */
2 |
3 | const express = require('express');
4 | const logger = require('./logger');
5 |
6 | const argv = require('minimist')(process.argv.slice(2));
7 | const setup = require('./middlewares/frontendMiddleware');
8 | const isDev = process.env.NODE_ENV !== 'production';
9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false;
10 | const resolve = require('path').resolve;
11 | const app = express();
12 |
13 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here
14 | // app.use('/api', myApi);
15 |
16 | // In production we need to pass these values in instead of relying on webpack
17 | setup(app, {
18 | outputPath: resolve(process.cwd(), 'build'),
19 | publicPath: '/',
20 | });
21 |
22 | // get the intended port number, use port 3000 if not provided
23 | const port = argv.port || process.env.PORT || 3000;
24 |
25 | // Start your app.
26 | app.listen(port, (err) => {
27 | if (err) {
28 | return logger.error(err.message);
29 | }
30 |
31 | // Connect to ngrok in dev mode
32 | if (ngrok) {
33 | ngrok.connect(port, (innerErr, url) => {
34 | if (innerErr) {
35 | return logger.error(innerErr);
36 | }
37 |
38 | logger.appStarted(port, url);
39 | });
40 | } else {
41 | logger.appStarted(port);
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/server/logger.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const chalk = require('chalk');
4 | const ip = require('ip');
5 |
6 | const divider = chalk.gray('\n-----------------------------------');
7 |
8 | /**
9 | * Logger middleware, you can customize it to make messages more personal
10 | */
11 | const logger = {
12 |
13 | // Called whenever there's an error on the server we want to print
14 | error: (err) => {
15 | console.error(chalk.red(err));
16 | },
17 |
18 | // Called when express.js app starts on given port w/o errors
19 | appStarted: (port, tunnelStarted) => {
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 | module.exports = logger;
38 |
--------------------------------------------------------------------------------
/server/middlewares/frontendMiddleware.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | const express = require('express');
3 | const path = require('path');
4 | const compression = require('compression');
5 | const pkg = require(path.resolve(process.cwd(), 'package.json'));
6 |
7 | // Dev middleware
8 | const addDevMiddlewares = (app, webpackConfig) => {
9 | const webpack = require('webpack');
10 | const webpackDevMiddleware = require('webpack-dev-middleware');
11 | const webpackHotMiddleware = require('webpack-hot-middleware');
12 | const compiler = webpack(webpackConfig);
13 | const middleware = webpackDevMiddleware(compiler, {
14 | noInfo: true,
15 | publicPath: webpackConfig.output.publicPath,
16 | silent: true,
17 | stats: 'errors-only',
18 | });
19 |
20 | app.use(middleware);
21 | app.use(webpackHotMiddleware(compiler));
22 |
23 | // Since webpackDevMiddleware uses memory-fs internally to store build
24 | // artifacts, we use it instead
25 | const fs = middleware.fileSystem;
26 |
27 | if (pkg.dllPlugin) {
28 | app.get(/\.dll\.js$/, (req, res) => {
29 | const filename = req.path.replace(/^\//, '');
30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename));
31 | });
32 | }
33 |
34 | app.get('*', (req, res) => {
35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
36 | if (err) {
37 | res.sendStatus(404);
38 | } else {
39 | res.send(file.toString());
40 | }
41 | });
42 | });
43 | };
44 |
45 | // Production middlewares
46 | const addProdMiddlewares = (app, options) => {
47 | const publicPath = options.publicPath || '/';
48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
49 |
50 | // compression middleware compresses your server responses which makes them
51 | // smaller (applies also to assets). You can read more about that technique
52 | // and other good practices on official Express.js docs http://mxs.is/googmy
53 | app.use(compression());
54 | app.use(publicPath, express.static(outputPath));
55 |
56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
57 | };
58 |
59 | /**
60 | * Front-end middleware
61 | */
62 | module.exports = (app, options) => {
63 | const isProd = process.env.NODE_ENV === 'production';
64 |
65 | if (isProd) {
66 | addProdMiddlewares(app, options);
67 | } else {
68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
69 | addDevMiddlewares(app, webpackConfig);
70 | }
71 |
72 | return app;
73 | };
74 |
--------------------------------------------------------------------------------