├── .babelrc
├── .editorconfig
├── .env.example
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.txt
├── README.md
├── ansible
├── deploy
│ ├── after-symlink-tasks.yml
│ └── deploy.yml
├── rollback
│ ├── after-symlink-tasks.yml
│ └── rollback.yml
└── setup
│ ├── boto
│ └── boto.cfg
│ ├── nginx
│ └── sites_available.conf
│ └── setup.yml
├── contributing.md
├── features_flags.js
├── gulpfile.babel.js
├── configs
│ ├── .eslintrc
│ ├── .stylelintrc
│ ├── config.js
│ ├── karma.conf.coverage.js
│ ├── karma.conf.integration.js
│ ├── karma.conf.unit.js
│ ├── wdio.conf.js
│ ├── webpack.config.js
│ └── webpack.config.production.js
├── index.js
└── tasks
│ ├── gulp_acceptance_test.js
│ ├── gulp_babel_server.js
│ ├── gulp_build_dev_server.js
│ ├── gulp_ci_test.js
│ ├── gulp_clean.js
│ ├── gulp_client_coverage_test.js
│ ├── gulp_client_integration_test.js
│ ├── gulp_client_unit_test.js
│ ├── gulp_copy_assets.js
│ ├── gulp_create_upload_artifact.js
│ ├── gulp_dev.js
│ ├── gulp_dev_server.js
│ ├── gulp_js_doc.js
│ ├── gulp_js_lint.js
│ ├── gulp_prod.js
│ ├── gulp_sass_dev.js
│ ├── gulp_sass_doc.js
│ ├── gulp_sass_prod.js
│ ├── gulp_server_integration_test.js
│ ├── gulp_server_unit_test.js
│ ├── gulp_style_lint.js
│ ├── gulp_svg_min.js
│ ├── gulp_test.js
│ ├── gulp_upload_static_files.js
│ ├── gulp_vendor_js.js
│ ├── gulp_webpack_dev_server.js
│ └── gulp_webpack_prod.js
├── logs
└── .gitkeep
├── npm-shrinkwrap.json
├── package.json
├── pm2
└── pm2.json5
├── src
├── assets
│ ├── fonts
│ │ ├── amble-light-webfont.woff
│ │ ├── amble-light-webfont.woff2
│ │ ├── amble-regular-webfont.woff
│ │ └── amble-regular-webfont.woff2
│ ├── icons
│ │ ├── android-chrome-144x144.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-256x256.png
│ │ ├── android-chrome-36x36.png
│ │ ├── android-chrome-384x384.png
│ │ ├── android-chrome-48x48.png
│ │ ├── android-chrome-512x512.png
│ │ ├── android-chrome-72x72.png
│ │ ├── android-chrome-96x96.png
│ │ ├── apple-touch-icon-114x114.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-144x144.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-57x57.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-72x72.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── manifest.json
│ │ ├── mstile-144x144.png
│ │ ├── mstile-150x150.png
│ │ ├── mstile-310x150.png
│ │ ├── mstile-310x310.png
│ │ ├── mstile-70x70.png
│ │ └── safari-pinned-tab.svg
│ └── images
│ │ └── logo.gif
├── client
│ ├── index.js
│ ├── services
│ │ └── logger_service.js
│ ├── styles
│ │ ├── framework
│ │ │ ├── _foundation_settings.scss
│ │ │ ├── _mixins.scss
│ │ │ └── _typography.scss
│ │ ├── main.scss
│ │ └── variables
│ │ │ └── _colors.scss
│ └── utils
│ │ ├── font_loader_util.js
│ │ ├── initializer_util.js
│ │ └── third_party_js_util.js
├── models
│ ├── repo_detail.js
│ └── search.js
├── react_router
│ └── react_router.js
├── redux
│ ├── action_creators
│ │ ├── about
│ │ │ └── about_page_action_creators.js
│ │ ├── index
│ │ │ └── index_page_action_creators.js
│ │ ├── initial_load_action_creator.js
│ │ ├── not_found_status_action_creator.js
│ │ ├── repo_detail
│ │ │ ├── repo_detail_action_creators.js
│ │ │ └── repo_detail_page_action_creators.js
│ │ └── search
│ │ │ ├── search_action_creators.js
│ │ │ └── search_page_action_creators.js
│ ├── actions
│ │ ├── about
│ │ │ └── async_about_page_actions.js
│ │ ├── index
│ │ │ └── async_index_page_actions.js
│ │ ├── repo_detail
│ │ │ ├── async_repo_detail_actions.js
│ │ │ └── async_repo_detail_page_actions.js
│ │ └── search
│ │ │ ├── async_search_actions.js
│ │ │ └── async_search_page_actions.js
│ ├── reducers
│ │ ├── about
│ │ │ └── about_page_reducer.js
│ │ ├── config_reducer.js
│ │ ├── index.js
│ │ ├── index
│ │ │ └── index_page_reducer.js
│ │ ├── meta_reducer.js
│ │ ├── repo_detail
│ │ │ ├── repo_detail_page_reducer.js
│ │ │ └── repo_detail_reducer.js
│ │ ├── search
│ │ │ ├── search_page_reducer.js
│ │ │ └── search_results_reducer.js
│ │ └── status_reducer.js
│ └── store
│ │ └── store.js
├── server
│ ├── config
│ │ ├── base.js
│ │ ├── development.js
│ │ ├── index.js
│ │ ├── production.js
│ │ └── test.js
│ ├── controllers
│ │ ├── get_ping_controller.js
│ │ ├── get_robots_txt_controller.js
│ │ └── post_log_controller.js
│ ├── index.js
│ ├── middleware
│ │ ├── error_middleware.js
│ │ └── react_router_middleware.js
│ ├── routes
│ │ ├── get_ping_route.js
│ │ ├── get_robots_txt.js
│ │ └── post_log_route.js
│ ├── services
│ │ ├── express_service.js
│ │ ├── logger_service.js
│ │ ├── redis_service.js
│ │ └── router_service.js
│ └── utils
│ │ ├── error_response_util.js
│ │ ├── graceful_exit_util.js
│ │ ├── initializer_util.js
│ │ ├── install_sourcemap_util.js
│ │ ├── load_env_var_util.js
│ │ ├── react_raf_util.js
│ │ ├── router_util.js
│ │ └── uncaught_exception_util.js
├── services
│ └── logger_service.js
├── utils
│ ├── custom_errors_util.js
│ ├── custom_validations_util.js
│ └── seo_util
│ │ ├── about_seo_util.js
│ │ ├── base_seo_util.js
│ │ ├── index_seo_util.js
│ │ ├── repo_detail_seo_util.js
│ │ └── search_seo_util.js
└── views
│ ├── components
│ ├── config
│ │ └── config.js
│ ├── footer
│ │ ├── _footer.scss
│ │ └── footer.js
│ ├── head
│ │ ├── head.js
│ │ ├── noscript.js
│ │ ├── script.js
│ │ └── style.js
│ ├── header
│ │ ├── _header.scss
│ │ └── header.js
│ └── nav
│ │ ├── _nav.scss
│ │ └── nav.js
│ └── containers
│ ├── layouts
│ ├── _layout.scss
│ └── layout.js
│ ├── pages
│ ├── about_page
│ │ ├── _about.scss
│ │ ├── about_page.js
│ │ ├── about_page_data_fetch.js
│ │ └── about_route.js
│ ├── error_page
│ │ ├── _error.scss
│ │ └── error_page.js
│ ├── index_page
│ │ ├── _index.scss
│ │ ├── index_page.js
│ │ ├── index_page_data_fetch.js
│ │ └── index_page_route.js
│ ├── not_found_page
│ │ ├── _not_found.scss
│ │ ├── not_found_page.js
│ │ ├── not_found_route.js
│ │ └── not_found_state_manager.js
│ ├── repo_detail_page
│ │ ├── _repo_detail.scss
│ │ ├── repo_detail_page.js
│ │ ├── repo_detail_page_data_fetch.js
│ │ └── repo_detail_route.js
│ └── search_results_page
│ │ ├── _search_results.scss
│ │ ├── search_results_data_fetch.js
│ │ ├── search_results_page.js
│ │ └── search_results_route.js
│ └── root_container.js
└── test
├── acceptance
└── spec
│ └── title_test.js
├── client
├── integration
│ └── example
│ │ └── index.js
└── unit
│ └── utils
│ ├── font_loader_util.js
│ ├── initializer_util.js
│ └── third_party_js_util.js
├── server
├── integration
│ ├── controllers
│ │ ├── get_ping_controller_test.js
│ │ └── post_log_controller_test.js
│ └── middleware
│ │ └── react_router_middleware.js
├── unit
│ ├── config
│ │ └── config_test.js
│ └── services
│ │ └── logger_service_test.js
└── utils
│ ├── app_util.js
│ ├── babel_register.js
│ ├── enzyme_initializer.js
│ ├── initializer_util.js
│ ├── test_initializer_util.js
│ └── test_teardown_util.js
└── shared
├── unit
└── views
│ ├── components
│ ├── config
│ │ └── config.js
│ ├── footer
│ │ └── footer.js
│ ├── head
│ │ ├── head.js
│ │ ├── noscript.js
│ │ ├── script.js
│ │ └── style.js
│ ├── header
│ │ └── header.js
│ └── nav
│ │ └── nav.js
│ └── containers
│ ├── about_page
│ └── about_page_test.js
│ ├── index_page
│ └── index_page_test.js
│ ├── layouts
│ └── layout_test.js
│ ├── not_found_page
│ └── not_found_page_test.js
│ ├── repo_detail_page
│ └── repo_detail_page_test.js
│ └── search_results_page
│ └── search_page_test.js
└── utils
└── enzyme_adapter_util.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "node": "current"
8 | }
9 | }
10 | ]
11 | ],
12 | "env": {
13 | "coverage": {
14 | "presets": [
15 | [
16 | "env",
17 | {
18 | "targets": {
19 | "node": "current"
20 | }
21 | }
22 | ],
23 | "react"
24 | ],
25 | "plugins": [
26 | "dynamic-import-node",
27 | "syntax-dynamic-import",
28 | "transform-react-jsx-source",
29 | [
30 | "istanbul",
31 | {
32 | "exclude": [
33 | "test/**/*.js"
34 | ]
35 | }
36 | ]
37 | ]
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env with your credentials
2 |
3 | # REDIS_PORT=6379
4 | # REDIS_HOST=127.0.0.1
5 |
6 | # https://console.aws.amazon.com/iam/home?region=us-east-1#users
7 | # AWS_ACCESS_KEY_ID=YOUR_KEY_HERE
8 | # AWS_SECRET_ACCESS_KEY=YOUR_KEY_HERE
9 | # FACEBOOK_APP_ID=id
10 | # FACEBOOK_APP_SECRET=secret
11 |
12 | # CI=JENKINS
13 | # GIT_COMMIT=000f7d5675a4f88e280d9691cee9e943d815a6c0
14 | # BUILD_NUMBER=development
15 | # STATIC_URL=//abcdefg.cloudfront.net/
16 | # STATIC_VENDOR_URL=vendor
17 | # STATIC_BUNDLE_URL=bundle
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env_dev
3 | .env_staging
4 | .env_prod
5 | .idea
6 | .DS_Store
7 | .nyc_output
8 | *.log
9 | coverage
10 | dist
11 | docs
12 | dump.rdb
13 | karma_coverage
14 | node_modules
15 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintrc
2 | .gitignore
3 | .npmignore
4 | contributing.md
5 | .idea
6 | .DS_Store
7 |
8 | docs
9 | logs
10 | test
11 | nginx
12 | pm2
13 | docker
14 | src
15 |
16 | gulpfile.babel.js
17 | yuidoc.json
18 | README.md
19 | yuidoc.json
20 | .editorconfig
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # addons:
2 | # sauce_connect: true
3 |
4 | env:
5 | - CXX=g++-4.8
6 | addons:
7 | apt:
8 | sources:
9 | - ubuntu-toolchain-r-test
10 | packages:
11 | - g++-4.8
12 |
13 | sudo: false
14 | language: node_js
15 |
16 | node_js:
17 | - "8.9.4"
18 |
19 | before_install:
20 | - export CHROME_BIN=chromium-browser
21 | - export DISPLAY=:99.0
22 | - sh -e /etc/init.d/xvfb start
23 |
24 | install:
25 | - npm i
26 |
27 | script:
28 | - npm test
29 | - npm run coverage
30 | - npm run client-coverage
31 | - npm run docs
32 | - npm run complexity-report
33 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 10.0.0
4 |
5 | ### Changed
6 |
7 | * Upgraded dependencies
8 | * Added babel transforms for code splitting
9 | * Added configuration changes for import() code quality tools and tests
10 | * Added example lazyloading search route
11 | * Added bundle analyzer
12 | * Added performance improvements
13 |
14 | ### 9.0.0
15 |
16 | ### Changed
17 |
18 | * Updated all dependencies
19 | * See: https://github.com/yannickcr/eslint-plugin-react/issues/1471
20 |
21 | ### 9.0.1
22 |
23 | ### Changed
24 |
25 | * Style lint fix now supported
26 | * npm run fix-all
27 |
28 | ### 9.0.0
29 |
30 | ### Changed
31 |
32 | * Updated all dependencies
33 | * See: https://github.com/yannickcr/eslint-plugin-react/issues/1471
34 |
35 | ## 8.0.0
36 |
37 | ### Changed
38 |
39 | * Removed jQuery
40 | * Remove React Guard
41 | * Upgraded dependencies
42 | * Added new dependencies for testing
43 |
44 | ## 7.0.0
45 |
46 | ## 6.0.0
47 |
48 | ### Bugfixes
49 |
50 | * React server rendering performance 30% faster
51 | * Code now abides most up to date linters
52 | * Removing deprecated vars/fns from updated libs
53 |
54 | ### Added
55 |
56 | * Foundation for acceptance testing
57 |
58 | ### Changed
59 |
60 | * Upgraded dependencies
61 |
62 | ## 5.0.0
63 |
64 | ### Bugfixes
65 |
66 | * Client config won't rerender after initial page load
67 |
68 | ### Added
69 |
70 | * Client side code coverage with codecov
71 | * Added badge to readme
72 | * Added client side test for coverage report
73 |
74 | ### Changed
75 |
76 | * Added dependencies for client side code coverage
77 | * Added client side coverage command to package.json and travis config
78 |
79 | ## 4.0.0
80 |
81 | ### Bugfixes
82 |
83 | * Fixed spelling on head component
84 | * Fixed hot reloading reducers
85 | * Fixed source maps in google chrome
86 | * Fixed enzyme breaking canUseDOM in integration tests
87 |
88 | ### Added
89 |
90 | * Upgraded dependencies
91 | * Added docs and badges to readme
92 | * Travis ci builds
93 | * Coveralls code coverage (server)
94 | * Updated to use babel-preset-node
95 | * Acceptance testing with webdriver.io
96 | * Test for server rendering title
97 |
98 | ### Changed
99 |
100 | * Updated Autoprefixer and Uglify config
101 | * Removed unused code
102 | * Dropped ie8 support
103 | * Updated to use latest-minimal babel
104 | * Updated .stylelintrc to be Airbnb compatible
105 |
106 |
107 | ## 3.0.0
108 |
109 | ### Added
110 |
111 | * Updated dependencies
112 | * Added helmet
113 | * Added client side test watching with karma
114 | * Flattened out test directories
115 | * Removed unused dependencies
116 | * Renamed server service to express service
117 | * Enable redux dev tools
118 | * Hot reload reducers
119 | * Updated live reloading for react-router and redux
120 |
121 |
122 | ## 2.0.0
123 |
124 | ### Added
125 |
126 | * Updated dependencies
127 | * Added contributing
128 | * Updated node version
129 | * Updated react-hot-loader latest
130 |
131 | ### Fixes
132 |
133 | * Remove unused dependency gulp-sass-lint
134 |
135 | ### Changed
136 |
137 | * Webpack dev config was modified for latest react-hot-loader, also entry points were updated.
138 |
139 |
140 |
141 | ## 1.0.0
142 |
143 | ### Added
144 |
145 | * Added project
146 |
147 | ### Fixes
148 |
149 | * .
150 |
151 | ### Changed
152 |
153 | * .
154 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beninmichael@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2014-2016 Michael Benin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/michaelBenin/react-ssr-spa) [](https://david-dm.org/michaelBenin/react-ssr-spa) [](https://david-dm.org/michaelBenin/react-ssr-spa?type=dev) [](https://nodesecurity.io/orgs/react-ssr-spa/projects/517c11e2-34a4-425f-bf5e-3b074e49ab7f)
2 | [](https://github.com/prettier/prettier)
3 |
4 | Server Coverage: [](https://coveralls.io/github/michaelBenin/react-ssr-spa?branch=master)
5 |
6 | Client Coverage: [](https://codecov.io/gh/michaelBenin/react-ssr-spa)
7 |
8 | Acceptance Tests: [](https://saucelabs.com/u/YOUR_SAUCE_USERNAME)
9 |
10 | [](https://saucelabs.com/u/YOUR_SAUCE_USERNAME)
11 |
12 |
13 | # react-ssr-spa
14 |
15 | ### About:
16 |
17 | react-ssr-spa is a react app that is server side rendered and is a single page app.
18 |
19 | Should you use this as a starting point for your application? A good way to know is if you answered yes to any of the following questions.
20 |
21 | Do we need SEO?
22 |
23 | Do we need fast page loads without a loading spinner?
24 |
25 | Do we need a fast app like website?
26 |
27 | ## Quickstart:
28 |
29 | Requirements:
30 |
31 | node.js v8.9.1
32 | npm v5.5.1
33 |
34 | ````
35 | git clone git@github.com:michaelBenin/react-ssr-spa.git
36 | cd react-ssr-spa
37 | npm i
38 | npm start
39 | Open browser http://localhost:8001/
40 | ````
41 |
42 | ### Configuration:
43 |
44 | (Optional) Create an .env file at the root of the directory. See .env.example in root.
45 |
46 | ## Commands:
47 |
48 | ### Run in dev mode:
49 |
50 | npm start
51 |
52 | Optimized for use with:
53 |
54 | [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
55 |
56 | [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
57 |
58 | [Node Inspector Manager](https://chrome.google.com/webstore/detail/nodejs-inspector-manager/bnmjajghllhhhgiaeipaibfmnjnponhd?hl=en)
59 |
60 | [React Performance Devtool](https://chrome.google.com/webstore/detail/react-performance-devtool/fcombecpigkkfcbfaeikoeegkmkjfbfm)
61 |
62 | [React-Sight](https://chrome.google.com/webstore/detail/react-sight/aalppolilappfakpmdfdkpppdnhpgifn)
63 |
64 | ### Server Side Unit Tests:
65 |
66 | npm run server-unit-test
67 |
68 | ### Server Side Integration Tests:
69 |
70 | npm run server-integration-test
71 |
72 |
73 | ### Client Side Unit Tests:
74 |
75 | npm run client-unit-test
76 | npm run client-unit-test-watch
77 |
78 |
79 | ### Client Side Integration Tests:
80 |
81 | npm run client-integration-test
82 | npm run client-integration-test-watch
83 |
84 |
85 | ### Functional / Acceptance Tests (WIP):
86 |
87 | npm run acceptance-test
88 |
89 |
90 | ### JS Lint (uses eslint):
91 |
92 | npm run js-lint
93 | npm run js-lint-fix
94 |
95 |
96 | ### Style Lint (uses styleint):
97 |
98 | npm run style-lint
99 | npm run style-lint-fix
100 |
101 | ### Fix JS/SCSS Lint:
102 |
103 | npm run fix-all
104 |
105 | ### Generate JS Documentation:
106 |
107 | npm run js-doc
108 |
109 | ### Generate Style Documentation:
110 |
111 | npm run style-doc
112 |
113 | ### Generate All Documentation:
114 |
115 | npm run docs
116 |
117 | ### Generate Complexity Report:
118 |
119 | npm run complexity-report
120 |
121 | ### Build production assets:
122 |
123 | npm run build-prod
124 |
125 | ### Emulate Production Locally:
126 |
127 | npm run build-prod
128 | NODE_ENV=test node dist/server
129 |
130 | ### Analyize bundle size
131 |
132 | npm run build-prod
133 |
134 | Update package.json with the appropriate js and map files.
135 |
136 | npm run analyzie-bundle
137 |
138 | ### Run production server (requires PM2 installed globally)
139 |
140 | If PM2 is not installed:
141 |
142 | npm i pm2 -g
143 |
144 | To run server in production mode:
145 |
146 | npm run prod-server
147 |
148 | ### Upload Artifact to S3
149 |
150 | npm run create-upload-artifact
151 |
152 | ### Upload Static Files to S3
153 |
154 | npm run upload-static-files
155 |
156 | ### Configure Server
157 |
158 | ansible-playbook -u ubuntu ./ansible/deploy/deploy.yml
159 |
160 | ### Deployment (ansible & ansistrano)
161 |
162 | ansible-playbook -u ubuntu ./ansible/deploy/deploy.yml
163 |
164 | ### Rollback (ansible & ansistrano)
165 |
166 | ansible-playbook -u ubuntu ./ansible/rollback/rollback.yml
167 |
168 | ### Core Libraries:
169 |
170 | https://github.com/facebook/react
171 |
172 | https://github.com/reactjs/react-router
173 |
174 | https://github.com/reactjs/redux
175 |
176 | https://github.com/reactjs/react-redux
177 |
178 | https://github.com/reactjs/react-router-redux
179 |
180 | https://github.com/gaearon/redux-thunk
181 |
182 | https://github.com/nfl/react-helmet
183 |
184 | https://github.com/expressjs/express
185 |
186 | ### Inspiration:
187 |
188 | https://github.com/rendrjs/rendr
189 |
190 | https://github.com/michaelBenin/node-startup
191 |
192 | https://github.com/ember-fastboot/ember-cli-fastboot
193 |
194 |
--------------------------------------------------------------------------------
/ansible/deploy/after-symlink-tasks.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: start pm2 process
3 | sudo: yes
4 | shell: HOME=/home/ubuntu pm2 startOrReload ./current/pm2/pm2.json5
5 | args:
6 | chdir: /var/www/react-ssr-spa
7 |
8 | - name: save processes for pm2
9 | sudo: yes
10 | shell: pm2 save
11 |
--------------------------------------------------------------------------------
/ansible/deploy/deploy.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: tag_Name_webAppApi
3 | sudo: yes
4 | roles:
5 | - role: carlosbuenosvinos.ansistrano-deploy
6 | ansistrano_deploy_from: "." # Where my local project is (relative or absolute path)
7 | ansistrano_deploy_to: "/var/www/react-ssr-spa" # Base path to deploy to.
8 | ansistrano_version_dir: "releases" # Releases folder name
9 | ansistrano_current_dir: "current" # Softlink name. You should rarely changed it.
10 | ansistrano_current_via: "symlink" # Deployment strategy who code should be deployed to current path. Options are symlink or rsync
11 | ansistrano_shared_paths: [] # Shared paths to symlink to release dir
12 | ansistrano_keep_releases: 2 # Releases to keep after a new deployment. See "Pruning old releases".
13 | ansistrano_deploy_via: "s3_unarchive" # Method used to deliver the code to the server. Options are copy, rsync, git, s3 or download.
14 | ansistrano_allow_anonymous_stats: no
15 |
16 | # Variables used in the S3 deployment strategy
17 | ansistrano_s3_bucket: react-ssr-spa-artifacts
18 | ansistrano_s3_object: "artifacts/{{ lookup('env', 'GIT_COMMIT') }}.tar.gz" # Add the _unarchive suffix to the ansistrano_deploy_via if your object is a package (ie: s3_unarchive)
19 | ansistrano_s3_region: us-east-1
20 | # Optional variables, omitted by default
21 | ansistrano_s3_aws_access_key: {{ lookup('env', 'AWS_ACCESS_KEY') }}
22 | ansistrano_s3_aws_secret_key: {{ lookup('env', 'AWS_SECRET_KEY') }}
23 |
24 | ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/after-symlink-tasks.yml"
25 |
--------------------------------------------------------------------------------
/ansible/rollback/after-symlink-tasks.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: start pm2 process
3 | sudo: yes
4 | shell: HOME=/home/ubuntu pm2 startOrReload ./current/pm2/pm2.json5
5 | args:
6 | chdir: /var/www/react-ssr-spa
7 |
8 | - name: save processes for pm2
9 | sudo: yes
10 | shell: pm2 save
11 |
--------------------------------------------------------------------------------
/ansible/rollback/rollback.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: tag_Name_webAppApi
3 | sudo: yes
4 | roles:
5 | - role: carlosbuenosvinos.ansistrano-rollback
6 | ansistrano_deploy_to: "/var/www/react-ssr-spa" # Base path to deploy to.
7 | ansistrano_version_dir: "releases" # Releases folder name
8 | ansistrano_current_dir: "current" # Softlink name. You should rarely changed it.
9 | ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/after-symlink-tasks.yml"
10 |
--------------------------------------------------------------------------------
/ansible/setup/boto/boto.cfg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/ansible/setup/boto/boto.cfg
--------------------------------------------------------------------------------
/ansible/setup/nginx/sites_available.conf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/ansible/setup/nginx/sites_available.conf
--------------------------------------------------------------------------------
/ansible/setup/setup.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: webAppApi
3 | gather_facts: False
4 | become: true
5 | tasks:
6 | - name: install python 2
7 | raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-pip)
8 | - apt:
9 | name: aptitude
10 | state: present
11 | - apt:
12 | update_cache: yes
13 | upgrade: safe
14 | cache_valid_time: 3600
15 |
16 | - hosts: webAppApi
17 | become: true
18 |
19 | roles:
20 | - geerlingguy.nginx
21 | - geerlingguy.nodejs
22 | - kamaln7.swapfile
23 | - nickhammond.logrotate
24 |
25 | environment:
26 | PM2_OPTIMIZE_MEMORY: 'true'
27 |
28 | vars:
29 | nginx_error_log: "off"
30 | nginx_access_log: "off"
31 | nodejs_version: "8.x"
32 | swapfile_size: 4GB
33 | nodejs_npm_global_packages:
34 | - name: pm2
35 | version: 2.8.0
36 | logrotate_scripts:
37 | - name: nginx
38 | path: /var/log/nginx/*.log
39 | options:
40 | - hourly
41 | - size 1M
42 | - rotate 2
43 | - missingok
44 | - compress
45 | - delaycompress
46 | - copytruncate
47 |
48 | tasks:
49 | - apt:
50 | name: python-boto
51 | state: latest
52 | - copy:
53 | src: "{{playbook_dir}}/boto/boto.cfg"
54 | dest: /etc/boto.cfg
55 | - copy:
56 | src: "{{playbook_dir}}/nginx/sites_available.conf"
57 | dest: /etc/nginx/sites-available/default
58 | - service:
59 | name: nginx
60 | state: restarted
61 | - command: "/usr/local/lib/npm/bin/pm2 ping"
62 | async: 1
63 | poll: 0
64 | ignore_errors: true
65 | - pause:
66 | seconds: 5
67 | - command: "/usr/local/lib/npm/bin/pm2 install pm2-logrotate"
68 | - command: "/usr/local/lib/npm/bin/pm2 set pm2-logrotate:max_size 500K"
69 | - command: "/usr/local/lib/npm/bin/pm2 set pm2-logrotate:retain 1"
70 | - command: "/usr/local/lib/npm/bin/pm2 set pm2-logrotate:rotateInterval '*/1 * * * *'"
71 | - command: "/usr/local/lib/npm/bin/pm2 set pm2-logrotate:compress true"
72 | - command: "/usr/local/lib/npm/bin/pm2 startup ubuntu"
73 | - command: "/usr/local/lib/npm/bin/pm2 save"
74 | - command: 'sleep 3 && sudo /sbin/reboot'
75 | async: 1
76 | poll: 0
77 | ignore_errors: true
78 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to react-ssr-spa
2 |
3 | - Open issue to make sure the work is valid.
4 |
5 | - Branch off of develop, name branch descriptively.
6 |
7 | - Commit referencing the issue #, example: Issue #14 in commit message.
8 |
9 | - Update contributors.
10 |
11 | - Open PR.
12 |
13 |
14 |
--------------------------------------------------------------------------------
/features_flags.js:
--------------------------------------------------------------------------------
1 | // on client, this is bootstrapped to redux store in the config
2 | // on server require it or pull from config
3 | // in sass, just use classnames from components
4 |
5 | module.exports = {
6 | example_feature: true
7 | };
8 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "airbnb",
5 | "prettier",
6 | "prettier/react"
7 | ],
8 | "plugins": ["prettier"],
9 | "rules": {
10 | "import/first": ["off"],
11 | "comma-dangle": [2, "never"],
12 | "react/jsx-filename-extension": [0, { "extensions": [".js"] }],
13 | "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }],
14 | "class-methods-use-this": [0],
15 | "jsx-a11y/no-static-element-interactions": [0],
16 | "jsx-a11y/anchor-is-valid": [ "error", {
17 | "components": [ "Link" ],
18 | "specialLink": [ "to" ]
19 | }],
20 | "react/jsx-boolean-value": ["off"],
21 | "react/no-unescaped-entities": ["off"],
22 | "prettier/prettier": ["warn", {
23 | "singleQuote": true,
24 | "semi": true,
25 | "useTabs": false
26 | }
27 | ]
28 | },
29 | "env": {
30 | "es6": true,
31 | "node": true,
32 | "browser": true,
33 | "mocha": true
34 | },
35 | "globals": {
36 | "browser": true
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "plugins": [
4 | "stylelint-scss",
5 | "stylelint-order"
6 | ],
7 | "rules": {
8 | "block-opening-brace-space-before": "always",
9 | "at-rule-no-unknown": [
10 | true,
11 | {
12 | "ignoreAtRules": [
13 | "each",
14 | "else",
15 | "extend",
16 | "for",
17 | "function",
18 | "if",
19 | "include",
20 | "mixin",
21 | "return",
22 | "while"
23 | ]
24 | }
25 | ],
26 | "comment-empty-line-before": [
27 | "always",
28 | {
29 | "ignore": [
30 | "stylelint-commands"
31 | ]
32 | }
33 | ],
34 | "declaration-colon-space-after": "always",
35 | "declaration-colon-space-before": "never",
36 | "indentation": 2,
37 | "max-nesting-depth": 4,
38 | "no-invalid-double-slash-comments": true,
39 | "rule-empty-line-before": [
40 | "always",
41 | {
42 | "ignore": [
43 | "after-comment"
44 | ]
45 | }
46 | ],
47 | "selector-list-comma-newline-after": "always",
48 | "scss/at-extend-no-missing-placeholder": true,
49 | "declaration-property-value-blacklist": {
50 | "/^border/": [
51 | "none"
52 | ]
53 | }
54 | },
55 | "order/order": [
56 | "custom-properties",
57 | "declarations"
58 | ],
59 | "order/properties-alphabetical-order": true
60 | }
61 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/config.js:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | const dir = __dirname;
4 | const cssSrcPath = './src/client/styles/main.scss';
5 | const staticFilePath = './dist/static/css';
6 |
7 | const config = {
8 | clean: ['docs', 'node_modules', 'dist', 'logs/**/*.log'],
9 | dest: 'dist',
10 | nodemon: {
11 | ignore: [
12 | join(dir, '../../dist/client/**'),
13 | join(dir, '../../src/**'),
14 | join(dir, '../../test/**'),
15 | join(dir, '../../.git'),
16 | join(dir, '../../node_modules/**')
17 | ],
18 | watch: [
19 | join(dir, '../../dist/'),
20 | join(dir, '../../.env'),
21 | join(dir, '../../feature_flags.js')
22 | ],
23 | script: 'dist/server',
24 | ext: 'js env',
25 | env: {
26 | NODE_ENV: 'development'
27 | },
28 | nodeArgs: ['--inspect']
29 | },
30 | test: {
31 | server: {
32 | integration: {
33 | src: [
34 | 'test/server/utils/babel_register.js',
35 | 'test/server/utils/test_initializer_util.js',
36 | 'test/server/integration/**/*.js',
37 | 'test/shared/integration/**/*.js',
38 | 'test/server/utils/test_teardown_util.js'
39 | ]
40 | },
41 | unit: {
42 | src: [
43 | 'test/server/utils/babel_register.js',
44 | 'test/server/utils/test_initializer_utils.js',
45 | 'test/server/utils/enzyme_initializer.js',
46 | 'test/shared/utils/**/*.js',
47 | 'test/server/unit/**/*.js',
48 | 'test/shared/unit/**/*.js'
49 | ]
50 | }
51 | }
52 | },
53 | eslint: {
54 | conf: {
55 | rules: {
56 | 'comma-dangle': [2, 'never'],
57 | 'react/jsx-filename-extension': [0, { extensions: ['.js'] }],
58 | 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }]
59 | }
60 | }
61 | },
62 | styles: {
63 | sassConf: {
64 | style: 'expanded'
65 | },
66 | autoprefixerBrowsers: ['last 2 versions'],
67 | main: {
68 | stylesSrc: cssSrcPath,
69 | cssDest: staticFilePath,
70 | scssWatch: ['src/**/*.scss']
71 | }
72 | },
73 | stylelint: {
74 | src: [
75 | './src/**/*.scss',
76 | '!./src/client/styles/framework/_foundation_settings.scss'
77 | ]
78 | },
79 | server: {
80 | src: ['src/**/*', '!src/assets/**/*', '!src/**/*.scss']
81 | },
82 | babel: {
83 | sourceMaps: 'inline'
84 | },
85 | // add external vendor js files here
86 | vendorJS: {
87 | src: [],
88 | dest: 'dist/static/js'
89 | },
90 | assets: {
91 | src: ['src/assets/**/*'],
92 | dest: 'dist/static/assets'
93 | },
94 | doc: {
95 | src: 'src'
96 | }
97 | };
98 |
99 | export default config;
100 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/karma.conf.coverage.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | const travis = process.env.TRAVIS_CI;
5 |
6 | // https://github.com/karma-runner/karma-coverage
7 | module.exports = function karmaConfIntegration(config) {
8 | const conf = {
9 | browsers: travis ? ['Chrome_travis_ci'] : ['Chrome'],
10 |
11 | files: [
12 | path.join(__dirname, '../../test/shared/utils/**/*.js'),
13 | path.join(__dirname, '../../test/client/unit/**/*'),
14 | path.join(__dirname, '../../test/client/integration/**/*'),
15 | path.join(__dirname, '../../test/shared/**/*'),
16 | {
17 | pattern: path.join(__dirname, '../../src/client/**/*.js'),
18 | watched: false,
19 | included: false,
20 | served: true,
21 | nocache: true
22 | }
23 | ],
24 |
25 | exclude: [path.join(__dirname, '../../src/client/index.js')],
26 |
27 | frameworks: ['mocha'],
28 |
29 | plugins: [
30 | 'karma-mocha',
31 | 'karma-webpack',
32 | 'karma-coverage',
33 | 'karma-chrome-launcher',
34 | 'karma-sourcemap-loader'
35 | ],
36 |
37 | singleRun: true,
38 | autoWatch: false,
39 |
40 | preprocessors: {},
41 |
42 | reporters: ['progress', 'coverage'],
43 |
44 | coverageReporter: {
45 | reporters: [
46 | {
47 | subdir: '.',
48 | type: 'lcov'
49 | }
50 | ],
51 | dir: path.join(__dirname, '../../', 'docs/client-coverage')
52 | },
53 |
54 | webpack: {
55 | devtool: 'sourcemap',
56 | plugins: [
57 | new webpack.DefinePlugin({
58 | 'process.env': {
59 | NODE_ENV: JSON.stringify('development'),
60 | RUNTIME_ENV: JSON.stringify('browser')
61 | }
62 | }),
63 | new webpack.optimize.ModuleConcatenationPlugin()
64 | ],
65 | externals: {
66 | cheerio: 'window',
67 | 'react/addons': true,
68 | 'react/lib/ExecutionEnvironment': true,
69 | 'react/lib/ReactContext': true
70 | },
71 | module: {
72 | rules: [
73 | {
74 | test: /\.js?$/,
75 | use: [
76 | {
77 | loader: 'babel-loader',
78 | options: {
79 | presets: [
80 | ['react'],
81 | [
82 | 'env',
83 | {
84 | targets: {
85 | browsers: ['last 2 versions']
86 | }
87 | }
88 | ]
89 | ],
90 | plugins: [
91 | 'istanbul',
92 | 'syntax-dynamic-import',
93 | 'transform-react-jsx-source'
94 | ]
95 | }
96 | }
97 | ],
98 | exclude: /node_modules/
99 | }
100 | ]
101 | }
102 | },
103 |
104 | webpackMiddleware: {
105 | noInfo: true
106 | },
107 |
108 | customLaunchers: {
109 | Chrome_travis_ci: {
110 | base: 'Chrome',
111 | flags: ['--no-sandbox']
112 | }
113 | }
114 | };
115 |
116 | conf.preprocessors[
117 | path.join(__dirname, '../../test/client/integration/**/*')
118 | ] = ['webpack', 'sourcemap'];
119 |
120 | conf.preprocessors[path.join(__dirname, '../../test/client/unit/**/*')] = [
121 | 'webpack',
122 | 'sourcemap'
123 | ];
124 |
125 | conf.preprocessors[path.join(__dirname, '../../src/client/**/*.js')] = [
126 | 'webpack',
127 | 'sourcemap',
128 | 'coverage'
129 | ];
130 |
131 | conf.preprocessors[path.join(__dirname, '../../test/shared/**/*')] = [
132 | 'webpack',
133 | 'sourcemap',
134 | 'coverage'
135 | ];
136 |
137 | config.set(conf);
138 | };
139 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/karma.conf.integration.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | const karmaAuto = process.env.KARMA_AUTOWATCH;
5 | const travis = process.env.TRAVIS_CI;
6 | const singleRun = karmaAuto !== 'on';
7 | const autoWatch = karmaAuto === 'on';
8 |
9 | // TODO: http://nicolasgallagher.com/how-to-test-react-components-karma-webpack/
10 | module.exports = function karmaConfIntegration(config) {
11 | const conf = {
12 | browsers: travis ? ['Chrome_travis_ci'] : ['Chrome'],
13 | // karma only needs to know about the test bundle
14 | files: [path.join(__dirname, '../../test/client/integration/**/*')],
15 | frameworks: ['mocha'],
16 | plugins: [
17 | // 'karma-chai',
18 | 'karma-mocha',
19 | 'karma-chrome-launcher',
20 | // 'karma-sourcemap-loader',
21 | 'karma-webpack'
22 | ],
23 |
24 | customLaunchers: {
25 | Chrome_travis_ci: {
26 | base: 'Chrome',
27 | flags: ['--no-sandbox']
28 | }
29 | },
30 |
31 | // run the bundle through the webpack and sourcemap plugins
32 | preprocessors: {},
33 | reporters: ['dots'],
34 | singleRun,
35 | autoWatch,
36 | // webpack config object
37 | webpack: {
38 | devtool: 'sourcemap',
39 | plugins: [
40 | new webpack.DefinePlugin({
41 | 'process.env': {
42 | NODE_ENV: JSON.stringify('development'),
43 | RUNTIME_ENV: JSON.stringify('browser')
44 | }
45 | }),
46 | new webpack.optimize.ModuleConcatenationPlugin()
47 | ],
48 | module: {
49 | rules: [
50 | {
51 | test: /\.js?$/,
52 | use: [
53 | {
54 | loader: 'babel-loader',
55 | options: {
56 | presets: [
57 | ['react'],
58 | [
59 | 'env',
60 | {
61 | targets: {
62 | browsers: ['last 2 versions']
63 | }
64 | }
65 | ]
66 | ],
67 | plugins: [
68 | 'syntax-dynamic-import',
69 | 'transform-react-jsx-source'
70 | ]
71 | }
72 | }
73 | ],
74 | exclude: /node_modules/
75 | }
76 | ]
77 | }
78 | },
79 | webpackMiddleware: {
80 | noInfo: true
81 | }
82 | };
83 |
84 | conf.preprocessors[
85 | path.join(__dirname, '../../test/client/integration/**/*')
86 | ] = [
87 | 'webpack'
88 | // 'sourcemap'
89 | ];
90 | config.set(conf);
91 | };
92 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/karma.conf.unit.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | const karmaAuto = process.env.KARMA_AUTOWATCH;
5 | const travis = process.env.TRAVIS_CI;
6 | const singleRun = karmaAuto !== 'on';
7 | const autoWatch = karmaAuto === 'on';
8 |
9 | // TODO: http://nicolasgallagher.com/how-to-test-react-components-karma-webpack/
10 | module.exports = function karmaConfUnit(config) {
11 | const conf = {
12 | browsers: travis ? ['Chrome_travis_ci'] : ['Chrome'],
13 | // karma only needs to know about the test bundle
14 | files: [
15 | path.join(__dirname, '../../test/shared/utils/enzyme_adapter_util.js'),
16 | path.join(__dirname, '../../test/client/unit/**/*'),
17 | path.join(__dirname, '../../test/shared/unit/**/*')
18 | ],
19 | frameworks: ['mocha'],
20 | plugins: [
21 | // 'karma-chai',
22 | 'karma-mocha',
23 | 'karma-chrome-launcher',
24 | // 'karma-sourcemap-loader',
25 | 'karma-webpack'
26 | ],
27 |
28 | customLaunchers: {
29 | Chrome_travis_ci: {
30 | base: 'Chrome',
31 | flags: ['--no-sandbox']
32 | }
33 | },
34 | // run the bundle through the webpack and sourcemap plugins
35 | preprocessors: {},
36 | reporters: ['dots'],
37 | singleRun,
38 | autoWatch,
39 | // webpack config object
40 | webpack: {
41 | devtool: 'sourcemap',
42 | plugins: [
43 | new webpack.DefinePlugin({
44 | 'process.env': {
45 | NODE_ENV: JSON.stringify('development'),
46 | RUNTIME_ENV: JSON.stringify('browser')
47 | }
48 | }),
49 | new webpack.optimize.ModuleConcatenationPlugin()
50 | ],
51 | externals: {
52 | cheerio: 'window',
53 | 'react/addons': true,
54 | 'react/lib/ExecutionEnvironment': true,
55 | 'react/lib/ReactContext': true
56 | },
57 | module: {
58 | rules: [
59 | {
60 | test: /\.js?$/,
61 | use: [
62 | {
63 | loader: 'babel-loader',
64 | options: {
65 | presets: [
66 | ['react'],
67 | [
68 | 'env',
69 | {
70 | targets: {
71 | browsers: ['last 2 versions']
72 | }
73 | }
74 | ]
75 | ],
76 | plugins: [
77 | 'syntax-dynamic-import',
78 | 'transform-react-jsx-source'
79 | ]
80 | }
81 | }
82 | ],
83 | exclude: /node_modules/
84 | }
85 | ]
86 | }
87 | },
88 | webpackMiddleware: {
89 | noInfo: true
90 | }
91 | };
92 |
93 | conf.preprocessors[path.join(__dirname, '../../test/client/unit/**/*')] = [
94 | 'webpack'
95 | // 'sourcemap'
96 | ];
97 |
98 | conf.preprocessors[path.join(__dirname, '../../test/shared/**/*')] = [
99 | 'webpack'
100 | // 'sourcemap'
101 | ];
102 |
103 | config.set(conf);
104 | };
105 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/wdio.conf.js:
--------------------------------------------------------------------------------
1 | // http://webdriver.io/guide/testrunner/configurationfile.html
2 |
3 | const conf = {
4 | services: ['selenium-standalone'],
5 | specs: ['test/acceptance/spec/**/*.js'],
6 | capabilities: [
7 | {
8 | browserName: 'chrome'
9 | }
10 | ],
11 | framework: 'mocha',
12 | mochaOpts: {
13 | require: 'babel-register'
14 | }
15 | };
16 |
17 | if (process.env.TRAVIS_CI) {
18 | conf.services = ['selenium-standalone', 'sauce'];
19 | conf.sauceConnect = true;
20 | conf.user = process.env.SAUCE_USERNAME;
21 | conf.key = process.env.SAUCE_ACCESS_KEY;
22 | conf.capabilities = [
23 | {
24 | browserName: 'chrome',
25 | version: '42',
26 | platform: 'XP',
27 | 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
28 | name: 'react-ssr-spa-acceptance',
29 | build: process.env.TRAVIS_BUILD_NUMBER
30 | }
31 | ];
32 | }
33 |
34 | exports.config = conf;
35 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/webpack.config.js:
--------------------------------------------------------------------------------
1 | // http://qiita.com/kimagure/items/f2d8d53504e922fe3c5c
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const ManifestPlugin = require('webpack-manifest-plugin');
5 |
6 | module.exports = {
7 | devtool: 'cheap-module-source-map',
8 | entry: {
9 | vendor: [
10 | 'fontfaceobserver',
11 | 'react',
12 | 'react-dom',
13 | 'react-loadable',
14 | 'bluebird',
15 | 'history/createBrowserHistory',
16 | 'react-router-config',
17 | 'react-router/Switch',
18 | 'react-router/Route',
19 | 'react-helmet',
20 | 'react-transition-group/TransitionGroup',
21 | 'react-transition-group/CSSTransition',
22 | 'react-error-boundary',
23 | 'lodash/get',
24 | 'axios',
25 | 'redux',
26 | 'redux-thunk',
27 | 'react-router-redux',
28 | 'react-redux',
29 | 'prop-types',
30 | 'exenv',
31 | 'scriptjs'
32 | ],
33 | bundle: [
34 | 'react-hot-loader/patch',
35 | // activate HMR for React
36 |
37 | 'webpack-dev-server/client?http://localhost:3001',
38 | // bundle the client for webpack-dev-server
39 | // and connect to the provided endpoint
40 |
41 | 'webpack/hot/only-dev-server',
42 | // bundle the client for hot reloading
43 | // only- means to only hot reload for successful updates
44 |
45 | path.join(__dirname, '../../src/client/index')
46 | ]
47 | },
48 | output: {
49 | path: path.join(__dirname, '../../dist'),
50 | filename: '[name].js',
51 | publicPath: 'http://localhost:3001/static'
52 | },
53 | devServer: {
54 | headers: {
55 | 'Access-Control-Allow-Origin': '*'
56 | }
57 | },
58 | plugins: [
59 | new webpack.DefinePlugin({
60 | 'process.env': {
61 | NODE_ENV: JSON.stringify('development'),
62 | RUNTIME_ENV: JSON.stringify('browser')
63 | }
64 | }),
65 | new ManifestPlugin({ writeToFileEmit: true }),
66 | new webpack.optimize.CommonsChunkPlugin({
67 | name: 'vendor',
68 | chunks: ['app'],
69 | minChunks(module /* , count */) {
70 | const { context } = module;
71 | return context && context.indexOf('node_modules') >= 0;
72 | },
73 | filename: 'vendor.js'
74 | }),
75 | // new webpack.optimize.ModuleConcatenationPlugin(),
76 | new webpack.HotModuleReplacementPlugin(),
77 | // enable HMR globally
78 |
79 | new webpack.NamedModulesPlugin(),
80 | // prints more readable module names in the browser console on HMR updates
81 |
82 | new webpack.NoEmitOnErrorsPlugin()
83 | // do not emit compiled assets that include errors
84 | ],
85 | module: {
86 | rules: [
87 | {
88 | test: /\.js?$/,
89 | use: [
90 | {
91 | loader: 'babel-loader',
92 | options: {
93 | presets: [
94 | ['react'],
95 | [
96 | 'env',
97 | {
98 | targets: {
99 | browsers: ['last 2 versions']
100 | }
101 | }
102 | ]
103 | ],
104 | plugins: [
105 | 'syntax-dynamic-import',
106 | 'react-hot-loader/babel',
107 | 'transform-react-jsx-source'
108 | ]
109 | }
110 | }
111 | ],
112 | exclude: /node_modules/
113 | }
114 | ]
115 | }
116 | };
117 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/configs/webpack.config.production.js:
--------------------------------------------------------------------------------
1 | // https://webpack.github.io/docs/list-of-plugins.html
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const ShakePlugin = require('webpack-common-shake').Plugin;
5 | const ManifestPlugin = require('webpack-manifest-plugin');
6 | const WebpackChunkHash = require('webpack-chunk-hash');
7 |
8 | module.exports = {
9 | devtool: 'source-map',
10 | entry: {
11 | vendor: [
12 | 'fontfaceobserver',
13 | 'react',
14 | 'react-dom',
15 | 'react-loadable',
16 | 'bluebird',
17 | 'history/createBrowserHistory',
18 | 'react-router-config',
19 | 'react-router/Switch',
20 | 'react-router/Route',
21 | 'react-helmet',
22 | 'react-transition-group/TransitionGroup',
23 | 'react-transition-group/CSSTransition',
24 | 'react-error-boundary',
25 | 'lodash/get',
26 | 'axios',
27 | 'redux',
28 | 'redux-thunk',
29 | 'react-router-redux',
30 | 'react-redux',
31 | 'prop-types',
32 | 'exenv',
33 | 'scriptjs'
34 | ],
35 | app: path.join(__dirname, '../../src/client/index')
36 | },
37 | output: {
38 | path: path.join(__dirname, '../../dist/static/js'),
39 | publicPath: '/js/',
40 | filename: '[name].[hash].js'
41 | },
42 | plugins: [
43 | new webpack.HashedModuleIdsPlugin({
44 | hashFunction: 'sha256',
45 | hashDigest: 'hex',
46 | hashDigestLength: 20
47 | }),
48 | new ManifestPlugin({ writeToFileEmit: true }),
49 | new webpack.DefinePlugin({
50 | 'process.env': {
51 | NODE_ENV: JSON.stringify('production'),
52 | RUNTIME_ENV: JSON.stringify('browser')
53 | }
54 | }),
55 | new webpack.optimize.CommonsChunkPlugin({
56 | name: 'vendor',
57 | chunks: ['app'],
58 | minChunks(module /* , count */) {
59 | const { context } = module;
60 | return context && context.indexOf('node_modules') >= 0;
61 | },
62 | filename: 'vendor.[hash].js'
63 | }),
64 | new WebpackChunkHash({ algorithm: 'md5' }),
65 | new ShakePlugin(),
66 | new webpack.optimize.ModuleConcatenationPlugin(),
67 | new webpack.LoaderOptionsPlugin({
68 | minimize: true
69 | }),
70 | new webpack.optimize.UglifyJsPlugin({
71 | sourceMap: true,
72 | compress: {
73 | warnings: false,
74 | screw_ie8: true
75 | },
76 | output: {
77 | comments: false,
78 | semicolons: true
79 | }
80 | })
81 | ],
82 | module: {
83 | rules: [
84 | {
85 | test: /\.js?$/,
86 | use: [
87 | {
88 | loader: 'babel-loader',
89 | options: {
90 | presets: [
91 | ['react'],
92 | [
93 | 'env',
94 | {
95 | targets: {
96 | browsers: ['last 2 versions']
97 | }
98 | }
99 | ]
100 | ],
101 | plugins: ['syntax-dynamic-import']
102 | }
103 | }
104 | ],
105 | exclude: /node_modules/
106 | }
107 | ]
108 | }
109 | };
110 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | gulpfile.js
3 | ===========
4 | Rather than manage one giant configuration file responsible
5 | for creating multiple tasks, each task has been broken out into
6 | its own file in gulpfile.js/tasks. Any files in that directory get
7 | automatically required below.
8 | To add a new task, simply add a new task file that directory.
9 | gulpfile.js/tasks/default.js specifies the default set of tasks to run
10 | when you run `gulp`.
11 | */
12 |
13 | import requireDir from 'require-dir';
14 |
15 | // Require all tasks in gulpfile.js/tasks, including subfolders
16 | requireDir('./tasks', {
17 | recurse: true
18 | });
19 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_acceptance_test.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import webdriver from 'gulp-webdriver';
3 | import runSequence from 'run-sequence';
4 | // import { log } from 'gulp-util';
5 | import { join } from 'path';
6 |
7 | const configPath = join(
8 | __dirname,
9 | '../../gulpfile.babel.js/configs/wdio.conf.js'
10 | );
11 |
12 | gulp.task('webdriver', () =>
13 | gulp.src(configPath).pipe(
14 | webdriver({
15 | // logLevel: 'verbose'
16 | })
17 | )
18 | );
19 |
20 | gulp.task('start-test-server', done => {
21 | require('../../dist/server').default.then(done); // eslint-disable-line global-require, max-len
22 | });
23 |
24 | gulp.task('kill', () => {
25 | const gracefulExit = require('../../dist/server/utils/graceful_exit_util'); // eslint-disable-line global-require, max-len
26 | gracefulExit.default(false, true);
27 | });
28 |
29 | gulp.task('acceptance-test', callback => {
30 | runSequence('start-test-server', 'webdriver', 'kill', callback);
31 | });
32 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_babel_server.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import { log } from 'gulp-util';
3 | import changed from 'gulp-changed';
4 | import babel from 'gulp-babel';
5 | import config from './../configs/config';
6 |
7 | gulp.task('babel-server', function babelServer() {
8 | const env = process.env.NODE_ENV;
9 | const plugins =
10 | env === 'production'
11 | ? ['dynamic-import-node']
12 | : ['dynamic-import-node', 'transform-react-jsx-source'];
13 |
14 | const babelStream = babel({
15 | sourceMaps: 'inline',
16 | presets: [
17 | 'react',
18 | [
19 | 'env',
20 | {
21 | targets: {
22 | node: 'current'
23 | }
24 | }
25 | ]
26 | ],
27 | plugins
28 | });
29 |
30 | babelStream.on('error', function handleError(err) {
31 | log(`Error transpiling babel in gulp_babel_server task.`);
32 | log(err);
33 | babelStream.end();
34 | });
35 |
36 | return gulp
37 | .src(config.server.src)
38 | .pipe(changed(config.dest))
39 | .pipe(babelStream)
40 | .pipe(gulp.dest(config.dest));
41 | });
42 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_build_dev_server.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import runSequence from 'run-sequence';
3 |
4 | gulp.task('build-dev-server', function buildDevServer(callback) {
5 | runSequence(
6 | ['sass-dev', 'babel-server', 'webpack-dev-server'],
7 | 'dev-server',
8 | callback
9 | );
10 | });
11 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_ci_test.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import runSequence from 'run-sequence';
3 | import { log } from 'gulp-util';
4 |
5 | gulp.task('ci-test', function test(cb) {
6 | runSequence(
7 | 'js-lint-src',
8 | 'js-lint-test',
9 | 'js-lint-gulp',
10 | // 'client-unit-test',
11 | // 'client-integration-test',
12 | 'server-integration-test',
13 | 'server-unit-test',
14 | 'sass-lint',
15 | // 'acceptance-test',
16 |
17 | err => {
18 | if (err) {
19 | const exitCode = 2;
20 | log('[ERROR] gulp build task failed', err);
21 | log(`[FAIL] gulp build task failed - exiting with code ${exitCode}`);
22 | return process.exit(exitCode);
23 | }
24 | return cb();
25 | }
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_clean.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import del from 'del';
3 | import config from './../configs/config';
4 |
5 | gulp.task('clean', function clean(cb) {
6 | del(config.clean, cb);
7 | });
8 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_client_coverage_test.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import path from 'path';
3 | import { Server } from 'karma';
4 | import { log } from 'gulp-util';
5 |
6 | const redisPath = '../../dist/server/services/redis_service';
7 | const serverPath = '../../dist/server/services/express_service';
8 |
9 | gulp.task('client-coverage', function clientIntegrationTest(done) {
10 | const startServer = require('../../dist/server'); // eslint-disable-line global-require, import/no-dynamic-require
11 |
12 | startServer.default
13 | .then(function handleStartServer() {
14 | new Server(
15 | {
16 | configFile: path.join(
17 | __dirname,
18 | '../../gulpfile.babel.js/configs/karma.conf.coverage.js'
19 | ),
20 | singleRun: true
21 | },
22 | function end() {
23 | require(serverPath) // eslint-disable-line global-require, import/no-dynamic-require
24 | .createOrGetServer()
25 | .close(function closeServer() {
26 | // eslint-disable-next-line global-require, import/no-dynamic-require
27 | const redis = require(redisPath).default;
28 | if (redis.quit) {
29 | redisPath.quit();
30 | }
31 | log('Closing express server');
32 | done();
33 | });
34 | }
35 | ).start();
36 | })
37 | .catch(function handleError(err) {
38 | log(err);
39 | require(serverPath) // eslint-disable-line global-require, import/no-dynamic-require
40 | .createOrGetServer()
41 | .close(function closeServer() {
42 | // eslint-disable-next-line global-require, import/no-dynamic-require
43 | require(redisPath).default.quit();
44 | log('Closing express server with error.');
45 | done();
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_client_integration_test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-dynamic-require */
2 | import gulp from 'gulp';
3 | import path from 'path';
4 | import { Server } from 'karma';
5 | import { log } from 'gulp-util';
6 |
7 | const redisPath = '../../dist/server/services/redis_service';
8 | const serverPath = '../../dist/server/services/express_service';
9 | const karmaAuto = process.env.KARMA_AUTOWATCH;
10 | const singleRun = karmaAuto !== 'on';
11 |
12 | gulp.task('client-integration-test', function clientIntegrationTest(done) {
13 | const startServer = require('../../dist/server'); // eslint-disable-line global-require
14 |
15 | startServer.default
16 | .then(function handleStartServer() {
17 | new Server(
18 | {
19 | configFile: path.join(
20 | __dirname,
21 | '../../gulpfile.babel.js/configs/karma.conf.integration.js'
22 | ),
23 | singleRun
24 | },
25 | function end() {
26 | require(serverPath) // eslint-disable-line global-require
27 | .createOrGetServer()
28 | .close(function closeServer() {
29 | const redis = require(redisPath).default; // eslint-disable-line global-require
30 | if (redis.quit) {
31 | redisPath.quit();
32 | }
33 | log('Closing express server');
34 | done();
35 | });
36 | }
37 | ).start();
38 | })
39 | .catch(function handleError(err) {
40 | log(err);
41 | require(serverPath) // eslint-disable-line global-require
42 | .createOrGetServer()
43 | .close(function closeServer() {
44 | require(redisPath).default.quit(); // eslint-disable-line global-require
45 | log('Closing express server with error.');
46 | done();
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_client_unit_test.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import path from 'path';
3 |
4 | const { Server } = require('karma');
5 |
6 | const karmaAuto = process.env.KARMA_AUTOWATCH;
7 | const singleRun = karmaAuto !== 'on';
8 |
9 | gulp.task('client-unit-test', function clientUnitTests(done) {
10 | new Server(
11 | {
12 | configFile: path.join(
13 | __dirname,
14 | '../../gulpfile.babel.js/configs/karma.conf.unit.js'
15 | ),
16 | singleRun
17 | },
18 | done
19 | ).start();
20 | });
21 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_copy_assets.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import config from './../configs/config';
3 |
4 | gulp.task('copy-assets', function copyAssets() {
5 | return gulp.src(config.assets.src).pipe(gulp.dest(config.assets.dest));
6 | });
7 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_create_upload_artifact.js:
--------------------------------------------------------------------------------
1 | import aws from 'gulp-aws';
2 | import tar from 'gulp-tar';
3 | import gzip from 'gulp-gzip';
4 | import gulp from 'gulp';
5 |
6 | gulp.task('create-upload-artifact', function compactS3Upload() {
7 | return gulp
8 | .src('./**/*', {
9 | buffer: false,
10 | dot: true
11 | })
12 | .pipe(tar(`${process.env.GIT_COMMIT}.tar`))
13 | .pipe(gzip())
14 | .pipe(gulp.dest(`./${process.env.GIT_COMMIT}`))
15 | .pipe(
16 | aws.s3('react-ssr-spa-artifacts/artifacts', {
17 | aws_cli_path: '/usr/bin/aws',
18 | aws_region: 'us-east-1',
19 | aws_key: process.env.AWS_ACCESS_KEY_ID,
20 | aws_secret: process.env.AWS_SECRET_ACCESS_KEY
21 | })
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_dev.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import livereload from 'gulp-livereload';
3 | import config from './../configs/config';
4 |
5 | gulp.task(
6 | 'dev',
7 | ['vendor-js', 'copy-assets', 'build-dev-server'],
8 | function dev() {
9 | livereload.listen();
10 | gulp.watch(config.styles.main.scssWatch, ['sass-dev']);
11 | gulp.watch(config.server.src, ['babel-server']);
12 | }
13 | );
14 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_dev_server.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import gutil from 'gulp-util';
3 | import nodemon from 'gulp-nodemon';
4 | import config from './../configs/config';
5 |
6 | gulp.task('dev-server', function devServer() {
7 | nodemon(config.nodemon)
8 | .on('log', function handleLogEvent(event) {
9 | gutil.log(event.colour);
10 | })
11 | .on('start', function handleStartEvent() {
12 | gutil.log('Nodemon reloading page with livereload.');
13 | })
14 | .on('exit', function handleExitEvent() {
15 | gutil.log('Nodemon script exiting.');
16 | })
17 | .on('crash', function handleCrash() {
18 | gutil.log('Nodemon script crashed for some reason');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_js_doc.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import esdoc from 'gulp-esdoc';
3 |
4 | gulp.task('js-doc', function jsDoc() {
5 | gulp.src(['src']).pipe(
6 | esdoc({
7 | destination: 'docs/js'
8 | })
9 | );
10 | });
11 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_js_lint.js:
--------------------------------------------------------------------------------
1 | import eslint from 'gulp-eslint';
2 | import gulp from 'gulp';
3 | import path from 'path';
4 | import { cloneDeep } from 'lodash';
5 | import gulpIf from 'gulp-if';
6 | import config from '../configs/config';
7 |
8 | const configFilePath = path.join(__dirname, './../configs/.eslintrc');
9 | const conf = {
10 | useEslintrc: false,
11 | configFile: configFilePath,
12 | rules: config.eslint.conf.rules
13 | };
14 |
15 | function isFixed(file) {
16 | return file.eslint !== null && file.eslint.fixed;
17 | }
18 |
19 | gulp.task('js-lint-src', function lintSrc() {
20 | return gulp
21 | .src('src/**/*.js')
22 | .pipe(eslint(conf))
23 | .pipe(eslint.format())
24 | .pipe(eslint.failAfterError());
25 | });
26 |
27 | gulp.task('js-lint-gulp', function lintGulp() {
28 | const gulpConf = cloneDeep(conf);
29 | gulpConf.rules['import/no-extraneous-dependencies'] = 0;
30 | return gulp
31 | .src('gulpfile.babel.js/**/*.js')
32 | .pipe(eslint(gulpConf))
33 | .pipe(eslint.format())
34 | .pipe(eslint.failAfterError());
35 | });
36 |
37 | gulp.task('js-lint-test', function lintTests() {
38 | const baseConfig = cloneDeep(config.eslint.conf);
39 | baseConfig.rules['prefer-arrow-callback'] = 0;
40 | baseConfig.rules['func-names'] = 0;
41 | baseConfig.rules['import/no-extraneous-dependencies'] = 0;
42 | return gulp
43 | .src('test/**/*.js')
44 | .pipe(
45 | eslint({
46 | useEslintrc: false,
47 | configFile: configFilePath,
48 | rules: baseConfig.rules,
49 | globals: ['$']
50 | })
51 | )
52 | .pipe(eslint.format())
53 | .pipe(eslint.failAfterError());
54 | });
55 |
56 | gulp.task(
57 | 'js-lint',
58 | ['js-lint-src', 'js-lint-gulp', 'js-lint-test'],
59 | function jslint(cb) {
60 | cb();
61 | }
62 | );
63 |
64 | gulp.task('js-lint-src-fix', function lintSrc() {
65 | const confFix = cloneDeep(conf);
66 | confFix.fix = true;
67 | return gulp
68 | .src('src/**/*.js')
69 | .pipe(eslint(confFix))
70 | .pipe(eslint.format())
71 | .pipe(gulpIf(isFixed, gulp.dest('src')));
72 | });
73 |
74 | gulp.task('js-lint-gulp-fix', function lintGulp() {
75 | const confFix = cloneDeep(conf);
76 | confFix.fix = true;
77 | confFix.rules['import/no-extraneous-dependencies'] = 0;
78 | return gulp
79 | .src('gulpfile.babel.js/**/*.js')
80 | .pipe(eslint(confFix))
81 | .pipe(eslint.format())
82 | .pipe(gulpIf(isFixed, gulp.dest('gulpfile.babel.js')));
83 | });
84 |
85 | gulp.task('js-lint-test-fix', function lintTests() {
86 | const baseConfig = cloneDeep(config.eslint.conf);
87 | baseConfig.rules['prefer-arrow-callback'] = 0;
88 | baseConfig.rules['func-names'] = 0;
89 | baseConfig.rules['import/no-extraneous-dependencies'] = 0;
90 | return gulp
91 | .src('test/**/*.js')
92 | .pipe(
93 | eslint({
94 | fix: true,
95 | useEslintrc: false,
96 | configFile: configFilePath,
97 | rules: baseConfig.rules,
98 | globals: ['$']
99 | })
100 | )
101 | .pipe(eslint.format())
102 | .pipe(gulpIf(isFixed, gulp.dest('test')));
103 | });
104 |
105 | gulp.task(
106 | 'js-lint-fix',
107 | ['js-lint-src-fix', 'js-lint-gulp-fix', 'js-lint-test-fix'],
108 | function jslint(cb) {
109 | cb();
110 | }
111 | );
112 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_prod.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import runSequence from 'run-sequence';
3 |
4 | gulp.task(
5 | 'build-prod',
6 | ['sass-prod', 'webpack-prod', 'babel-server', 'copy-assets'],
7 | function prod(callback) {
8 | runSequence(['svg-min'], callback);
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_sass_dev.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import sass from 'gulp-sass';
3 | import autoprefixer from 'gulp-autoprefixer';
4 | import sourcemaps from 'gulp-sourcemaps';
5 | import livereload from 'gulp-livereload';
6 | import config from './../configs/config';
7 |
8 | gulp.task('sass-dev', function sassDev() {
9 | const targetProps = config.styles.main;
10 | const { cssDest, stylesSrc, sassConf, autoprefixerBrowsers } = targetProps;
11 |
12 | return gulp
13 | .src(stylesSrc)
14 | .pipe(sourcemaps.init())
15 | .pipe(sass(sassConf).on('error', sass.logError))
16 | .pipe(autoprefixer(autoprefixerBrowsers))
17 | .pipe(sourcemaps.write())
18 | .pipe(gulp.dest(cssDest))
19 | .pipe(
20 | livereload({ file: 'http://localhost:3000/dist/static/css/main.css' })
21 | );
22 | });
23 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_sass_doc.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import sassdoc from 'sassdoc';
3 |
4 | gulp.task('style-doc', function styleDoc() {
5 | return gulp.src(['src/**/*.scss', '!src/styles/vendor/**/*.scss']).pipe(
6 | sassdoc({
7 | dest: './docs/styles',
8 | verbose: true
9 | })
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_sass_prod.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import sass from 'gulp-sass';
3 | import autoprefixer from 'gulp-autoprefixer';
4 | import postcss from 'gulp-postcss';
5 | import cssnano from 'cssnano';
6 | import config from './../configs/config';
7 |
8 | gulp.task('sass-prod', function sassProd() {
9 | const {
10 | cssDest,
11 | stylesSrc,
12 | sassConf,
13 | autoprefixerBrowsers
14 | } = config.styles.main;
15 | return gulp
16 | .src(stylesSrc)
17 | .pipe(sass(sassConf).on('error', sass.logError))
18 | .pipe(autoprefixer(autoprefixerBrowsers))
19 | .pipe(postcss([cssnano()]))
20 | .pipe(gulp.dest(cssDest));
21 | });
22 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_server_integration_test.js:
--------------------------------------------------------------------------------
1 | import mocha from 'gulp-mocha';
2 | import gulp from 'gulp';
3 | import { log } from 'gulp-util';
4 | import config from './../configs/config';
5 |
6 | gulp.task('server-integration-test', function serverIntegrationTest() {
7 | return gulp
8 | .src(config.test.server.integration.src, {
9 | read: false
10 | })
11 | .pipe(mocha())
12 | .once('error', function handleError(err) {
13 | log(err);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_server_unit_test.js:
--------------------------------------------------------------------------------
1 | import mocha from 'gulp-mocha';
2 | import gulp from 'gulp';
3 | import { log } from 'gulp-util';
4 | import config from './../configs/config';
5 |
6 | gulp.task('server-unit-test', function serverUnitTeset() {
7 | return gulp
8 | .src(config.test.server.unit.src, {
9 | read: false
10 | })
11 | .pipe(mocha())
12 | .once('error', function handleError(err) {
13 | log(err);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_style_lint.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import stylelint from 'gulp-stylelint';
3 | import path from 'path';
4 | import config from '../configs/config';
5 |
6 | const configFilePath = path.join(__dirname, './../configs/.stylelintrc');
7 |
8 | gulp.task('style-lint', function lintSass() {
9 | return gulp.src(config.stylelint.src).pipe(
10 | stylelint({
11 | configFile: configFilePath,
12 | reporters: [
13 | {
14 | formatter: 'string',
15 | console: true
16 | }
17 | ],
18 | failAfterError: true
19 | })
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_svg_min.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import svgmin from 'gulp-svgmin';
3 |
4 | gulp.task('svg-min', function gulpSVG() {
5 | return gulp
6 | .src(['./dist/static/**/*.svg'])
7 | .pipe(svgmin())
8 | .pipe(gulp.dest('./dist/static'));
9 | });
10 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_test.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import runSequence from 'run-sequence';
3 | import { log } from 'gulp-util';
4 |
5 | gulp.task('test', function test(cb) {
6 | runSequence(
7 | 'js-lint-src',
8 | 'js-lint-test',
9 | 'js-lint-gulp',
10 | 'client-integration-test',
11 | 'server-integration-test',
12 | 'client-unit-test',
13 | 'server-unit-test',
14 | 'style-lint',
15 | // 'acceptance-test',
16 |
17 | err => {
18 | if (err) {
19 | const exitCode = 2;
20 | log('[ERROR] gulp build task failed', err);
21 | log(`[FAIL] gulp build task failed - exiting with code ${exitCode}`);
22 | return process.exit(exitCode);
23 | }
24 | return cb();
25 | }
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_upload_static_files.js:
--------------------------------------------------------------------------------
1 | import awspublish from 'gulp-awspublish';
2 | import gulp from 'gulp';
3 | import rename from 'gulp-rename';
4 | import { join } from 'path';
5 | import { log } from 'gulp-util';
6 | import { existsSync } from 'fs';
7 |
8 | const bundle = process.env.STATIC_BUNDLE_URL;
9 | const vendor = process.env.STATIC_VENDOR_URL;
10 |
11 | const manifestPath = join(__dirname, '../../dist/static/js/manifest.json');
12 |
13 | let manifestJSON = false;
14 |
15 | gulp.task('upload-static-files', function publishAWS() {
16 | const publisher = awspublish.create({
17 | region: 'us-east-1',
18 | params: {
19 | Bucket: process.env.s3bucket
20 | }
21 | });
22 |
23 | const headers = {
24 | 'Cache-Control': 'max-age=315360000, no-transform, public'
25 | };
26 |
27 | return gulp
28 | .src('./dist/static/**/*')
29 | .pipe(
30 | rename(function renamePlugin(path) {
31 | path.dirname = `/${bundle}/${path.dirname}`; // eslint-disable-line no-param-reassign
32 | return path;
33 | })
34 | )
35 | .pipe(awspublish.gzip())
36 | .pipe(publisher.publish(headers))
37 | .pipe(awspublish.reporter());
38 | });
39 |
40 | gulp.task('upload-vendor-file', function publishAWS() {
41 | if (existsSync(manifestPath)) {
42 | // eslint-disable-next-line global-require, import/no-dynamic-require
43 | manifestJSON = require(manifestPath);
44 | } else {
45 | log(`Manifest file not found: ${manifestPath}`);
46 | throw new Error(`Manifest file not found: ${manifestPath}`);
47 | }
48 |
49 | const publisher = awspublish.create({
50 | region: 'us-east-1',
51 | params: {
52 | Bucket: process.env.s3bucket
53 | }
54 | });
55 |
56 | const headers = {
57 | 'Cache-Control': 'max-age=315360000, no-transform, public'
58 | };
59 |
60 | return gulp
61 | .src([
62 | `./dist/static/js/${manifestJSON['vendor.js']}`,
63 | `./dist/static/js/${manifestJSON['vendor.js.map']}`
64 | ])
65 | .pipe(
66 | rename(function renamePlugin(path) {
67 | path.dirname = `/${vendor}/${path.dirname}`; // eslint-disable-line no-param-reassign
68 | return path;
69 | })
70 | )
71 | .pipe(awspublish.gzip())
72 | .pipe(publisher.publish(headers))
73 | .pipe(awspublish.reporter());
74 | });
75 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_vendor_js.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import config from './../configs/config';
3 |
4 | gulp.task('vendor-js', function vendorJS() {
5 | return gulp.src(config.vendorJS.src).pipe(gulp.dest(config.vendorJS.dest));
6 | });
7 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_webpack_dev_server.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import webpack from 'webpack';
3 | import WebpackDevServer from 'webpack-dev-server';
4 | import { log } from 'gulp-util';
5 | import config from './../configs/webpack.config';
6 |
7 | gulp.task('webpack-dev-server', function runWebpackDevServer(callback) {
8 | const webpackApp = new WebpackDevServer(webpack(config), {
9 | publicPath: config.output.publicPath,
10 | hot: true,
11 | historyApiFallback: true,
12 | headers: {
13 | 'Access-Control-Allow-Origin': '*'
14 | }
15 | });
16 |
17 | const server = webpackApp.listen(
18 | 3001,
19 | 'localhost',
20 | function webpackDevServerRunning(err /* , result */) {
21 | if (err) {
22 | log(err);
23 | }
24 | log('Webpack dev server listening at localhost:3001');
25 | callback();
26 | }
27 | );
28 |
29 | process.on('SIGINT', () => {
30 | log('Process interrupted');
31 | server.close();
32 | process.exit();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/gulpfile.babel.js/tasks/gulp_webpack_prod.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import webpack from 'webpack';
3 | import gutil from 'gulp-util';
4 | import conf from '../configs/webpack.config.production';
5 |
6 | gulp.task('webpack-prod', function webpackProd(callback) {
7 | webpack(conf, function runWebpackProd(err, stats) {
8 | if (err) {
9 | throw new gutil.PluginError('webpack', err);
10 | }
11 | gutil.log('[webpack]', stats.toString({}));
12 | callback();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/logs/.gitkeep
--------------------------------------------------------------------------------
/pm2/pm2.json5:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "react-ssr-spa",
5 | "cwd": "./current",
6 | "script": "dist/server/index.js",
7 | "instances": "0",
8 | "exec_mode": "cluster_mode",
9 | "max_memory_restart": "500M",
10 | "min_uptime": 1000,
11 | "listen_timeout": 5000,
12 | "wait_ready": true,
13 | "watch": false,
14 | "merge_logs": true,
15 | "log_file": "/var/log/react-ssr-spa/pm2_react-ssr-spa_web_app_child.log",
16 | "error_file": "/var/log/react-ssr-spa/pm2_react-ssr-spa_web_app_error.log",
17 | "out_file": "/var/log/react-ssr-spa/pm2_react-ssr-spa_web_app_child_out.log",
18 | "exec_interpreter": "node",
19 | "env": {
20 | "NODE_ENV": "production"
21 | },
22 | "pmx": false,
23 | "vizion": false,
24 | "restart_delay": 4000
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/src/assets/fonts/amble-light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/fonts/amble-light-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/amble-light-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/fonts/amble-light-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/amble-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/fonts/amble-regular-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/amble-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/fonts/amble-regular-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-144x144.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-256x256.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-36x36.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-384x384.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-48x48.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-72x72.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/android-chrome-96x96.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/assets/icons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #da532c
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-ssr-spa",
3 | "icons": [
4 | {
5 | "src": "\/android-chrome-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png"
8 | },
9 | {
10 | "src": "\/android-chrome-48x48.png",
11 | "sizes": "48x48",
12 | "type": "image\/png"
13 | },
14 | {
15 | "src": "\/android-chrome-72x72.png",
16 | "sizes": "72x72",
17 | "type": "image\/png"
18 | },
19 | {
20 | "src": "\/android-chrome-96x96.png",
21 | "sizes": "96x96",
22 | "type": "image\/png"
23 | },
24 | {
25 | "src": "\/android-chrome-144x144.png",
26 | "sizes": "144x144",
27 | "type": "image\/png"
28 | },
29 | {
30 | "src": "\/android-chrome-192x192.png",
31 | "sizes": "192x192",
32 | "type": "image\/png"
33 | },
34 | {
35 | "src": "\/android-chrome-256x256.png",
36 | "sizes": "256x256",
37 | "type": "image\/png"
38 | },
39 | {
40 | "src": "\/android-chrome-384x384.png",
41 | "sizes": "384x384",
42 | "type": "image\/png"
43 | },
44 | {
45 | "src": "\/android-chrome-512x512.png",
46 | "sizes": "512x512",
47 | "type": "image\/png"
48 | }
49 | ],
50 | "theme_color": "#ffffff",
51 | "display": "standalone"
52 | }
53 |
--------------------------------------------------------------------------------
/src/assets/icons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/mstile-144x144.png
--------------------------------------------------------------------------------
/src/assets/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/assets/icons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/mstile-310x150.png
--------------------------------------------------------------------------------
/src/assets/icons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/mstile-310x310.png
--------------------------------------------------------------------------------
/src/assets/icons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/icons/mstile-70x70.png
--------------------------------------------------------------------------------
/src/assets/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/assets/images/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/src/assets/images/logo.gif
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import React from 'react';
3 | import { hydrate, render } from 'react-dom';
4 | import matchRoutes from 'react-router-config/matchRoutes';
5 | import routes from '../react_router/react_router';
6 | import createHistory from 'history/createBrowserHistory';
7 | import scriptJS from 'scriptjs';
8 | import get from 'lodash/get';
9 | import log from './services/logger_service';
10 | import initialize from './utils/initializer_util';
11 | import configureStore from '../redux/store/store';
12 | import initialLoadActionCreator from '../redux/action_creators/initial_load_action_creator';
13 | import Root from '../views/containers/root_container';
14 | import { ThirdPartyJs, loadAllThirdPartyJs } from './utils/third_party_js_util';
15 |
16 | initialize().catch(function logError(err) {
17 | log.error(err);
18 | });
19 |
20 | const config = window.appState;
21 |
22 | function bootReact() {
23 | const browserHistory = createHistory();
24 | const originalHash = browserHistory.location.hash;
25 | browserHistory.location.hash = '';
26 |
27 | const env = get(config, 'config.env');
28 | browserHistory.location.key = config.routing.location.key;
29 | const store = configureStore(browserHistory, config, env);
30 |
31 | /*
32 | browserHistory.listen((location, action) => {
33 | // const url = `${location.pathname}`;
34 | // analytics tracking
35 | });
36 | */
37 |
38 | ThirdPartyJs.setThirdPartyGlobals();
39 |
40 | function renderedApp() {
41 | browserHistory.location.hash = originalHash;
42 | store.dispatch(initialLoadActionCreator());
43 | loadAllThirdPartyJs(env);
44 | document.documentElement.className +=
45 | document.documentElement.className === '' ? 'hydrated' : ' hydrated';
46 | }
47 |
48 | // eslint-disable-next-line no-restricted-globals
49 | const branch = matchRoutes(routes, location.pathname);
50 | const preloadChunks = branch.reduce(function matchMap(list, { route }) {
51 | if (route.preloadChunk) {
52 | list.push(route.preloadChunk);
53 | }
54 | return list;
55 | }, []);
56 |
57 | function hydrateApp() {
58 | try {
59 | hydrate(
60 | ,
61 | window.document,
62 | renderedApp
63 | );
64 | } catch (err) {
65 | // fire ad code here to still show ads
66 | log.fatal(`Unable to render app: ${err.message}`, err.stack);
67 | }
68 | }
69 |
70 | if (preloadChunks.length) {
71 | P.all(preloadChunks.map(chunk => chunk())).then(hydrateApp);
72 | } else {
73 | hydrateApp();
74 | }
75 |
76 | if (module.hot) {
77 | module.hot.accept(
78 | ['../react_router/react_router', '../views/containers/root_container'],
79 | () => {
80 | // eslint-disable-next-line global-require
81 | const HotLoadRoot = require('../views/containers/root_container')
82 | .default;
83 | render(
84 | ,
85 | window.document
86 | );
87 | }
88 | );
89 | }
90 | }
91 |
92 | const img = document.createElement('img');
93 | const supportSrcset = 'srcset' in img && 'sizes' in img;
94 |
95 | if (
96 | !window.Map ||
97 | !window.Set ||
98 | !window.requestAnimationFrame ||
99 | !supportSrcset
100 | ) {
101 | scriptJS(
102 | [
103 | 'https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js',
104 | 'https://cdn.jsdelivr.net/picturefill/3.0.3/picturefill.min.js',
105 | 'https://cdnjs.cloudflare.com/ajax/libs/dom4/1.8.3/dom4.js'
106 | ],
107 | bootReact
108 | );
109 | } else {
110 | bootReact();
111 | }
112 |
--------------------------------------------------------------------------------
/src/client/services/logger_service.js:
--------------------------------------------------------------------------------
1 | // TODO: Add configuration based logging for different envs
2 |
3 | // todo, configure to use axios
4 | import axios from 'axios';
5 |
6 | /**
7 | * Send log request to site.
8 | * @param {string} level The log level.
9 | * @param {Object} data The log data.
10 | * @param {string} message The log message.
11 | * @private
12 | * @returns {Promise}
13 | */
14 | function log(level, data, message) {
15 | return axios({
16 | url: '/api/v1/log',
17 | type: 'POST',
18 | dataType: 'json',
19 | data: {
20 | level,
21 | data: JSON.stringify(data),
22 | message
23 | }
24 | });
25 | /* TODO: Uncomment when we have analytics
26 | .then(function loggerResponse(response) {
27 | // Track response with analytics
28 | // response.code
29 | }).catch(function loggerErrorResponse(error) {
30 | // Track response with analytics
31 | // error.code
32 | });
33 | */
34 | }
35 |
36 | /**
37 | * Factory to create log methods.
38 | * @param {string} level The log level method name.
39 | * @private
40 | * @returns {function}
41 | */
42 | function createLogFn(level) {
43 | return function logFn(data, message) {
44 | if (!data && !message) {
45 | // console error if in development
46 | return false;
47 | }
48 | if (typeof data === 'string') {
49 | return log(level, {}, data);
50 | }
51 | return log(level, data, message);
52 | };
53 | }
54 |
55 | /**
56 | * Send log request to server side.
57 | * @typedef {function} logMethod
58 | * @param {Object|string} data The log data or message.
59 | * @param {string=} message The log message if the log data was specified.
60 | * @returns {Promise}
61 | */
62 |
63 | /**
64 | * Methods: trace, debug, info, warn, error, fatal.
65 | *
66 | * See methods API: https://github.com/trentm/node-bunyan#log-method-api
67 | *
68 | * @example
69 | * var log = require('./services/logger');
70 | * log.error({ stack: err.stack }, 'Descriptive Error message' );
71 | *
72 | * @module services/logger
73 | */
74 | export default {
75 | /**
76 | * @name module:services/logger.trace
77 | * @type {logMethod}
78 | */
79 | trace: createLogFn('trace'),
80 |
81 | /**
82 | * @name module:services/logger.debug
83 | * @type {logMethod}
84 | */
85 | debug: createLogFn('debug'),
86 |
87 | /**
88 | * @name module:services/logger.info
89 | * @type {logMethod}
90 | */
91 | info: createLogFn('info'),
92 |
93 | /**
94 | * @name module:services/logger.warn
95 | * @type {logMethod}
96 | */
97 | warn: createLogFn('warn'),
98 |
99 | /**
100 | * @name module:services/logger.error
101 | * @type {logMethod}
102 | */
103 | error: createLogFn('error'),
104 |
105 | /**
106 | * @name module:services/logger.fatal
107 | * @type {logMethod}
108 | */
109 | fatal: createLogFn('fatal')
110 | };
111 |
--------------------------------------------------------------------------------
/src/client/styles/framework/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin visually-hidden {
2 | border: 0;
3 | clip: rect(0 0 0 0);
4 | height: 1px;
5 | margin: -1px;
6 | overflow: hidden;
7 | padding: 0;
8 | position: absolute;
9 | width: 1px;
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/client/styles/framework/_typography.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'AmbleLight';
3 | src: url('../assets/fonts/amble-light-webfont.woff2') format('woff2'), url('../assets/fonts/amble-light-webfont.woff') format('woff'); /* Pretty Modern Browsers */
4 | }
5 |
6 | @font-face {
7 | font-family: 'Amble';
8 | src: url('../assets/fonts/amble-regular-webfont.woff2') format('woff2'), url('../assets/fonts/amble-regular-webfont.woff') format('woff'); /* Pretty Modern Browsers */
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/styles/main.scss:
--------------------------------------------------------------------------------
1 | // Framework
2 | @import '../../../node_modules/normalize-scss/sass/normalize';
3 |
4 | @include normalize;
5 |
6 | @import 'variables/colors';
7 | @import 'framework/mixins';
8 | @import 'framework/typography';
9 | @import 'framework/foundation_settings';
10 | @import '../../../node_modules/foundation-sites/scss/foundation';
11 |
12 | @include foundation-flex-grid;
13 | @include foundation-responsive-embed;
14 |
15 | // External Libs
16 | @import '../../../node_modules/font-awesome/scss/font-awesome';
17 |
18 | //@import '../../../node_modules/slick-carousel/slick/slick.scss';
19 |
20 | // Layouts
21 | @import '../../views/containers/layouts/layout';
22 | // Pages
23 | @import '../../views/containers/pages/index_page/index';
24 | @import '../../views/containers/pages/about_page/about';
25 | @import '../../views/containers/pages/repo_detail_page/repo_detail';
26 | @import '../../views/containers/pages/not_found_page/not_found';
27 | @import '../../views/containers/pages/search_results_page/search_results';
28 |
29 | // Components
30 | @import '../../views/components/header/header';
31 | @import '../../views/components/nav/nav';
32 | @import '../../views/components/footer/footer';
33 |
34 |
--------------------------------------------------------------------------------
/src/client/styles/variables/_colors.scss:
--------------------------------------------------------------------------------
1 | $purple: rgb(123, 0, 81);
2 | $red: rgb(255, 90, 95);
3 | $light-red: rgba($red, 0.2);
4 | $yellow: rgb(253, 179, 43);
5 | $gold: rgb(180, 167, 108);
6 | $blue: rgb(56, 151, 240);
7 | $turquoise: rgb(85, 222, 211);
8 | $green: rgb(0, 166, 153);
9 | $aqua: rgb(0, 122, 135);
10 | $black: rgb(72, 72, 72);
11 | $gray: #808080;
12 | $light-gray: #d3d3d3;
13 | $white: #fdfff9;
14 |
--------------------------------------------------------------------------------
/src/client/utils/font_loader_util.js:
--------------------------------------------------------------------------------
1 | import FontFaceObserver from 'fontfaceobserver';
2 | import P from 'bluebird';
3 |
4 | if (!window.Promise) {
5 | window.Promise = P;
6 | }
7 |
8 | export default class FontLoaderUtil {
9 | static loadFonts() {
10 | const font = new FontFaceObserver('AmbleLight');
11 | const font2 = new FontFaceObserver('Amble');
12 | const fontPromise = P.all([font.load(), font2.load()]);
13 | return fontPromise
14 | .then(function fontLoadSuccess() {
15 | document.documentElement.className +=
16 | document.documentElement.className === ''
17 | ? 'fonts-loaded'
18 | : ' fonts-loaded';
19 | })
20 | .catch(function fontLoadFail(/* err */) {
21 | document.documentElement.className +=
22 | document.documentElement.className === ''
23 | ? 'fonts-loaded fonts-loaded-error'
24 | : ' fonts-loaded fonts-loaded-error';
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/client/utils/initializer_util.js:
--------------------------------------------------------------------------------
1 | // import log from '../services/logger_service';
2 | import P from 'bluebird';
3 | import FontLoaderUtil from '../utils/font_loader_util';
4 |
5 | export default function initialize() {
6 | return P.all([FontLoaderUtil.loadFonts()]);
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/utils/third_party_js_util.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars, func-names, no-undef, prefer-rest-params, no-underscore-dangle */
2 | import scriptJS from 'scriptjs';
3 | import P from 'bluebird';
4 |
5 | export class ThirdPartyJs {
6 | // config here: https://analytics.google.com/analytics/web/#management/Settings/a45063514w75598220p78098335/%3Fm.page%3DPropertySettings/
7 | static loadGA(env) {
8 | // https://developers.google.com/analytics/devguides/collection/analyticsjs/
9 | /*
10 | ga.l = +new Date;
11 | ga('create', '', 'auto');
12 | ga('require', 'eventTracker');
13 | ga('require', 'cleanUrlTracker');
14 | ga('require', 'impressionTracker');
15 | ga('require', 'mediaQueryTracker');
16 | ga('require', 'outboundFormTracker');
17 | ga('require', 'outboundLinkTracker');
18 | ga('require', 'pageVisibilityTracker');
19 | ga('require', 'socialWidgetTracker');
20 | ga('require', 'urlChangeTracker');
21 | return scriptJS('https://www.google-analytics.com/analytics.js');
22 | */
23 | }
24 |
25 | static loadFB(env) {
26 | // https://developers.facebook.com/docs/plugins/like-button
27 | // return scriptJS('https://connect.facebook.net/en_US/sdk.js');
28 | }
29 |
30 | static loadTwitter(env) {
31 | // https://dev.twitter.com/web/tweet-button
32 | // return scriptJS('https://platform.twitter.com/widgets.js');
33 | }
34 |
35 | static loadPinterest(env) {
36 | // https://developers.pinterest.com/docs/sdks/js/
37 | // return scriptJS('https://assets.pinterest.com/js/pinit.js');
38 | }
39 |
40 | static loadGoogleApi(env) {
41 | // https://developers.google.com/+/web/+1button/
42 | // return scriptJS('https://apis.google.com/js/client:platform.js');
43 | }
44 |
45 | static loadGoogleTag(env) {
46 | // https://support.google.com/dfp_premium/answer/1638622?hl=en
47 | // return scriptJS('https://www.googletagservices.com/tag/js/gpt.js');
48 | }
49 |
50 | static fbTracking(env) {
51 | /*
52 | // https://www.facebook.com/ads/manager/pixel/facebook_pixel/?act=1375631352670761&pid=p1
53 | if (window.fbq) {
54 | return false;
55 | }
56 | window.fbq = function () {
57 | window.fbq.callMethod ?
58 | window.fbq.callMethod.apply(window.fbq, arguments) : window.fbq.queue.push(arguments);
59 | };
60 |
61 | if (!window._fbq) {
62 | window._fbq = window.fbq;
63 | }
64 | window.fbq.push = window.fbq;
65 | window.fbq.loaded = !0;
66 | window.fbq.version = '2.0';
67 | window.fbq.queue = [];
68 |
69 |
70 | window.fbq('init', '');
71 | window.fbq('track', 'PageView');
72 |
73 | return scriptJS('https://connect.facebook.net/en_US/fbevents.js');
74 | */
75 | }
76 |
77 | static setThirdPartyGlobals(env) {
78 | window.ga =
79 | window.ga ||
80 | function() {
81 | (ga.q = ga.q || []).push(arguments);
82 | };
83 |
84 | window.fbAsyncInit = function facebookInitialized() {
85 | FB.init({
86 | appId: '',
87 | xfbml: true,
88 | version: 'v2.7'
89 | });
90 | };
91 |
92 | window.twttr = window.twttr || {};
93 |
94 | window.___gcfg = {
95 | lang: 'en',
96 | parsetags: 'explicit'
97 | };
98 |
99 | window.googletag = window.googletag || {};
100 | window.googletag.cmd = window.googletag.cmd || [];
101 |
102 | window.amzn_ps_tracking_id = '';
103 | window.amzn_ps_instance_id = '';
104 | }
105 |
106 | static loadAdSense(env) {
107 | (window.adsbygoogle = window.adsbygoogle || []).push({
108 | google_ad_client: '',
109 | enable_page_level_ads: true
110 | });
111 | // return scriptJS('https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js');
112 | }
113 |
114 | static loadSentry(env) {
115 | /*
116 | if(env !== 'development') {
117 | return scriptJS('https://cdn.ravenjs.com/3.7.0/raven.min.js', function configureRaven() {
118 | window.Raven
119 | .config('')
120 | .install();
121 | });
122 | }
123 | */
124 | }
125 |
126 | static loadAmazonAdSystem(env) {
127 | // return scriptJS('https://ps-us.amazon-adsystem.com/scripts/US/studio.js');
128 | }
129 | }
130 |
131 | export function loadAllThirdPartyJs(env) {
132 | return P.all([
133 | // ThirdPartyJs.loadAdSense(env),
134 | // ThirdPartyJs.loadGA(env),
135 | // ThirdPartyJs.loadFB(env),
136 | // ThirdPartyJs.loadTwitter(env),
137 | // ThirdPartyJs.loadPinterest(env),
138 | // ThirdPartyJs.loadGoogleApi(env),
139 | // ThirdPartyJs.loadGoogleTag(env),
140 | // ThirdPartyJs.fbTracking(env),
141 | // ThirdPartyJs.loadDisqus(env),
142 | // ThirdPartyJs.loadSentry(env),
143 | // ThirdPartyJs.loadAmazonAdSystem(env)
144 | ]);
145 | }
146 |
--------------------------------------------------------------------------------
/src/models/repo_detail.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default class RepoDetail {
4 | static fetch({ title, user }, state) {
5 | const host = state.config.apiUrl;
6 | return axios.get(`https://${host}/repos/${user}/${title}`);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/models/search.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default class Search {
4 | static fetch(query, state) {
5 | const host = state.config.apiUrl;
6 | return axios.get(`http://${host}/search/repositories?q=${query}`);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/react_router/react_router.js:
--------------------------------------------------------------------------------
1 | // Page Containers with components
2 | import Layout from '../views/containers/layouts/layout';
3 |
4 | // Data handle async / sync data fetching for page
5 | import IndexPageRoute from '../views/containers/pages/index_page/index_page_route';
6 | import searchResultsRoute from '../views/containers/pages/search_results_page/search_results_route';
7 | import aboutRoute from '../views/containers/pages/about_page/about_route';
8 | import repoDetailRoute from '../views/containers/pages/repo_detail_page/repo_detail_route';
9 | import notFoundRoute from '../views/containers/pages/not_found_page/not_found_route';
10 |
11 | export default [
12 | {
13 | component: Layout,
14 | routes: [
15 | aboutRoute,
16 | searchResultsRoute,
17 | repoDetailRoute,
18 | IndexPageRoute,
19 | notFoundRoute
20 | ]
21 | }
22 | ];
23 |
--------------------------------------------------------------------------------
/src/redux/action_creators/about/about_page_action_creators.js:
--------------------------------------------------------------------------------
1 | export function aboutPageLoading(state = {}) {
2 | return {
3 | type: 'ABOUT_PAGE_LOADING',
4 | isLoading: true,
5 | state
6 | };
7 | }
8 |
9 | export function aboutPageLoaded(state = {}) {
10 | return {
11 | type: 'ABOUT_PAGE_LOADED',
12 | isLoading: false,
13 | state
14 | };
15 | }
16 |
17 | export function aboutPageLoadError(state = {}, error) {
18 | return {
19 | type: 'ABOUT_PAGE_ERROR',
20 | isLoading: false,
21 | state,
22 | error
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/action_creators/index/index_page_action_creators.js:
--------------------------------------------------------------------------------
1 | export function indexPageLoading(state = {}) {
2 | return {
3 | type: 'INDEX_PAGE_LOADING',
4 | isLoading: true,
5 | state
6 | };
7 | }
8 |
9 | export function indexPageLoaded(state = {}) {
10 | return {
11 | type: 'INDEX_PAGE_LOADED',
12 | isLoading: false,
13 | state
14 | };
15 | }
16 |
17 | export function indexPageLoadError(state = {}, error) {
18 | return {
19 | type: 'INDEX_PAGE_ERROR',
20 | isLoading: false,
21 | state,
22 | error
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/action_creators/initial_load_action_creator.js:
--------------------------------------------------------------------------------
1 | export default function() {
2 | return {
3 | type: 'INITIAL_PAGE_LOAD',
4 | initialPageLoad: false
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/redux/action_creators/not_found_status_action_creator.js:
--------------------------------------------------------------------------------
1 | export default function(status, action) {
2 | return {
3 | type: action,
4 | status
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/redux/action_creators/repo_detail/repo_detail_action_creators.js:
--------------------------------------------------------------------------------
1 | // import { canUseDOM } from 'exenv';
2 |
3 | export function repoDetailLoading(repo = {}) {
4 | return {
5 | type: 'REPO_DETAIL_LOADING',
6 | repo
7 | };
8 | }
9 |
10 | export function repoDetailLoaded(repo = {}) {
11 | return {
12 | type: 'REPO_DETAIL_LOADED',
13 | repo
14 | };
15 | }
16 |
17 | export function repoDetailLoadError(error) {
18 | return {
19 | type: 'REPO_DETAIL_ERROR',
20 | repo: {
21 | isLoading: false,
22 | error: true,
23 | errorMessage: error ? error.message : 'no message'
24 | }
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/redux/action_creators/repo_detail/repo_detail_page_action_creators.js:
--------------------------------------------------------------------------------
1 | // import { canUseDOM } from 'exenv';
2 |
3 | export function repoDetailPageLoading(state = {}) {
4 | return {
5 | type: 'REPO_DETAIL_PAGE_LOADING',
6 | isLoading: true,
7 | state
8 | };
9 | }
10 |
11 | export function repoDetailPageLoaded(state, data) {
12 | return {
13 | type: 'REPO_DETAIL_PAGE_LOADED',
14 | isLoading: false,
15 | state,
16 | data
17 | };
18 | }
19 |
20 | export function repoDetailPageLoadError(state = {}, error) {
21 | return {
22 | type: 'REPO_DETAIL_PAGE_ERROR',
23 | isLoading: false,
24 | state,
25 | error
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/redux/action_creators/search/search_action_creators.js:
--------------------------------------------------------------------------------
1 | // https://api.github.com/searchs/michaelbenin
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export function searchLoading(search = {}, state) {
5 | return {
6 | type: 'SEARCH_LOADING',
7 | isLoading: true,
8 | search,
9 | state
10 | };
11 | }
12 |
13 | export function searchLoaded(search, state) {
14 | return {
15 | type: 'SEARCH_LOADED',
16 | isLoading: false,
17 | search,
18 | state
19 | };
20 | }
21 |
22 | export function searchError(error, state) {
23 | return {
24 | type: 'SEARCH_ERROR',
25 | isLoading: false,
26 | error,
27 | state
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/redux/action_creators/search/search_page_action_creators.js:
--------------------------------------------------------------------------------
1 | // https://api.github.com/searchs/michaelbenin
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export function searchPageLoading(state = {}) {
5 | return {
6 | type: 'SEARCH_PAGE_LOADING',
7 | isLoading: true,
8 | state
9 | };
10 | }
11 |
12 | export function searchPageLoaded(state = {}) {
13 | return {
14 | type: 'SEARCH_PAGE_LOADED',
15 | isLoading: false,
16 | state
17 | };
18 | }
19 |
20 | export function searchPageError(error, state = {}) {
21 | return {
22 | type: 'SEARCH_PAGE_ERROR',
23 | isLoading: false,
24 | state,
25 | error
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/redux/actions/about/async_about_page_actions.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import * as aboutPageActions from '../../action_creators/about/about_page_action_creators';
3 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
4 | import log from '../../../services/logger_service';
5 |
6 | export default function fetchAboutData(params, dispatch, state) {
7 | dispatch(aboutPageActions.aboutPageLoading());
8 | return P.all([
9 | // This is a static page, but if you needed data
10 | ])
11 | .then(function handleIndexPageDataLoaded() {
12 | dispatch(aboutPageActions.aboutPageLoaded());
13 | })
14 | .catch(function handleUserError(err) {
15 | log.error(err, 'Error in fetching repo detail page.');
16 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
17 | dispatch(aboutPageActions.aboutPageLoadError(err, state));
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/redux/actions/index/async_index_page_actions.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import * as indexPageActions from '../../action_creators/index/index_page_action_creators';
3 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
4 | import log from '../../../services/logger_service';
5 |
6 | export default function fetchIndexData(params, dispatch, state) {
7 | dispatch(indexPageActions.indexPageLoading());
8 | return P.all([
9 | // This is a static page, but if you needed data
10 | ])
11 | .then(function handleIndexPageDataLoaded() {
12 | dispatch(indexPageActions.indexPageLoaded());
13 | })
14 | .catch(function handleUserError(err) {
15 | log.error(err, 'Error in fetching repo detail page.');
16 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
17 | dispatch(indexPageActions.indexPageLoadError(err, state));
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/redux/actions/repo_detail/async_repo_detail_actions.js:
--------------------------------------------------------------------------------
1 | import * as repoDetailActions from '../../action_creators/repo_detail/repo_detail_action_creators';
2 | import RepoDetailModel from '../../../models/repo_detail';
3 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
4 | import log from '../../../services/logger_service';
5 |
6 | export default function fetchRepoDetail(params, dispatch, state) {
7 | dispatch(repoDetailActions.repoDetailLoading());
8 | return RepoDetailModel.fetch(params, state)
9 | .then(function handleRepoDetailData(repoData) {
10 | dispatch(repoDetailActions.repoDetailLoaded(repoData.data, state));
11 | return repoData.data;
12 | })
13 | .catch(function handleUserError(err) {
14 | log.error(err, 'Error in fetching repo.');
15 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
16 | dispatch(repoDetailActions.repoDetailLoadError(err, state));
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/redux/actions/repo_detail/async_repo_detail_page_actions.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import * as repoDetailPageActions from '../../action_creators/repo_detail/repo_detail_page_action_creators';
3 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
4 | import log from '../../../services/logger_service';
5 |
6 | import repoDetailAsyncAction from './async_repo_detail_actions';
7 |
8 | export default function fetchRepoDetail(params, dispatch, state) {
9 | dispatch(repoDetailPageActions.repoDetailPageLoading());
10 | return P.all([repoDetailAsyncAction(params, dispatch, state)])
11 | .then(function handleRepoDetailData(actions) {
12 | dispatch(repoDetailPageActions.repoDetailPageLoaded(state, actions));
13 | })
14 | .catch(function handleUserError(err) {
15 | log.error(err, 'Error in fetching repo detail page.');
16 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
17 | dispatch(repoDetailPageActions.repoDetailPageLoadError(err, state));
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/redux/actions/search/async_search_actions.js:
--------------------------------------------------------------------------------
1 | // https://api.github.com/searchs/michaelbenin
2 | // import { canUseDOM } from 'exenv';
3 | import * as searchActions from '../../action_creators/search/search_action_creators';
4 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
5 | import SearchModel from '../../../models/search';
6 | import log from '../../../services/logger_service';
7 |
8 | export default function fetchSearchAction(query, dispatch, state) {
9 | dispatch(searchActions.searchLoading());
10 | return SearchModel.fetch(query, state)
11 | .then(function handleSearchModelData(searchData) {
12 | dispatch(searchActions.searchLoaded(searchData.data, state));
13 | })
14 | .catch(function handleSearchError(err) {
15 | log.error(err, 'Error in fetching repo.');
16 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
17 | dispatch(searchActions.searchError(err, state));
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/redux/actions/search/async_search_page_actions.js:
--------------------------------------------------------------------------------
1 | // https://api.github.com/searchs/michaelbenin
2 | // import { canUseDOM } from 'exenv';
3 | import P from 'bluebird';
4 | import * as searchPageActions from '../../action_creators/search/search_page_action_creators';
5 | import notFoundActionCreator from '../../action_creators/not_found_status_action_creator';
6 | import log from '../../../services/logger_service';
7 | import searchAsyncAction from './async_search_actions';
8 |
9 | export default function fetchRepoDetail(params, dispatch, state) {
10 | dispatch(searchPageActions.searchPageLoading());
11 | return P.all([searchAsyncAction(params, dispatch, state)])
12 | .then(function handleRepoDetailData() {
13 | dispatch(searchPageActions.searchPageLoaded());
14 | })
15 | .catch(function handleUserError(err) {
16 | log.error(err, 'Error in search page.');
17 | dispatch(notFoundActionCreator(500, 'ERROR_STATUS'));
18 | dispatch(searchPageActions.searchPageLoadError(err, state));
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/redux/reducers/about/about_page_reducer.js:
--------------------------------------------------------------------------------
1 | export default function(state = {}, action) {
2 | const typeMap = {
3 | ABOUT_PAGE_LOADING() {
4 | return {
5 | isLoading: true
6 | };
7 | },
8 | ABOUT_PAGE_LOADED() {
9 | return {
10 | isLoading: false
11 | };
12 | },
13 | ABOUT_PAGE_ERROR() {
14 | return {
15 | isLoading: false,
16 | error: action.error
17 | };
18 | }
19 | };
20 |
21 | if (typeMap[action.type]) {
22 | return typeMap[action.type]();
23 | }
24 |
25 | return state;
26 | }
27 |
--------------------------------------------------------------------------------
/src/redux/reducers/config_reducer.js:
--------------------------------------------------------------------------------
1 | export default function(state = {}, action) {
2 | if (action.type === 'INITIAL_PAGE_LOAD') {
3 | return Object.assign({}, state, {
4 | initialPageLoad: false
5 | });
6 | }
7 | return state;
8 | }
9 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer as routing } from 'react-router-redux';
3 |
4 | import config from './config_reducer';
5 | import meta from './meta_reducer';
6 | import status from './status_reducer';
7 |
8 | import repoDetail from './repo_detail/repo_detail_reducer';
9 | import repoDetailPage from './repo_detail/repo_detail_page_reducer';
10 |
11 | import searchPage from './search/search_page_reducer';
12 | import search from './search/search_results_reducer';
13 |
14 | import indexPage from './index/index_page_reducer';
15 | import aboutPage from './about/about_page_reducer';
16 |
17 | export default combineReducers({
18 | routing,
19 |
20 | config,
21 | meta,
22 | status,
23 |
24 | indexPage,
25 | aboutPage,
26 |
27 | repoDetailPage,
28 | repoDetail,
29 |
30 | searchPage,
31 | search
32 | });
33 |
--------------------------------------------------------------------------------
/src/redux/reducers/index/index_page_reducer.js:
--------------------------------------------------------------------------------
1 | export default function(state = {}, action) {
2 | const typeMap = {
3 | INDEX_PAGE_LOADING() {
4 | return {
5 | isLoading: true
6 | };
7 | },
8 | INDEX_PAGE_LOADED() {
9 | return {
10 | isLoading: false
11 | };
12 | },
13 | INDEX_PAGE_ERROR() {
14 | return {
15 | isLoading: false,
16 | error: action.error
17 | };
18 | }
19 | };
20 |
21 | if (typeMap[action.type]) {
22 | return typeMap[action.type]();
23 | }
24 |
25 | return state;
26 | }
27 |
--------------------------------------------------------------------------------
/src/redux/reducers/meta_reducer.js:
--------------------------------------------------------------------------------
1 | import repoDetailSeoUtil from '../../utils/seo_util/repo_detail_seo_util';
2 | import indexSeoUtil from '../../utils/seo_util/index_seo_util';
3 | import searchSeoUtil from '../../utils/seo_util/search_seo_util';
4 | import aboutSeoUtil from '../../utils/seo_util/about_seo_util';
5 |
6 | const typeMap = {
7 | INDEX_PAGE_LOADED(state, action) {
8 | return indexSeoUtil(state, action);
9 | },
10 |
11 | ABOUT_PAGE_LOADED(state, action) {
12 | return aboutSeoUtil(state, action);
13 | },
14 |
15 | REPO_DETAIL_PAGE_LOADED({ state, data }) {
16 | return repoDetailSeoUtil(state, data);
17 | },
18 |
19 | SEARCH_PAGE_LOADED(state, action) {
20 | return searchSeoUtil(state, action);
21 | }
22 | };
23 |
24 | export default function(state = {}, action) {
25 | const { type } = action;
26 |
27 | if (typeMap[type]) {
28 | return typeMap[type](action);
29 | }
30 |
31 | return state;
32 | }
33 |
--------------------------------------------------------------------------------
/src/redux/reducers/repo_detail/repo_detail_page_reducer.js:
--------------------------------------------------------------------------------
1 | // https://github.com/reactjs/redux/issues/99
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export default function(state = {}, action) {
5 | const typeMap = {
6 | REPO_DETAIL_PAGE_LOADING() {
7 | return {
8 | isLoading: true
9 | };
10 | },
11 | REPO_DETAIL_PAGE_LOADED() {
12 | return {
13 | isLoading: false
14 | };
15 | },
16 | REPO_DETAIL_PAGE_ERROR() {
17 | return {
18 | isLoading: false,
19 | error: action.error
20 | };
21 | }
22 | };
23 |
24 | if (typeMap[action.type]) {
25 | return typeMap[action.type]();
26 | }
27 |
28 | return state;
29 | }
30 |
--------------------------------------------------------------------------------
/src/redux/reducers/repo_detail/repo_detail_reducer.js:
--------------------------------------------------------------------------------
1 | // https://github.com/reactjs/redux/issues/99
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export default function(state = {}, action) {
5 | const typeMap = {
6 | REPO_DETAIL_LOADING() {
7 | return {
8 | isLoading: true
9 | };
10 | },
11 | REPO_DETAIL_LOADED() {
12 | return {
13 | isLoading: false,
14 | repo: action.repo
15 | };
16 | },
17 | REPO_DETAIL_ERROR() {
18 | return {
19 | isLoading: false,
20 | error: action.error
21 | };
22 | }
23 | };
24 |
25 | if (typeMap[action.type]) {
26 | return typeMap[action.type]();
27 | }
28 |
29 | return state;
30 | }
31 |
--------------------------------------------------------------------------------
/src/redux/reducers/search/search_page_reducer.js:
--------------------------------------------------------------------------------
1 | // https://github.com/reactjs/redux/issues/99
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export default function(state = {}, action) {
5 | const typeMap = {
6 | SEARCH_PAGE_LOADING() {
7 | return {
8 | isLoading: true
9 | };
10 | },
11 | SEARCH_PAGE_LOADED() {
12 | return {
13 | isLoading: false
14 | };
15 | },
16 | SEARCH_PAGE_ERROR() {
17 | return {
18 | isLoading: false,
19 | error: action.error
20 | };
21 | }
22 | };
23 |
24 | if (typeMap[action.type]) {
25 | return typeMap[action.type]();
26 | }
27 |
28 | return state;
29 | }
30 |
--------------------------------------------------------------------------------
/src/redux/reducers/search/search_results_reducer.js:
--------------------------------------------------------------------------------
1 | // https://github.com/reactjs/redux/issues/99
2 | // import { canUseDOM } from 'exenv';
3 |
4 | export default function(state = {}, action) {
5 | const typeMap = {
6 | SEARCH_LOADING() {
7 | return {
8 | isLoading: true
9 | };
10 | },
11 | SEARCH_LOADED() {
12 | return {
13 | isLoading: false,
14 | response: action.search
15 | };
16 | },
17 | SEARCH_ERROR() {
18 | return {
19 | isLoading: false,
20 | error: action.error
21 | };
22 | }
23 | };
24 |
25 | if (typeMap[action.type]) {
26 | return typeMap[action.type]();
27 | }
28 |
29 | return state;
30 | }
31 |
--------------------------------------------------------------------------------
/src/redux/reducers/status_reducer.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import extend from 'lodash/extend';
3 |
4 | export default function(
5 | state = {
6 | code: 200
7 | },
8 | action
9 | ) {
10 | if (action.type === 'NOT_FOUND_STATUS') {
11 | return {
12 | code: 404
13 | };
14 | }
15 |
16 | if (action.type === 'ERROR_STATUS') {
17 | return {
18 | code: 500
19 | };
20 | }
21 |
22 | if (/PAGE_LOADING/.test(action.type) && canUseDOM) {
23 | return extend({}, state, {
24 | code: 200
25 | });
26 | }
27 |
28 | return state;
29 | }
30 |
--------------------------------------------------------------------------------
/src/redux/store/store.js:
--------------------------------------------------------------------------------
1 | // https://raw.githubusercontent.com/reactjs/react-router-redux/master/examples/server/store.js
2 | import { createStore, compose, applyMiddleware } from 'redux';
3 | // https://github.com/gaearon/redux-thunk
4 | import thunk from 'redux-thunk';
5 | import { routerMiddleware } from 'react-router-redux';
6 | import reducer from '../reducers';
7 |
8 | export default function configureStore(history, initialState = {}, env) {
9 | let store = {}; // eslint-disable-line import/no-mutable-exports
10 |
11 | // eslint-disable-next-line no-param-reassign
12 | initialState.config.initialQueryParams = JSON.parse(
13 | initialState.config.initialQueryParams
14 | );
15 |
16 | if (env !== 'development') {
17 | store = createStore(
18 | reducer,
19 | initialState,
20 | compose(
21 | applyMiddleware(routerMiddleware(history)),
22 | applyMiddleware(thunk)
23 | )
24 | );
25 | } else {
26 | const composeEnhancers =
27 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // eslint-disable-line no-underscore-dangle, max-len
28 | store = createStore(
29 | reducer,
30 | initialState,
31 | composeEnhancers(
32 | applyMiddleware(routerMiddleware(history)),
33 | applyMiddleware(thunk)
34 | )
35 | );
36 | }
37 |
38 | if (module.hot) {
39 | // Enable Webpack hot module replacement for reducers
40 | module.hot.accept('../reducers', () => {
41 | const nextReducer = require('../reducers').default; // eslint-disable-line global-require
42 | store.replaceReducer(nextReducer);
43 | });
44 | }
45 |
46 | return store;
47 | }
48 |
--------------------------------------------------------------------------------
/src/server/config/base.js:
--------------------------------------------------------------------------------
1 | import PrettyStream from 'bunyan-prettystream'; // eslint-disable-line import/no-extraneous-dependencies
2 | import '../utils/load_env_var_util';
3 | import featureFlags from '../../../features_flags';
4 |
5 | const prettyStdOut = new PrettyStream();
6 | prettyStdOut.pipe(process.stdout);
7 |
8 | const {
9 | // CI,
10 | // AWS_ACCESS_KEY_ID,
11 | // AWS_SECRET_ACCESS_KEY,
12 | GIT_COMMIT,
13 | REDIS_PORT,
14 | REDIS_HOST,
15 | STATIC_URL,
16 | STATIC_VENDOR_URL,
17 | STATIC_BUNDLE_URL
18 | } = process.env;
19 |
20 | export default {
21 | featureFlags,
22 | cacheEnabled: false,
23 | redis: {
24 | port: REDIS_PORT,
25 | host: REDIS_HOST
26 | },
27 | port: 8001,
28 | logMiddleware: {
29 | streams: [
30 | {
31 | path: 'logs/express-middleware-development.log'
32 | }
33 | ]
34 | },
35 | bunyanLogger: {
36 | name: 'react-ssr-spa',
37 | streams: [
38 | {
39 | level: 'debug',
40 | type: 'raw',
41 | stream: prettyStdOut
42 | }
43 | ]
44 | },
45 | gitCommit: GIT_COMMIT,
46 | pm2WebInterface: '127.0.0.1:9615',
47 | websocket: 'ws://127.0.0.1:3000',
48 | websocketPort: 3000,
49 | staticUrl: `${STATIC_URL}/${GIT_COMMIT}`,
50 | staticBundleUrl: `${STATIC_URL}/${STATIC_BUNDLE_URL}`,
51 | staticVendorUrl: `${STATIC_URL}/${STATIC_VENDOR_URL}`,
52 | apiUrl: 'api.github.com', // '127.0.0.1:8000'
53 | host: '127.0.0.1:8001',
54 | protocol: 'http://'
55 | };
56 |
--------------------------------------------------------------------------------
/src/server/config/development.js:
--------------------------------------------------------------------------------
1 | export default {
2 | cacheEnabled: false,
3 | staticUrl: ''
4 | // apiUrl: '127.0.0.1:8000'
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/config/index.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import _ from 'lodash';
3 | import base from './base';
4 |
5 | /**
6 | * Configuration module which allows a centralized location for retrieving config properties.
7 | * @module config
8 | */
9 | // Defaults configuration to development if not provided
10 | if (!process.env.NODE_ENV) {
11 | process.env.NODE_ENV = 'development';
12 | }
13 |
14 | const env =
15 | process.env.NODE_ENV === 'coverage' ? 'development' : process.env.NODE_ENV;
16 | const configPath = path.join(__dirname, env);
17 |
18 | const envConfig = require(configPath); // eslint-disable-line import/no-dynamic-require
19 | const configuration = _.defaultsDeep(
20 | {
21 | env
22 | },
23 | envConfig.default,
24 | base
25 | );
26 |
27 | class Config {
28 | static get(property) {
29 | return _.get(configuration, property);
30 | }
31 |
32 | static set(property, value) {
33 | return _.set(configuration, property, value);
34 | }
35 | }
36 |
37 | export default Config;
38 |
--------------------------------------------------------------------------------
/src/server/config/production.js:
--------------------------------------------------------------------------------
1 | // Production settings (should be as close to staging as possible)
2 |
3 | export default {};
4 |
--------------------------------------------------------------------------------
/src/server/config/test.js:
--------------------------------------------------------------------------------
1 | export default {
2 | cacheEnabled: false,
3 | staticUrl: ''
4 | // apiUrl: '127.0.0.1:8000'
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/controllers/get_ping_controller.js:
--------------------------------------------------------------------------------
1 | export default (req, res) => {
2 | res.send('pong');
3 | };
4 |
--------------------------------------------------------------------------------
/src/server/controllers/get_robots_txt_controller.js:
--------------------------------------------------------------------------------
1 | export default (req, res) => {
2 | res.type('text/plain');
3 | res.send(
4 | `User-agent: *
5 | Allow: /
6 |
7 | Sitemap: http://react-ssr-spa/sitemap.xml`
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/server/controllers/post_log_controller.js:
--------------------------------------------------------------------------------
1 | import { get as _get, extend } from 'lodash';
2 | import log from '../services/logger_service';
3 |
4 | // Same levels provided by Bunyan
5 | const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
6 |
7 | /**
8 | * Logging controller
9 | * @module controllers/log
10 | */
11 |
12 | /**
13 | * Middleware for request logger.
14 | *
15 | * @name module:controllers/log.log
16 | * @type {middleware}
17 | */
18 | const loggerController = (req, res) => {
19 | const level = _get(req, 'body.level');
20 | let message = _get(req, 'body.message');
21 | let data = _get(req, 'body.data');
22 | const validLevel = levels.indexOf(level) > -1;
23 |
24 | if (validLevel) {
25 | try {
26 | data = JSON.parse(data);
27 | } catch (e) {
28 | log.error(
29 | {
30 | req: req.url
31 | },
32 | 'Bad json passed from browser log.'
33 | );
34 | data = data || {
35 | data: 'Bad json passed from browser.'
36 | };
37 | }
38 |
39 | message = message || 'Message not provided.';
40 |
41 | extend(data, {
42 | endpoint: 'browserIW'
43 | });
44 |
45 | log[level](data, message);
46 | res.status(200).json({
47 | status: 200
48 | });
49 | return false;
50 | }
51 |
52 | if (!validLevel) {
53 | const failedLogMessage = 'Log attempted with unsupported method.';
54 | log.warn(
55 | {
56 | req: req.url
57 | },
58 | failedLogMessage
59 | );
60 |
61 | res.status(405).json({
62 | status: 405,
63 | message: failedLogMessage
64 | });
65 |
66 | return false;
67 | }
68 |
69 | const unknownFailureMessage = 'Browser log failed for unknown reasons.';
70 |
71 | log.error(
72 | {
73 | req: req.url
74 | },
75 | unknownFailureMessage
76 | );
77 |
78 | res.status(503).json({
79 | status: 503,
80 | message: unknownFailureMessage
81 | });
82 |
83 | return false;
84 | };
85 |
86 | export default loggerController;
87 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | import './utils/install_sourcemap_util';
2 | import config from './config';
3 | import './utils/react_raf_util';
4 | import { app, createOrGetServer, setServer } from './services/express_service';
5 | import errorMiddleware from './middleware/error_middleware';
6 | import log from './services/logger_service';
7 | import initialize from './utils/initializer_util';
8 | import doGracefulExit from './utils/graceful_exit_util';
9 | import './utils/uncaught_exception_util';
10 |
11 | process.send = process.send || (() => {});
12 | const port = config.get('port');
13 | app.use(errorMiddleware);
14 |
15 | const startServer = () => {
16 | const server = createOrGetServer().listen(port, () => {
17 | log.info(
18 | {
19 | port
20 | },
21 | 'Server is running.'
22 | );
23 | });
24 | process.send('ready');
25 | setServer(server);
26 | };
27 |
28 | const serverError = err => {
29 | log.fatal(err, err.message);
30 | doGracefulExit(err);
31 | };
32 |
33 | log.info('Server is booting.');
34 | export default initialize.then(startServer).catch(serverError);
35 |
--------------------------------------------------------------------------------
/src/server/middleware/error_middleware.js:
--------------------------------------------------------------------------------
1 | import config from '../config';
2 | import gracefulExit from '../utils/graceful_exit_util';
3 | import log from '../services/logger_service';
4 |
5 | export default (err, req, res) => {
6 | if (err.code !== 'EBADCSRFTOKEN') {
7 | // handle CSRF token errors here
8 | res.status(403);
9 | return res.send('Form tampered with.');
10 | }
11 | log.fatal(
12 | err,
13 | `Error on request: method: ${req.method},
14 | url: ${req.url}`
15 | );
16 | // Check for dev here etc..
17 | // todo: email/alert dev team
18 | if (config.env === 'development') {
19 | res.status(500).json({
20 | message: err.message,
21 | error: err.stack
22 | });
23 | }
24 | res.status(500).send('Internal Error Occured.');
25 | return gracefulExit(err);
26 | };
27 |
--------------------------------------------------------------------------------
/src/server/middleware/react_router_middleware.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import React from 'react';
3 | import createMemoryHistory from 'history/createMemoryHistory';
4 | import { renderToString } from 'react-dom/server';
5 | import { matchRoutes } from 'react-router-config';
6 | import { existsSync } from 'fs';
7 | import path from 'path';
8 | import serialize from 'serialize-javascript';
9 | import redisClient from '../services/redis_service';
10 | import log from '../services/logger_service';
11 | import routes from '../../react_router/react_router';
12 | import configureStore from '../../redux/store/store';
13 | import Root from '../../views/containers/root_container';
14 | import config from '../config';
15 |
16 | const featureFlags = config.get('featureFlags');
17 | const env = config.get('env');
18 | const staticUrl = config.get('staticUrl');
19 | const apiUrl = config.get('apiUrl');
20 | const cacheEnabled = config.get('cacheEnabled');
21 | const staticVendorUrl = config.get('staticVendorUrl');
22 | const staticBundleUrl = config.get('staticBundleUrl');
23 | const cacheExpire = 60 * 6;
24 |
25 | const manifestPath = path.join(
26 | __dirname,
27 | '../../../dist/static/js/manifest.json'
28 | );
29 |
30 | let manifestJSON = {};
31 |
32 | if (env === 'production' || env === 'test') {
33 | if (existsSync(manifestPath)) {
34 | // eslint-disable-next-line global-require, import/no-dynamic-require
35 | manifestJSON = require(manifestPath);
36 | } else {
37 | log.fatal(`Manifest file not found in middleware: ${manifestPath}`);
38 | }
39 | }
40 |
41 | export default (req, res) => {
42 | const htmlKey = `${req.url}:__html`;
43 | const statusKey = `${req.url}:__status`;
44 |
45 | function returnFromApi() {
46 | const memoryHistory = createMemoryHistory({ initialEntries: [req.url] });
47 | const branch = matchRoutes(routes, req.path);
48 |
49 | const chunks = branch.reduce(function matchMap(list, { route }) {
50 | if (route.chunk) {
51 | list.push(route.chunk);
52 | }
53 | return list;
54 | }, []);
55 |
56 | // Unexpected keys will be ignored.
57 | const store = configureStore(memoryHistory, {
58 | config: {
59 | env,
60 | chunks,
61 | staticUrl,
62 | staticVendorUrl,
63 | staticBundleUrl,
64 | manifestJSON,
65 | apiUrl,
66 | initialPageLoad: true,
67 | featureFlags,
68 | initialQueryParams: serialize(req.query, { isJSON: true }),
69 | navHistory: [`${req.protocol}://${req.hostname}${req.originalUrl}`]
70 | }
71 | });
72 |
73 | const promises = branch.map(function matchMap({ route, match }) {
74 | return route.loadData
75 | ? route.loadData(match, store.dispatch, store.getState())
76 | : P.resolve(null);
77 | });
78 |
79 | P.all(promises)
80 | .then(function hydrateStoreSuccess() {
81 | const status = store.getState().status.code;
82 |
83 | const renderedDOM = `${renderToString(
84 |
85 | )}`;
86 |
87 | // TODO: cache rendered dom in redis
88 | res.writeHead(status, {
89 | 'Content-Type': 'text/html',
90 | 'Access-Control-Allow-Origin': '*'
91 | });
92 |
93 | res.end(renderedDOM);
94 | if (config.get('cacheEnabled')) {
95 | redisClient.setex(htmlKey, cacheExpire, renderedDOM);
96 | redisClient.setex(statusKey, cacheExpire, status);
97 | }
98 | return false;
99 | })
100 | .catch(err => {
101 | log.error(err);
102 | res.status(500).json(err);
103 | });
104 | }
105 |
106 | if (!config.get('cacheEnabled')) {
107 | return returnFromApi();
108 | }
109 |
110 | const redisHtml = redisClient.getAsync(htmlKey);
111 | const redisStatus = redisClient.getAsync(statusKey);
112 |
113 | return P.all([redisStatus, redisHtml])
114 | .then(function returnFromCache(cacheResponse) {
115 | if (!cacheEnabled) {
116 | throw new Error('Cache disabled.');
117 | }
118 |
119 | if (!cacheResponse[0] || !cacheResponse[1]) {
120 | throw new Error('Not in cache');
121 | }
122 | res.writeHead(cacheResponse[0], {
123 | 'Content-Type': 'text/html'
124 | });
125 | return res.end(cacheResponse[1]);
126 | })
127 | .catch(returnFromApi);
128 | };
129 |
--------------------------------------------------------------------------------
/src/server/routes/get_ping_route.js:
--------------------------------------------------------------------------------
1 | import ping from '../controllers/get_ping_controller';
2 |
3 | export default router => {
4 | router.get('/api/v1/ping', ping);
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/routes/get_robots_txt.js:
--------------------------------------------------------------------------------
1 | import robotsController from '../controllers/get_robots_txt_controller';
2 |
3 | export default router => {
4 | router.get('/robots.txt', robotsController);
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/routes/post_log_route.js:
--------------------------------------------------------------------------------
1 | import logController from '../controllers/post_log_controller';
2 |
3 | export default router => {
4 | router.post('/api/v1/log', logController);
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/services/express_service.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import http from 'http';
3 | import https from 'https';
4 | import express from 'express';
5 | import bodyParser from 'body-parser';
6 | import hpp from 'hpp'; // Used in conjunction with urlencoded.
7 | import expressGracefulExit from 'express-graceful-exit';
8 | import expressBunyanLogger from 'express-bunyan-logger';
9 | import responseTime from 'response-time';
10 | import helmet from 'helmet';
11 | import contentValidator from 'express-content-length-validator';
12 | import config from '../config';
13 | import router from './router_service';
14 |
15 | import reactRouterMiddleware from '../middleware/react_router_middleware';
16 |
17 | const env = config.get('env');
18 |
19 | /**
20 | * Concurrent sockets the agent can have open per origin.
21 | * Defaults to Infinity. {@link http://nodejs.org/api/http.html#http_agent_maxsockets More Info}
22 | * @type {number}
23 | */
24 | https.globalAgent.maxSockets = 1000;
25 | http.globalAgent.maxSockets = 1000;
26 |
27 | const MAX_CONTENT_LENGTH_ACCEPTED = 9999;
28 |
29 | export const app = express();
30 |
31 | app.use(responseTime());
32 | app.disable('x-powered-by');
33 |
34 | // TODO: configure for specific routes
35 | app.use(helmet());
36 |
37 | // TODO: configure for specific routes
38 | app.use(
39 | contentValidator.validateMax({
40 | max: MAX_CONTENT_LENGTH_ACCEPTED,
41 | status: 400,
42 | message: 'Exceeds Max Content.'
43 | })
44 | );
45 |
46 | app.use(expressGracefulExit.middleware(app));
47 |
48 | // TODO: configure for specific routes
49 | app.use(bodyParser.json());
50 |
51 | app.use(
52 | bodyParser.urlencoded({
53 | extended: false
54 | })
55 | );
56 |
57 | // USED WITH BODY PARSER
58 | // TODO: configure for specific routes
59 | app.use(hpp());
60 |
61 | if (env === 'development' || env === 'test') {
62 | app.use(express.static(path.join(__dirname, '../../static')));
63 | }
64 |
65 | app.use('/', router);
66 | app.use(expressBunyanLogger(config.get('logMiddleware')));
67 | app.use(reactRouterMiddleware);
68 |
69 | let server = false;
70 |
71 | export const createOrGetServer = () => {
72 | if (!server) {
73 | server = http.createServer(app);
74 | expressGracefulExit.init(server);
75 | }
76 | return server;
77 | };
78 |
79 | export const setServer = runningServer => {
80 | server = runningServer;
81 | };
82 |
--------------------------------------------------------------------------------
/src/server/services/logger_service.js:
--------------------------------------------------------------------------------
1 | import bunyan from 'bunyan';
2 | import config from '../config';
3 |
4 | export default bunyan.createLogger(config.get('bunyanLogger'));
5 |
--------------------------------------------------------------------------------
/src/server/services/redis_service.js:
--------------------------------------------------------------------------------
1 | import { promisifyAll } from 'bluebird';
2 | import redis from 'redis';
3 | import config from '../config';
4 | // import hiredis from 'hiredis'
5 | let redisClient = function redisClient() {}; // eslint-disable-line import/no-mutable-exports
6 |
7 | if (config.get('cacheEnabled')) {
8 | promisifyAll(redis.RedisClient.prototype);
9 | promisifyAll(redis.Multi.prototype);
10 | redisClient = redis.createClient(config.get('redis'));
11 | }
12 |
13 | export default redisClient;
14 |
--------------------------------------------------------------------------------
/src/server/services/router_service.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { Router } from 'express';
3 | import setRoutes from '../utils/router_util';
4 |
5 | const router = new Router();
6 | const globDir = path.join(__dirname, '../routes/**/*.js');
7 |
8 | export default setRoutes(globDir, router);
9 |
--------------------------------------------------------------------------------
/src/server/utils/error_response_util.js:
--------------------------------------------------------------------------------
1 | import { pick, map } from 'lodash';
2 | import log from '../services/logger_service';
3 |
4 | function mapErrorList(errorItem) {
5 | return pick(errorItem, 'message');
6 | }
7 |
8 | export default (message, code, errorList) => {
9 | if (!message || !code) {
10 | log.fatal('Logger used incorrectly.');
11 | return {
12 | error: {
13 | message: 'Create Error Response used incorrectly.'
14 | }
15 | };
16 | }
17 |
18 | if (errorList) {
19 | return {
20 | error: {
21 | message,
22 | statusCode: code,
23 | errors: map(errorList, mapErrorList)
24 | }
25 | };
26 | }
27 |
28 | return {
29 | error: {
30 | message,
31 | statusCode: code
32 | }
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/src/server/utils/graceful_exit_util.js:
--------------------------------------------------------------------------------
1 | // import config from '../config';
2 | // import nodemailer from 'nodemailer';
3 | import gracefulExit from 'express-graceful-exit';
4 | import log from '../services/logger_service';
5 | import { app, createOrGetServer } from '../services/express_service';
6 | import redisClient from '../services/redis_service';
7 | import config from '../config';
8 |
9 | export default (err, silent) => {
10 | const exitCode = err ? 1 : 0;
11 | if (err) {
12 | log.info(`Process exiting because of error: ${err.message}`);
13 | log.fatal(
14 | err.stack,
15 | `${err.message} - commit: ${process.env.GIT_COMMIT}, Build Number: ${
16 | process.env.BUILD_NUMBER
17 | }`
18 | );
19 | } else {
20 | log.info('Exiting without error.');
21 | }
22 |
23 | if (silent) {
24 | return createOrGetServer().close();
25 | }
26 |
27 | function gracefulExitCallback() {
28 | if (config.get('cacheEnabled')) {
29 | if (redisClient && redisClient.quit) {
30 | redisClient.quit();
31 | return process.exit(exitCode);
32 | }
33 | }
34 | return process.exit(exitCode);
35 | }
36 |
37 | return gracefulExit.gracefulExitHandler(app, createOrGetServer(), {
38 | log: true,
39 | logger(data) {
40 | return log.info(data);
41 | },
42 | suicideTimeout: 3 * 1000, // pm2 waits 4 seconds
43 | exitProcess: false,
44 | force: true,
45 | callback: gracefulExitCallback
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/server/utils/initializer_util.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import log from '../services/logger_service';
3 | import '../../utils/custom_validations_util';
4 | import redis from '../services/redis_service';
5 | import config from '../config';
6 |
7 | const bootPromises = [];
8 |
9 | log.info('booting initializer');
10 |
11 | if (config.get('cacheEnabled')) {
12 | const redisReady = new P(function redisReadyPromise(resolve, reject) {
13 | function handleRedisError(error) {
14 | reject(error);
15 | }
16 |
17 | redis.on('error', handleRedisError);
18 | redis.on('ready', function handleRedisReady() {
19 | log.info('Redis connection successful.');
20 | log.info('Socket.io redis connection successful.');
21 | resolve(true);
22 | });
23 | });
24 | bootPromises.push(redisReady);
25 | }
26 |
27 | export default P.all(bootPromises);
28 |
--------------------------------------------------------------------------------
/src/server/utils/install_sourcemap_util.js:
--------------------------------------------------------------------------------
1 | import sourceMapSupport from 'source-map-support';
2 |
3 | sourceMapSupport.install();
4 |
--------------------------------------------------------------------------------
/src/server/utils/load_env_var_util.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import path from 'path';
3 |
4 | const envFilePath = path.resolve(__dirname, '../../../', '.env');
5 |
6 | dotenv.config({
7 | silent: true,
8 | path: envFilePath
9 | });
10 |
--------------------------------------------------------------------------------
/src/server/utils/react_raf_util.js:
--------------------------------------------------------------------------------
1 | global.requestAnimationFrame = function raf(callback) {
2 | setTimeout(callback, 0);
3 | };
4 |
--------------------------------------------------------------------------------
/src/server/utils/router_util.js:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 |
3 | export default (globDir, router) => {
4 | glob
5 | .sync(globDir)
6 | .map(require)
7 | .forEach(function setRoute(routeFunc) {
8 | routeFunc.default(router);
9 | });
10 | return router;
11 | };
12 |
--------------------------------------------------------------------------------
/src/server/utils/uncaught_exception_util.js:
--------------------------------------------------------------------------------
1 | import gracefulExit from '../utils/graceful_exit_util';
2 | import log from '../services/logger_service';
3 |
4 | process.once('uncaughtException', function handleUncaughtException(err) {
5 | log.info('Process event: uncaughtException');
6 | gracefulExit(err);
7 | });
8 |
9 | process.once('SIGTERM', function handleSIGTERM(err) {
10 | log.info('Process event: SIGTERM');
11 | gracefulExit(err);
12 | });
13 |
14 | process.once('SIGINT', function handleSIGINT(err) {
15 | log.info('Process event: SIGINT');
16 | gracefulExit(err);
17 | });
18 |
19 | process.on('message', function handleMessage(message) {
20 | log.info(`Process event: message=${message}`);
21 | if (message === 'shutdown') {
22 | gracefulExit();
23 | }
24 | });
25 |
26 | process.on('unhandledRejection', function handleRejection(reason, p) {
27 | log.error(`Unhandled Rejection at: Promise , ${p}`, ` reason: ${reason}`);
28 | });
29 |
--------------------------------------------------------------------------------
/src/services/logger_service.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 |
3 | let serverLog = false;
4 | let clientLog = false;
5 |
6 | if (process.env.RUNTIME_ENV !== 'browser') {
7 | serverLog = require('../server/services/logger_service'); // eslint-disable-line global-require
8 | } else {
9 | clientLog = require('../client/services/logger_service'); // eslint-disable-line global-require
10 | }
11 |
12 | const log = canUseDOM ? clientLog : serverLog;
13 |
14 | module.exports = log;
15 |
--------------------------------------------------------------------------------
/src/utils/custom_errors_util.js:
--------------------------------------------------------------------------------
1 | // Validation Errors
2 | function ValidationErrors(errors, options, attributes, constraints) {
3 | Error.captureStackTrace(this, this.constructor);
4 | this.errors = errors;
5 | this.options = options;
6 | this.attributes = attributes;
7 | this.constraints = constraints;
8 | }
9 | ValidationErrors.prototype = new Error();
10 |
11 | // eslint-disable-next-line import/prefer-default-export
12 | export const wrappedValidation = {
13 | wrapErrors: ValidationErrors
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/custom_validations_util.js:
--------------------------------------------------------------------------------
1 | import P from 'bluebird';
2 | import validator from 'validator';
3 | import validate from 'validate.js';
4 |
5 | validate.Promise = P;
6 |
7 | function validateUuid(str) {
8 | return validator.isUUID(str, 4);
9 | }
10 |
11 | // http://validatejs.org/#custom-validator
12 | validate.validators.uuidv4 = function uuidv4(
13 | value,
14 | options,
15 | key /* , attributes */
16 | ) {
17 | if (validateUuid(value) === true) {
18 | return null;
19 | }
20 | return `${key} is not a valid uuid version 4.`;
21 | };
22 |
23 | export default validate;
24 |
25 | // Example: // http://stackoverflow.com/questions/29121733/validate-js-promises-on-custom-validation
26 |
--------------------------------------------------------------------------------
/src/utils/seo_util/about_seo_util.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 | import last from 'lodash/last';
3 | import baseSeoUtil from './base_seo_util';
4 |
5 | // eslint-disable-next-line no-unused-vars
6 | export default function(state, data) {
7 | return baseSeoUtil({
8 | site: 'react-ssr-spa',
9 | siteConf: {
10 | '[og:site_name]': 'react-ssr-spa'
11 | },
12 | facebook: {
13 | fanpageId: ''
14 | },
15 | title: 'About Page of react-ssr-spa',
16 | url: last(get(state, 'config.navHistory')),
17 | description:
18 | 'About | An example of simple server rendering a react progressive web app.',
19 | image:
20 | 'https://upload.wikimedia.org/wikipedia/en/thumb/a/a7/React-icon.svg/1200px-React-icon.svg.png',
21 | extend: [
22 | {
23 | meta: [
24 | /*
25 | {
26 | property: 'article:published_time',
27 | content: articleDate
28 | }
29 | */
30 | ]
31 | }
32 | ]
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/seo_util/base_seo_util.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 |
3 | export default function(data = {}) {
4 | const { siteConf, title, type, publisher = true, extend = [] } = data;
5 | const siteHumanReadable = get(siteConf, '[og:site_name]', '');
6 | const description = data.description || '';
7 | const url = data.url || '';
8 | const siteSlug = data.slug || '';
9 | const image = data.image || data.logo;
10 |
11 | const config = {
12 | title,
13 | meta: [
14 | {
15 | charSet: 'utf-8'
16 | },
17 | {
18 | httpEquiv: 'X-UA-Compatible',
19 | content: 'IE=edge,chrome=1'
20 | },
21 | {
22 | property: 'fb:pages',
23 | content: get(data, 'facebook.fanpageId')
24 | },
25 | {
26 | name: 'viewport',
27 | content:
28 | 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
29 | },
30 | {
31 | name: 'description',
32 | content: description
33 | },
34 | {
35 | property: 'og:type',
36 | content: type || 'article'
37 | },
38 | {
39 | property: 'og:title',
40 | content: title
41 | },
42 | {
43 | property: 'og:url',
44 | content: url
45 | },
46 | {
47 | property: 'og:locale',
48 | content: 'en_US'
49 | },
50 | {
51 | property: 'og:description',
52 | content: description
53 | },
54 | {
55 | property: 'og:image',
56 | content: image
57 | },
58 | {
59 | property: 'og:site_name',
60 | content: siteHumanReadable
61 | },
62 | {
63 | property: 'fb:app_id',
64 | content: get(data, 'appId')
65 | },
66 | {
67 | property: 'fb:admins',
68 | content: get(data, 'appAdminId')
69 | },
70 | {
71 | name: 'twitter:card',
72 | content: data.twitterCard || 'summary_large_image'
73 | },
74 | {
75 | name: 'twitter:title',
76 | content: title
77 | },
78 | {
79 | name: 'twitter:description',
80 | content: description
81 | },
82 | {
83 | name: 'twitter:url',
84 | content: url
85 | },
86 | {
87 | name: 'twitter:site',
88 | content: `@${siteSlug}`
89 | },
90 | {
91 | name: 'twitter:image',
92 | content: data.twitterImage || image
93 | },
94 | {
95 | name: 'twitter:domain',
96 | content: `${siteSlug}.com`
97 | },
98 | {
99 | itemProp: 'name',
100 | content: title
101 | },
102 | {
103 | itemProp: 'description',
104 | content: description
105 | },
106 | {
107 | name: 'theme-color',
108 | content: '#ffffff'
109 | }
110 | ],
111 | link: [
112 | {
113 | rel: 'canonical',
114 | href: url
115 | },
116 | {
117 | rel: 'apple-touch-icon',
118 | sizes: '180x180',
119 | href: '/apple-touch-icon.png?v=2'
120 | },
121 | {
122 | rel: 'icon',
123 | type: 'image/png',
124 | href: '/favicon-32x32.png',
125 | sizes: '32x32'
126 | },
127 | {
128 | rel: 'icon',
129 | type: 'image/png',
130 | href: '/favicon-16x16.png',
131 | sizes: '16x16'
132 | },
133 | {
134 | rel: 'manifest',
135 | href: '/manifest.json'
136 | },
137 | {
138 | rel: 'mask-icon',
139 | href: '/safari-pinned-tab.svg',
140 | color: '#000000'
141 | },
142 | {
143 | rel: 'shortcut icon',
144 | href: '/favicon.ico?v=3'
145 | },
146 | {
147 | rel: 'alternate',
148 | href: url,
149 | hrefLang: 'en'
150 | }
151 | ],
152 | noscript: [],
153 | style: []
154 | };
155 |
156 | if (publisher) {
157 | config.meta.push({
158 | property: 'article:publisher',
159 | content: siteConf['article:publisher'] || ''
160 | });
161 | }
162 |
163 | if (data.articleSection) {
164 | config.meta.push({
165 | property: 'article:section',
166 | content: data.articleSection
167 | });
168 | }
169 |
170 | // Extend is used to add objects to the arrays in the config object.
171 | // For example if you would like to add another meta tag you can:
172 | // const data = baseSeoUtil({
173 | // ...
174 | // extend: [{
175 | // meta: [{
176 | // property: 'META_PROPERTY',
177 | // content: 'META_CONTENT'
178 | // }]
179 | // }]
180 | // });
181 | extend.forEach(extras => {
182 | const key = Object.keys(extras).join('');
183 | const arr = extras[key];
184 |
185 | config[key] = config[key].concat(arr);
186 | });
187 |
188 | return config;
189 | }
190 |
--------------------------------------------------------------------------------
/src/utils/seo_util/index_seo_util.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 | import last from 'lodash/last';
3 | import baseSeoUtil from './base_seo_util';
4 |
5 | // eslint-disable-next-line no-unused-vars
6 | export default function(state, data) {
7 | return baseSeoUtil({
8 | site: 'react-ssr-spa',
9 | siteConf: {
10 | '[og:site_name]': 'react-ssr-spa'
11 | },
12 | facebook: {
13 | fanpageId: ''
14 | },
15 | title: 'Homepage of react-ssr-spa',
16 | url: last(get(state, 'config.navHistory')),
17 | description:
18 | 'An example of simple server rendering a react progressive web app.',
19 | image:
20 | 'https://upload.wikimedia.org/wikipedia/en/thumb/a/a7/React-icon.svg/1200px-React-icon.svg.png',
21 | extend: [
22 | {
23 | meta: [
24 | /*
25 | {
26 | property: 'article:published_time',
27 | content: articleDate
28 | }
29 | */
30 | ]
31 | }
32 | ]
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/seo_util/repo_detail_seo_util.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 | import last from 'lodash/last';
3 | import baseSeoUtil from './base_seo_util';
4 |
5 | export default function(state, data) {
6 | return baseSeoUtil({
7 | site: 'react-ssr-spa',
8 | siteConf: {
9 | '[og:site_name]': 'react-ssr-spa'
10 | },
11 | facebook: {
12 | fanpageId: ''
13 | },
14 | title: get(data, '[0].name'),
15 | url: last(get(state, 'config.navHistory')),
16 | description: get(data, '[0].description'),
17 | image: get(data, '[0].owner.avatar_url'),
18 | articleSection: get(data, '[0].language'),
19 | appId: '',
20 | extend: [
21 | {
22 | meta: [
23 | {
24 | property: 'article:published_time',
25 | content: get(data, '[0].created_at')
26 | }
27 | ]
28 | }
29 | ]
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/seo_util/search_seo_util.js:
--------------------------------------------------------------------------------
1 | import baseSeoUtil from './base_seo_util';
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | export default function(state, action) {
5 | const data = baseSeoUtil({
6 | site: '',
7 | siteConf: {},
8 | title: '',
9 | url: '',
10 | description: '',
11 | image: '',
12 | image_width: '',
13 | image_height: '',
14 | articleSection: '',
15 | extend: [
16 | {
17 | meta: [
18 | /*
19 | {
20 | property: 'article:published_time',
21 | content: articleDate
22 | }
23 | */
24 | ]
25 | }
26 | ]
27 | });
28 |
29 | return data;
30 | }
31 |
--------------------------------------------------------------------------------
/src/views/components/config/config.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import clone from 'lodash/clone';
4 | import serialize from 'serialize-javascript';
5 |
6 | class Config extends Component {
7 | shouldComponentUpdate() {
8 | return false;
9 | }
10 |
11 | render() {
12 | const clonedState = clone(this.props.state);
13 | const { initialPageLoad } = this.props.state.config;
14 |
15 | if (!initialPageLoad) {
16 | return {};
17 | }
18 |
19 | clonedState.config.initialQueryParams = serialize(
20 | clonedState.config.initialQueryParams,
21 | { isJSON: true }
22 | );
23 | const state = initialPageLoad ? serialize(clonedState) : '';
24 |
25 | /* eslint-disable react/no-danger */
26 | return (
27 | ;
56 | return [
57 | ,
58 |
59 | ];
60 | }
61 |
62 | if (env === 'test') {
63 | return [
64 | ,
69 |
74 | ];
75 | }
76 |
77 | return [
78 | ,
85 |
92 | ];
93 | }
94 |
95 | scriptChunks(env) {
96 | if (env === 'development') {
97 | return null;
98 | }
99 |
100 | if (env === 'test') {
101 | return this.props.chunks.map(chunk => (
102 |
107 | ));
108 | }
109 |
110 | return this.props.chunks.map(chunk => (
111 |
116 | ));
117 | }
118 |
119 | render() {
120 | return (
121 |
122 |
123 |
124 | {
126 | log.error(componentStack, error);
127 | }}
128 | fallbackcomponent={Error
}
129 | >
130 |
138 |
145 | {renderRoutes(this.props.route.routes, this.props.location)}
146 |
147 |
148 |
149 | {this.livereload()}
150 |
151 | {this.scriptChunks(this.props.env)}
152 | {this.scriptbundle(this.props.env)}
153 |
154 | );
155 | }
156 | }
157 |
158 | function mapStateToProps(state) {
159 | return {
160 | env: state.config.env,
161 | manifestJSON: state.config.manifestJSON,
162 | chunks: state.config.chunks,
163 | staticVendorUrl: state.config.staticVendorUrl,
164 | staticBundleUrl: state.config.staticBundleUrl,
165 | state
166 | };
167 | }
168 |
169 | Layout.propTypes = {
170 | route: PropTypes.shape({
171 | routes: PropTypes.arrayOf(PropTypes.shape({}))
172 | }),
173 | env: PropTypes.string.isRequired,
174 | chunks: PropTypes.arrayOf(PropTypes.string),
175 | manifestJSON: PropTypes.shape({
176 | 'vendor.js': PropTypes.string,
177 | 'app.js': PropTypes.string
178 | }).isRequired,
179 | staticBundleUrl: PropTypes.string.isRequired,
180 | staticVendorUrl: PropTypes.string.isRequired,
181 | location: PropTypes.shape({
182 | pathname: PropTypes.string,
183 | key: PropTypes.string
184 | }).isRequired,
185 | state: PropTypes.shape({}).isRequired
186 | };
187 |
188 | Layout.defaultProps = {
189 | route: {
190 | routes: []
191 | },
192 | chunks: [],
193 | env: 'development'
194 | };
195 |
196 | export default connect(mapStateToProps)(Layout);
197 |
--------------------------------------------------------------------------------
/src/views/containers/pages/about_page/_about.scss:
--------------------------------------------------------------------------------
1 | .about-page {
2 | @include flex-grid-column($columns: 12);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/containers/pages/about_page/about_page.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 | import PropTypes from 'prop-types';
3 | import React, { Component } from 'react';
4 | import { connect } from 'react-redux';
5 | import withRouter from 'react-router/withRouter';
6 | import Link from 'react-router-dom/Link';
7 |
8 | import loadData from './about_page_data_fetch';
9 |
10 | import Footer from './../../../components/footer/footer';
11 |
12 | class AboutPage extends Component {
13 | componentWillMount() {
14 | if (!get(this.props, 'state.config.initialPageLoad')) {
15 | loadData(this.props.match, this.props.dispatch, this.props.state);
16 | } else {
17 | // TODO: warm cache for PWA, don't trigger render
18 | }
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
What's this about?
25 |
26 | This project aims to do one thing well: make server side rendering
27 | simple in a react application using only mature community maintained
28 | libraries.
29 | demo: react-ssr-spa
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | AboutPage.propTypes = {
38 | match: PropTypes.shape({}).isRequired,
39 | dispatch: PropTypes.func.isRequired,
40 | state: PropTypes.shape({}).isRequired
41 | };
42 |
43 | AboutPage.defaultProps = {};
44 |
45 | function mapStateToProps(state = {}) {
46 | return {
47 | state
48 | };
49 | }
50 |
51 | export default withRouter(connect(mapStateToProps)(AboutPage));
52 |
--------------------------------------------------------------------------------
/src/views/containers/pages/about_page/about_page_data_fetch.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import asyncAboutPageAction from '../../../../redux/actions/about/async_about_page_actions';
3 | import log from '../../../../services/logger_service';
4 |
5 | export default function fetchAboutData(match, dispatch, state) {
6 | if (canUseDOM) {
7 | if (state.config.initialPageLoad === true) {
8 | return false;
9 | }
10 | }
11 | return asyncAboutPageAction(match.params, dispatch, state).catch(
12 | function handleError(err) {
13 | log.error(err);
14 | }
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/containers/pages/about_page/about_route.js:
--------------------------------------------------------------------------------
1 | import AboutPage from './about_page';
2 | import aboutPageLoadData from './about_page_data_fetch';
3 |
4 | export default {
5 | path: '/about',
6 | component: AboutPage,
7 | exact: true,
8 | strict: true,
9 | loadData: aboutPageLoadData
10 | };
11 |
--------------------------------------------------------------------------------
/src/views/containers/pages/error_page/_error.scss:
--------------------------------------------------------------------------------
1 | .about-page {
2 | @include flex-grid-column($columns: 12);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/containers/pages/error_page/error_page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Footer from './../../../components/footer/footer';
5 |
6 | function ErrorPage({ env, componentInfo, err }) {
7 | const isDevelopment = env === 'development';
8 | if (isDevelopment) {
9 | console.error(err); // eslint-disable-line no-console
10 | console.error(err.stack); // eslint-disable-line no-console
11 | }
12 | return (
13 |
14 |
Error Occurred.
15 | {isDevelopment ? (
16 |
17 |
Error Message: {err.message}
18 | Error Stack: {JSON.stringify(err.stack, null, 2)}
19 | Component Info: {JSON.stringify(componentInfo, null, 2)}
20 |
21 | ) : (
22 |
We\'re sorry please try again later.
23 | )}
24 |
25 |
26 | );
27 | }
28 |
29 | ErrorPage.propTypes = {
30 | err: PropTypes.shape({}),
31 | componentInfo: PropTypes.shape({}),
32 | env: PropTypes.string.isRequired
33 | };
34 |
35 | ErrorPage.defaultProps = {
36 | err: {},
37 | componentInfo: {}
38 | };
39 |
40 | export default ErrorPage;
41 |
--------------------------------------------------------------------------------
/src/views/containers/pages/index_page/_index.scss:
--------------------------------------------------------------------------------
1 | .index-page {
2 | @include flex-grid-column($columns: 12);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/containers/pages/index_page/index_page.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 | import PropTypes from 'prop-types';
3 | import React, { Component } from 'react';
4 | import { connect } from 'react-redux';
5 | import withRouter from 'react-router/withRouter';
6 | import Link from 'react-router-dom/Link';
7 |
8 | import loadData from './index_page_data_fetch';
9 |
10 | import Footer from './../../../components/footer/footer';
11 |
12 | class Homepage extends Component {
13 | componentWillMount() {
14 | if (!get(this.props, 'state.config.initialPageLoad')) {
15 | loadData(this.props.match, this.props.dispatch, this.props.state);
16 | } else {
17 | // TODO: warm cache for PWA, don't trigger render
18 | }
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
Welcome to react-ssr-spa working demo.
25 | demo: react-ssr-spa
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | Homepage.propTypes = {
33 | match: PropTypes.shape({}).isRequired,
34 | dispatch: PropTypes.func.isRequired,
35 | state: PropTypes.shape({}).isRequired
36 | };
37 |
38 | Homepage.defaultProps = {};
39 |
40 | function mapStateToProps(state = {}) {
41 | return {
42 | state
43 | };
44 | }
45 |
46 | export default withRouter(connect(mapStateToProps)(Homepage));
47 |
--------------------------------------------------------------------------------
/src/views/containers/pages/index_page/index_page_data_fetch.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import asyncIndexPageAction from '../../../../redux/actions/index/async_index_page_actions';
3 | import log from '../../../../services/logger_service';
4 |
5 | export default function fetchIndexData(match, dispatch, state) {
6 | if (canUseDOM) {
7 | if (state.config.initialPageLoad === true) {
8 | return false;
9 | }
10 | }
11 | return asyncIndexPageAction(match.params, dispatch, state).catch(
12 | function handleError(err) {
13 | log.error(err);
14 | }
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/containers/pages/index_page/index_page_route.js:
--------------------------------------------------------------------------------
1 | import IndexPage from './index_page';
2 | import indexPageLoadData from './index_page_data_fetch';
3 |
4 | export default {
5 | path: '/',
6 | component: IndexPage,
7 | exact: true,
8 | strict: true,
9 | loadData: indexPageLoadData
10 | };
11 |
--------------------------------------------------------------------------------
/src/views/containers/pages/not_found_page/_not_found.scss:
--------------------------------------------------------------------------------
1 | .not-found {
2 | @include flex-grid-column($columns: 12);
3 |
4 | background: $light-gray;
5 | margin: 2rem 0 0;
6 | }
7 |
--------------------------------------------------------------------------------
/src/views/containers/pages/not_found_page/not_found_page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Footer from './../../../components/footer/footer';
4 |
5 | export default function() {
6 | return (
7 |
8 |
Doh! Page not found.
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/views/containers/pages/not_found_page/not_found_route.js:
--------------------------------------------------------------------------------
1 | import NotFound from './not_found_page';
2 | import notFoundStateManager from './not_found_state_manager';
3 |
4 | export default {
5 | component: NotFound,
6 | loadData: notFoundStateManager
7 | };
8 |
--------------------------------------------------------------------------------
/src/views/containers/pages/not_found_page/not_found_state_manager.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import notFoundActionCreator from '../../../../redux/action_creators/not_found_status_action_creator';
3 |
4 | export default function handleNotFound(match, dispatch) {
5 | if (canUseDOM) {
6 | return false;
7 | }
8 | dispatch(notFoundActionCreator(404, 'NOT_FOUND_STATUS'));
9 | return false;
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/containers/pages/repo_detail_page/_repo_detail.scss:
--------------------------------------------------------------------------------
1 | .repo-detail-page {
2 |
3 | &__main {
4 | @include flex-grid-column($columns: 12);
5 | }
6 |
7 | &__sidebar {
8 | display: none;
9 | }
10 | }
11 |
12 | @include breakpoint(large up) {
13 |
14 | .repo-detail-page {
15 | @include flex-grid-row;
16 |
17 | flex-flow: row;
18 | padding: 0;
19 |
20 | &__main {
21 | @include flex-grid-column($columns: 8);
22 | }
23 |
24 | &__sidebar {
25 | @include flex-grid-column($columns: 4);
26 |
27 | display: block;
28 | margin-top: 1rem;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/views/containers/pages/repo_detail_page/repo_detail_page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import withRouter from 'react-router/withRouter';
5 | import get from 'lodash/get';
6 |
7 | import loadData from './repo_detail_page_data_fetch';
8 |
9 | import Footer from './../../../components/footer/footer';
10 |
11 | class RepoDetail extends Component {
12 | componentWillMount() {
13 | if (!get(this.props, 'state.config.initialPageLoad')) {
14 | loadData(this.props.match, this.props.dispatch, this.props.state);
15 | } else {
16 | // TODO: warm cache for PWA, don't trigger render
17 | }
18 | }
19 |
20 | render() {
21 | if (this.props.error) {
22 | const errorMessage = "We're sorry, please try again later.";
23 | return (
24 |
25 |
26 | {errorMessage}
27 | {this.props.errorMessage}
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | if (this.props.isLoading) {
35 | return (
36 |
37 |
38 | Loading...
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | const { repo } = this.props;
46 | return (
47 |
48 |
49 | {repo.name}
50 | {repo.description}
51 |
52 |
53 |
54 | {`Owner: ${repo.owner.login}`}
55 | {`Stars: ${repo.stargazers_count}`}
56 | {`Watchers: ${repo.watchers_count}`}
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | RepoDetail.propTypes = {
65 | match: PropTypes.shape({}).isRequired,
66 | dispatch: PropTypes.func.isRequired,
67 | state: PropTypes.shape({}).isRequired,
68 | repo: PropTypes.shape({}), // eslint-disable-line react/require-default-props
69 | isLoading: PropTypes.bool, // eslint-disable-line react/require-default-props
70 | error: PropTypes.bool, // eslint-disable-line react/require-default-props
71 | errorMessage: PropTypes.string // eslint-disable-line react/require-default-props
72 | };
73 |
74 | // More info here: https://github.com/reactjs/react-redux/issues/210
75 | RepoDetail.defaultProps = {
76 | isLoading: true,
77 | error: false
78 | };
79 |
80 | function mapStateToProps(state = {}) {
81 | return {
82 | error: get(state, 'repoDetail.error'),
83 | errorMessage: get(state, 'repoDetail.errorMessage'),
84 | isLoading: get(state, 'repoDetail.isLoading'),
85 | repo: get(state, 'repoDetail.repo'),
86 | state
87 | };
88 | }
89 |
90 | export default withRouter(connect(mapStateToProps)(RepoDetail));
91 |
--------------------------------------------------------------------------------
/src/views/containers/pages/repo_detail_page/repo_detail_page_data_fetch.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import asyncRepoDetailPageAction from '../../../../redux/actions/repo_detail/async_repo_detail_page_actions';
3 | import log from '../../../../services/logger_service';
4 |
5 | export default function fetchData(match, dispatch, state) {
6 | if (canUseDOM) {
7 | if (state.config.initialPageLoad === true) {
8 | return false;
9 | }
10 | }
11 | return asyncRepoDetailPageAction(match.params, dispatch, state).catch(
12 | function handleActionError(err) {
13 | log.error(err);
14 | }
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/containers/pages/repo_detail_page/repo_detail_route.js:
--------------------------------------------------------------------------------
1 | import RepoDetailPage from './repo_detail_page';
2 | import repoDetailPageStateManager from './repo_detail_page_data_fetch';
3 |
4 | export default {
5 | path: '/repo/:user/:title',
6 | component: RepoDetailPage,
7 | loadData: repoDetailPageStateManager
8 | };
9 |
--------------------------------------------------------------------------------
/src/views/containers/pages/search_results_page/_search_results.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | @include flex-grid-column($columns: 12);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/containers/pages/search_results_page/search_results_data_fetch.js:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'exenv';
2 | import fetchSearchPageAction from '../../../../redux/actions/search/async_search_page_actions';
3 | import log from '../../../../services/logger_service';
4 |
5 | export default function fetchSearchData(nextState, dispatch, state) {
6 | if (canUseDOM) {
7 | if (state.config.initialPageLoad === true) {
8 | return false;
9 | }
10 | }
11 | return fetchSearchPageAction(nextState.params.query, dispatch, state).catch(
12 | function handleServerSideRenderError(err) {
13 | log.error(err);
14 | }
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/containers/pages/search_results_page/search_results_page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Link from 'react-router-dom/Link';
5 | import withRouter from 'react-router/withRouter';
6 | import get from 'lodash/get';
7 |
8 | import loadData from './search_results_data_fetch';
9 | import Footer from './../../../components/footer/footer';
10 |
11 | class Search extends Component {
12 | static loadData(nextState, dispatch, state) {
13 | return loadData(nextState, dispatch, state);
14 | }
15 |
16 | componentWillMount() {
17 | if (
18 | !get(this.props, 'state.config.initialPageLoad') ||
19 | !this.props.isLoading
20 | ) {
21 | loadData(this.props.match, this.props.dispatch, this.props.state);
22 | }
23 | /*
24 | // TODO: warm cache for PWA, don't trigger render
25 | if(get(this.props, 'state.config.initialPageLoad')) {
26 | const CACHE_WARM = true;
27 | loadData(
28 | this.props.match,
29 | this.props.dispatch,
30 | this.props.state,
31 | CACHE_WARM
32 | );
33 | }
34 | */
35 | }
36 |
37 | /*
38 | componentDidCatch(error, info) {
39 | // log.error(error, info);
40 | // this.setState({error});
41 | }
42 | */
43 |
44 | render() {
45 | if (this.props.error === true) {
46 | return (
47 |
48 | We're sorry! There was an error. Message:
49 | {this.props.errorMessage}
50 |
51 |
52 | );
53 | }
54 |
55 | if (this.props.isLoading === true || this.props.isLoading === undefined) {
56 | return (
57 |
58 | Be Patient, we are loading in the search results.
59 |
60 |
61 | );
62 | }
63 |
64 | return (
65 |
66 |
67 | {get(this.props.response, 'items', []).map(function mapItems(item) {
68 | return (
69 |
70 |
71 | {item.name}
72 | {item.owner.login}
73 |
74 |
75 |
76 | );
77 | })}
78 |
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | Search.propTypes = {
86 | match: PropTypes.shape({}).isRequired,
87 | state: PropTypes.shape({}).isRequired,
88 | dispatch: PropTypes.func.isRequired,
89 | isLoading: PropTypes.bool, // eslint-disable-line react/require-default-props
90 | error: PropTypes.bool, // eslint-disable-line react/require-default-props
91 | errorMessage: PropTypes.string, // eslint-disable-line react/require-default-props
92 | response: PropTypes.shape({}) // eslint-disable-line react/require-default-props
93 | };
94 |
95 | Search.defaultProps = {
96 | error: false,
97 | response: {
98 | items: []
99 | }
100 | };
101 |
102 | const mapStateToProps = (state = {}) => ({
103 | isLoading: state.search.isLoading,
104 | response: state.search.response,
105 | state
106 | });
107 |
108 | export default withRouter(connect(mapStateToProps)(Search));
109 |
--------------------------------------------------------------------------------
/src/views/containers/pages/search_results_page/search_results_route.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loadable from 'react-loadable';
3 |
4 | let Component = false;
5 |
6 | if (process.env.RUNTIME_ENV !== 'browser') {
7 | // eslint-disable-next-line global-require
8 | Component = require('./search_results_page').default;
9 | }
10 |
11 | const routeConfig = {
12 | path: '/search/:query',
13 | component() {
14 | if (Component) {
15 | return ;
16 | }
17 | return ;
18 | },
19 | loadData: Component ? Component.loadData : () => {},
20 | preloadChunk() {
21 | return import(/* webpackChunkName: "search" */ './search_results_page').then(
22 | resp => {
23 | Component = resp.default;
24 | routeConfig.loadData = Component.loadData;
25 | return Component;
26 | }
27 | );
28 | },
29 | chunk: 'search'
30 | };
31 |
32 | const LazyComponent = Loadable({
33 | loader() {
34 | return routeConfig.preloadChunk();
35 | },
36 | loading() {
37 | return (
38 |
39 | Be Patient, we are loading in the search results.
40 |
41 | );
42 | }
43 | });
44 |
45 | export default routeConfig;
46 |
--------------------------------------------------------------------------------
/src/views/containers/root_container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import renderRoutes from 'react-router-config/renderRoutes';
4 | import ConnectedRouter from 'react-router-redux/ConnectedRouter';
5 | import { Provider } from 'react-redux';
6 | import routes from '../../react_router/react_router';
7 | import Head from '../components/head/head';
8 |
9 | function Root({ store, history }) {
10 | return (
11 |
12 |
13 |
14 |
15 | {renderRoutes(routes)}
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | Root.propTypes = {
23 | store: PropTypes.shape({}).isRequired,
24 | history: PropTypes.shape({}).isRequired
25 | };
26 |
27 | export default Root;
28 |
--------------------------------------------------------------------------------
/test/acceptance/spec/title_test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | describe('#homepage', function() {
4 | it('should have the right server rendered title', function() {
5 | browser.url('http://127.0.0.1:8001');
6 | const title = browser.getTitle();
7 | expect(title).to.equal('Homepage of react-ssr-spa');
8 | });
9 |
10 | it('should execute js correctly', function() {
11 | browser.waitForExist('.fonts-loaded.hydrated', 10000);
12 | });
13 |
14 | browser.click('[href="/repo/michaelBenin/react-ssr-spa"]');
15 |
16 | browser.waitForExist('.repo-detail-page__main', 2000);
17 |
18 | browser.waitUntil(
19 | function() {
20 | return browser.getTitle() === 'react-ssr-spa';
21 | },
22 | 2000,
23 | 'expected title update after 2s'
24 | );
25 | });
26 |
27 | describe('#repoDetail', function() {
28 | it('should have the right server rendered title', function() {
29 | browser.url('http://127.0.0.1:8001/repo/michaelBenin/react-ssr-spa');
30 | const title = browser.getTitle();
31 | expect(title).to.equal('react-ssr-spa');
32 | });
33 |
34 | it('should execute js correctly', function() {
35 | browser.waitForExist('.fonts-loaded.hydrated', 10000);
36 | });
37 | });
38 |
39 | describe('#about', function() {
40 | it('should have the right server rendered title', function() {
41 | browser.url('http://127.0.0.1:8001/about');
42 | const title = browser.getTitle();
43 | expect(title).to.equal('About Page of react-ssr-spa');
44 | });
45 |
46 | it('should execute js correctly', function() {
47 | browser.waitForExist('.fonts-loaded.hydrated', 10000);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/client/integration/example/index.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | describe('Example test', function() {
4 | it('Should return hello', function() {
5 | expect('hello').to.equal('hello');
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/test/client/unit/utils/font_loader_util.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import FontLoaderUtil from '../../../../src/client/utils/font_loader_util';
3 |
4 | describe('Font loader util unit tests:', function() {
5 | it('Should have method loadFonts', function() {
6 | expect(typeof FontLoaderUtil.loadFonts).to.equal('function');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/test/client/unit/utils/initializer_util.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import initializerUtil from '../../../../src/client/utils/initializer_util';
3 |
4 | describe('initializer util unit tests:', function() {
5 | it('Should be a function', function() {
6 | expect(typeof initializerUtil).to.equal('function');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/test/client/unit/utils/third_party_js_util.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import {
3 | loadAllThirdPartyJs,
4 | ThirdPartyJs
5 | } from '../../../../src/client/utils/third_party_js_util';
6 |
7 | describe('third party js util unit tests:', function() {
8 | it('loadAllThirdPartyJS Should be a function', function() {
9 | expect(typeof loadAllThirdPartyJs).to.equal('function');
10 | });
11 |
12 | it('SetThirdPartyGlobals should be a function', function() {
13 | expect(typeof ThirdPartyJs.setThirdPartyGlobals).to.equal('function');
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/test/server/integration/controllers/get_ping_controller_test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import server from '../../utils/app_util';
4 |
5 | describe('Ping controller/route test', function() {
6 | this.timeout(1000);
7 | it('Should give the correct response or ping', function() {
8 | return server
9 | .get('/api/v1/ping')
10 | .expect(200)
11 | .then(res => {
12 | expect(res.text).to.equal('pong');
13 | expect(res.header['x-powered-by']).to.equal(undefined);
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/server/integration/controllers/post_log_controller_test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import server from '../../utils/app_util';
3 |
4 | describe('post log controller/route test', function() {
5 | this.timeout(1000);
6 | it('Should give the correct response for empty obj', function() {
7 | return server
8 | .post('/api/v1/log')
9 | .set('Accept', 'application/json')
10 | .send({})
11 | .expect(405)
12 | .then(res => {
13 | expect(res.header['x-powered-by']).to.equal(undefined);
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/server/integration/middleware/react_router_middleware.js:
--------------------------------------------------------------------------------
1 | import server from '../../utils/app_util';
2 |
3 | describe('React router middleware test', function() {
4 | this.timeout(1000);
5 | it('Should have a 404 status for the not found route.', function() {
6 | return server.get('/404').expect(404);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/test/server/unit/config/config_test.js:
--------------------------------------------------------------------------------
1 | // import { expect } from 'chai';
2 | // import config from '../../../../../src/server/config';
3 |
--------------------------------------------------------------------------------
/test/server/unit/services/logger_service_test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import log from '../../../../src/server/services/logger_service';
3 |
4 | describe('Testing logger service.', () => {
5 | it('Should return a function', () => {
6 | expect(typeof log.info).to.equal('function');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/test/server/utils/app_util.js:
--------------------------------------------------------------------------------
1 | import supertest from 'supertest';
2 | import { app } from '../../../src/server/services/express_service';
3 |
4 | function createVhostTester(supertestApp, vhost) {
5 | const real = supertest(supertestApp);
6 | const proxy = {};
7 |
8 | Object.keys(real).forEach(methodName => {
9 | proxy[methodName] = function() {
10 | return (
11 | real
12 | // eslint-disable-next-line prefer-rest-params, no-unexpected-multiline
13 | [methodName](...arguments)
14 | .set('host', vhost)
15 | );
16 | };
17 | });
18 |
19 | return proxy;
20 | }
21 |
22 | // example of setting virtual host in supertest, used if
23 | // needing to support multiple domains
24 | // export const sitename = createVhostTester(app, 'example-host.com');
25 |
26 | export default createVhostTester(app, 'example-host.com');
27 |
--------------------------------------------------------------------------------
/test/server/utils/babel_register.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line global-require
2 | require('babel-register')({
3 | presets: [
4 | [
5 | 'env',
6 | {
7 | targets: {
8 | node: 'current'
9 | }
10 | }
11 | ],
12 | 'react'
13 | ],
14 | plugins: [
15 | 'dynamic-import-node',
16 | 'syntax-dynamic-import',
17 | 'transform-react-jsx-source'
18 | ]
19 | });
20 |
--------------------------------------------------------------------------------
/test/server/utils/enzyme_initializer.js:
--------------------------------------------------------------------------------
1 | import { JSDOM } from 'jsdom';
2 | import '../../../src/server/utils/react_raf_util';
3 |
4 | const exposedProperties = ['window', 'navigator', 'document'];
5 |
6 | const jsdom = new JSDOM('');
7 | global.document = jsdom.window.document;
8 | global.window = jsdom.window;
9 |
10 | Object.keys(document).forEach(property => {
11 | if (typeof global[property] === 'undefined') {
12 | exposedProperties.push(property);
13 | global[property] = document.defaultView[property];
14 | }
15 | });
16 |
17 | global.navigator = {
18 | userAgent: 'node.js'
19 | };
20 |
21 | global.documentRef = document;
22 |
--------------------------------------------------------------------------------
/test/server/utils/initializer_util.js:
--------------------------------------------------------------------------------
1 | import '../../../src/server/utils/install_sourcemap_util';
2 | import '../../../src/server/utils/load_env_var_util';
3 |
--------------------------------------------------------------------------------
/test/server/utils/test_initializer_util.js:
--------------------------------------------------------------------------------
1 | import '../../../src/server/utils/install_sourcemap_util';
2 | import '../../../src/server/utils/load_env_var_util';
3 | import '../../../src/utils/custom_validations_util';
4 |
--------------------------------------------------------------------------------
/test/server/utils/test_teardown_util.js:
--------------------------------------------------------------------------------
1 | import typeDetect from 'type-detect';
2 | import { expect } from 'chai';
3 | import redisClient from '../../../src/server/services/redis_service';
4 | import { createOrGetServer } from '../../../src/server/services/express_service';
5 |
6 | describe('#closeConnections tests', function() {
7 | it('Should close connections without error.', function(done) {
8 | this.timeout(10000);
9 | expect(typeDetect(createOrGetServer)).to.equal('function');
10 |
11 | if (redisClient.quit) {
12 | redisClient.quit();
13 | }
14 |
15 | createOrGetServer().close();
16 | done();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/test/shared/unit/views/components/config/config.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/config/config.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/footer/footer.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/footer/footer.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/head/head.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/head/head.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/head/noscript.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/head/noscript.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/head/script.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/head/script.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/head/style.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/head/style.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/header/header.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/header/header.js
--------------------------------------------------------------------------------
/test/shared/unit/views/components/nav/nav.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaelBenin/react-ssr-spa/9c36bc68789c523efe04f4a34c439bb7fd48e93c/test/shared/unit/views/components/nav/nav.js
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/about_page/about_page_test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { expect } from 'chai';
4 | import { StaticRouter } from 'react-router';
5 |
6 | import AboutPage from '../../../../../../src/views/containers/pages/about_page/about_page';
7 | import Enzyme from '../../../../utils/enzyme_adapter_util';
8 |
9 | const { shallow } = Enzyme;
10 |
11 | describe('A suite for about page', function() {
12 | it('renders without error', function() {
13 | expect(
14 | shallow(
15 |
16 |
17 |
18 | ).dive().length
19 | ).to.equal(1);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/index_page/index_page_test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { expect } from 'chai';
4 | import { StaticRouter } from 'react-router';
5 |
6 | import IndexPage from '../../../../../../src/views/containers/pages/index_page/index_page';
7 | import Enzyme from '../../../../utils/enzyme_adapter_util';
8 |
9 | const { shallow } = Enzyme;
10 |
11 | describe('A suite for index page', function() {
12 | it('renders without error', function() {
13 | expect(
14 | shallow(
15 |
16 |
17 |
18 | ).dive().length
19 | ).to.equal(1);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/layouts/layout_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | import React from 'react';
3 |
4 | import { expect } from 'chai';
5 | import { shallow, mount } from 'enzyme';
6 | import { StaticRouter } from 'react-router';
7 |
8 | import Layout from '../../../../../src/views/containers/layouts/layout';
9 |
10 | describe('A suite for layout page', function() {
11 | it('contains the correct class', function correctClass() {
12 | expect(
13 | shallow( ).find(
14 | '.layout-page'
15 | ).length
16 | ).to.equal(1);
17 | });
18 |
19 | it('contains spec with an expectation', function() {
20 | expect(
21 | mount( ).find(
22 | '.layout-page'
23 | ).length
24 | ).to.equal(1);
25 | });
26 | });
27 | */
28 |
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/not_found_page/not_found_page_test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { expect } from 'chai';
4 | import { StaticRouter } from 'react-router';
5 |
6 | import NotFoundPage from '../../../../../../src/views/containers/pages/not_found_page/not_found_page';
7 |
8 | import Enzyme from '../../../../utils/enzyme_adapter_util';
9 |
10 | const { mount } = Enzyme;
11 |
12 | describe('A suite for not-found page', function() {
13 | /*
14 | it('contains the correct class', function correctClass() {
15 | expect(
16 | shallow( ).find(
17 | '.not-found'
18 | )
19 | ).to.equal(1);
20 | });
21 | */
22 |
23 | it('contains spec with an expectation', function() {
24 | expect(
25 | mount(
26 |
27 |
28 |
29 | ).find('.not-found').length
30 | ).to.equal(1);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/repo_detail_page/repo_detail_page_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | import React from 'react';
3 |
4 | import { expect } from 'chai';
5 | import { shallow, mount } from 'enzyme';
6 | import { StaticRouter } from 'react-router';
7 |
8 | import RepoDetailPage from '../../../../../src/views/containers/pages/repo_detail_page/repo_detail_page';
9 |
10 | describe('A suite for repo-detail page', function() {
11 | it('contains the correct class', function correctClass() {
12 | expect(
13 | shallow( ).find(
14 | '.repo-detail-page'
15 | ).length
16 | ).to.equal(1);
17 | });
18 |
19 | it('contains spec with an expectation', function() {
20 | expect(
21 | mount( ).find(
22 | '.repo-detail-page'
23 | ).length
24 | ).to.equal(1);
25 | });
26 | });
27 | */
28 |
--------------------------------------------------------------------------------
/test/shared/unit/views/containers/search_results_page/search_page_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | import React from 'react';
3 |
4 | import { expect } from 'chai';
5 | import { shallow, mount } from 'enzyme';
6 | import { StaticRouter } from 'react-router';
7 |
8 | import SearchResultsPage from '../../../../../src/views/containers/pages/search_results_page/search_results_page';
9 |
10 | describe('A suite for search page', function() {
11 | it('contains the correct class', function correctClass() {
12 | expect(
13 | shallow(
14 |
15 | ).find('.search')
16 | ).to.equal(1);
17 | });
18 |
19 | it('contains spec with an expectation', function() {
20 | expect(
21 | mount(
22 |
23 | ).find('.search').length
24 | ).to.equal(1);
25 | });
26 | });
27 | */
28 |
--------------------------------------------------------------------------------
/test/shared/utils/enzyme_adapter_util.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 | export default Enzyme;
6 |
--------------------------------------------------------------------------------