├── .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 | [![Build Status](https://travis-ci.org/michaelBenin/react-ssr-spa.svg?branch=master)](https://travis-ci.org/michaelBenin/react-ssr-spa) [![dependencies Status](https://david-dm.org/michaelBenin/react-ssr-spa/status.svg)](https://david-dm.org/michaelBenin/react-ssr-spa) [![devDependencies Status](https://david-dm.org/michaelBenin/react-ssr-spa/dev-status.svg)](https://david-dm.org/michaelBenin/react-ssr-spa?type=dev) [![NSP Status](https://nodesecurity.io/orgs/react-ssr-spa/projects/517c11e2-34a4-425f-bf5e-3b074e49ab7f/badge)](https://nodesecurity.io/orgs/react-ssr-spa/projects/517c11e2-34a4-425f-bf5e-3b074e49ab7f) 2 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 3 | 4 | Server Coverage: [![Coverage Status](https://coveralls.io/repos/github/michaelBenin/react-ssr-spa/badge.svg?branch=master)](https://coveralls.io/github/michaelBenin/react-ssr-spa?branch=master) 5 | 6 | Client Coverage: [![codecov](https://codecov.io/gh/michaelBenin/react-ssr-spa/branch/master/graph/badge.svg)](https://codecov.io/gh/michaelBenin/react-ssr-spa) 7 | 8 | Acceptance Tests: [![Sauce Test Status](https://saucelabs.com/buildstatus/YOUR_SAUCE_USERNAME)](https://saucelabs.com/u/YOUR_SAUCE_USERNAME) 9 | 10 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/YOUR_SAUCE_USERNAME.svg)](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 |