diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4262033 --- /dev/null +++ b/.babelrc @@ -0,0 +1,108 @@ +{ + "presets": [ + "node6", + "es2015", + "react", + "stage-0" + ], + "env": { + "development": { + "plugins": [ + "add-module-exports", + [ + "typecheck", + { + "disable": { + "production": true + } + } + ], + [ + "react-transform", + { + "transforms": [ + { + "transform": "react-transform-hmr", + "imports": [ + "react" + ], + "locals": [ + "module" + ] + }, + { + "transform": "react-transform-catch-errors", + "imports": [ + "react", + "redbox-react" + ] + } + ] + } + ], + [ + "babel-root-import", + { + "rootPathPrefix": "@", + "rootPathSuffix": "server" + } + ], + "transform-decorators-legacy" + ] + }, + "test": { + "plugins": [ + "add-module-exports", + [ + "typecheck", + { + "disable": { + "production": true + } + } + ], + [ + "babel-root-import", + { + "rootPathPrefix": "@", + "rootPathSuffix": "server" + } + ], + "transform-decorators-legacy" + ] + } + }, + "plugins": [ + [ + "add-module-exports", + "typecheck", + { + "disable": { + "production": true + } + } + ], + [ + "react-transform", + { + "transforms": [ + { + "transform": "react-transform-catch-errors", + "imports": [ + "react", + "redbox-react" + ] + } + ] + } + ], + [ + "babel-root-import", + { + "rootPathPrefix": "@", + "rootPathSuffix": "server" + } + ], + "transform-decorators-legacy" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b05a9ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f6ba891 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,210 @@ +{ + "parser": "babel-eslint", + "plugins": ["react"], + "env": { + "es6": true, + "browser": true, + "node": true, + "mocha": true + }, + "rules": { +/** + * Strict mode + */ + // babel inserts "use strict"; for us + // http://eslint.org/docs/rules/strict + "strict": [2, "never"], + +/** + * ES6 + */ + "no-var": 2, // http://eslint.org/docs/rules/no-var + +/** + * Variables + */ + "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow + "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names + "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars + "vars": "local", + "args": "after-used" + }], + "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define + +/** + * Possible errors + */ + "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle + "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign + "no-console": 2, // http://eslint.org/docs/rules/no-console + "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger + "no-alert": 1, // http://eslint.org/docs/rules/no-alert + "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition + "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys + "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case + "no-empty": 2, // http://eslint.org/docs/rules/no-empty + "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign + "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast + "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi + "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign + "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations + "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp + "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace + "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls + "quote-props": [1, "as-needed"], // http://eslint.org/docs/rules/quote-props + "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays + "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable + "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan + "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var + +/** + * Best practices + */ + "consistent-return": 0, // http://eslint.org/docs/rules/consistent-return + "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly + "default-case": 2, // http://eslint.org/docs/rules/default-case + "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation + "allowKeywords": true + }], + "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq + "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in + "no-caller": 2, // http://eslint.org/docs/rules/no-caller + "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return + "no-eq-null": 0, // http://eslint.org/docs/rules/no-eq-null + "no-eval": 2, // http://eslint.org/docs/rules/no-eval + "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native + "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind + "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough + "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal + "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval + "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks + "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func + "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str + "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign + "no-new": 2, // http://eslint.org/docs/rules/no-new + "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func + "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers + "no-octal": 2, // http://eslint.org/docs/rules/no-octal + "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape + "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign + "no-proto": 2, // http://eslint.org/docs/rules/no-proto + "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare + "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign + "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url + "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare + "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences + "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal + "no-with": 2, // http://eslint.org/docs/rules/no-with + "radix": 2, // http://eslint.org/docs/rules/radix + "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top + "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife + "yoda": 2, // http://eslint.org/docs/rules/yoda + +/** + * Style + */ + "indent": [0, 2], // http://eslint.org/docs/rules/ + "brace-style": [2, // http://eslint.org/docs/rules/brace-style + "1tbs", { + "allowSingleLine": true + }], + "quotes": [ + 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes + ], + "camelcase": [2, { // http://eslint.org/docs/rules/camelcase + "properties": "never" + }], + "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing + "before": false, + "after": true + }], + "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style + "eol-last": 2, // http://eslint.org/docs/rules/eol-last + "func-names": 2, // http://eslint.org/docs/rules/func-names + "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing + "beforeColon": false, + "afterColon": true + }], + "new-cap": [0, { // http://eslint.org/docs/rules/new-cap + "newIsCap": true + }], + "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines + "max": 2 + }], + "no-nested-ternary": 0, // http://eslint.org/docs/rules/no-nested-ternary + "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object + "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func + "no-trailing-spaces": 1, // http://eslint.org/docs/rules/no-trailing-spaces + "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle + "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var + "padded-blocks": [1, "never"], // http://eslint.org/docs/rules/padded-blocks + "semi": [2, "always"], // http://eslint.org/docs/rules/semi + "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing + "before": false, + "after": true + }], + "keyword-spacing": 2, // http://eslint.org/docs/rules/keyword-spacing + "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks + "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren + "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops + "spaced-comment": [0, "never"], // http://eslint.org/docs/rules/spaced-line-comment +/** + * JSX style + */ + "react/display-name": 0, + "react/jsx-boolean-value": 0, + "jsx-quotes": [2, "prefer-double"], + "react/jsx-no-undef": 2, + "react/jsx-sort-props": 0, + "react/jsx-sort-prop-types": 0, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/no-did-update-set-state": 2, + "react/no-multi-comp": 2, + "react/no-unknown-property": 2, + "react/prop-types": 2, + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/jsx-wrap-multilines": 2, + "react/sort-comp": [2, { + "order": [ + "displayName", + "constructor", + "mixins", + "statics", + "propTypes", + "getDefaultProps", + "getInitialState", + "componentWillMount", + "componentDidMount", + "componentWillReceiveProps", + "shouldComponentUpdate", + "componentWillUpdate", + "componentWillUnmount", + "/^_on.+$/", + "/^on.+$/", + "/^get.+$/", + "/^render.+$/", + "render" + ] + }] + }, + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": false, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "jsx": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50037e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +.tmp +.idea +public +*.DS_Store +*.rdb +webpack-build-stats.json +api_test_report.xml diff --git a/.jestrc b/.jestrc new file mode 100644 index 0000000..421dc47 --- /dev/null +++ b/.jestrc @@ -0,0 +1,23 @@ +{ + "verbose": true, + "moduleDirectories": [ + "node_modules", + "app" + ], + "scriptPreprocessor": "/jest/preprocessor", + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js|jsx)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "scss", + "json" + ], + "moduleNameMapper": { + "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "^.+\\.(css|scss)$": "identity-obj-proxy", + "^services$": "/app/services", + "^components$": "/app/components" + } +} diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..fccc702 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,3 @@ +# Changelog + +Init version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d6d57dd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +You can contribute via issue creation & pull/merge requests diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d1e1072 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1 @@ +MIT License diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b93a6c --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Hobo game + + +## How to run it + +``` +$ npm i +$ npm start +``` + + +And go to http://localhost:8095/ diff --git a/app/actions/.gitkeep b/app/actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/client.jsx b/app/client.jsx new file mode 100644 index 0000000..64ab202 --- /dev/null +++ b/app/client.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {render} from 'react-dom'; +import {Provider} from 'react-redux'; +import {Router} from 'react-router'; +import {syncHistoryWithStore} from 'react-router-redux'; +import {browserHistory} from 'react-router'; +import getRoutes from './routes'; +import configureStore from 'store/configureStore'; + + +const initialState = window.__INITIAL_STATE__; +const store = configureStore(initialState, browserHistory); + + +// Installs hooks that always keep react-router and redux store in sync +let history = syncHistoryWithStore(browserHistory, store); + + +render( + + + {getRoutes()} + + , document.getElementById('app') +); diff --git a/app/components/.gitkeep b/app/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/components/IndexPage.js b/app/components/IndexPage.js new file mode 100644 index 0000000..363e69d --- /dev/null +++ b/app/components/IndexPage.js @@ -0,0 +1,218 @@ +import React, {Component, PropTypes} from 'react'; +import {autobind} from 'core-decorators'; + +import _ from 'lodash'; + +import classNames from 'utils/classnames'; + +import styles from './_styles.scss'; + +const cx = classNames(styles); + +const INTERVAL = 1000; + + +export default class IndexPage extends Component { + + @autobind + _onGameStart() { + this.setState({mode: 'game'}); + } + + + @autobind + _onKeyDown(e) { + } + + + componentWillMount() { + let game = this._generateEnemies(); + + game[0][0] = 'h'; + + this.setState({game}); + } + + + componentDidMount() { + setInterval(() => this._updateEnemies(), INTERVAL); + } + + + @autobind + _onHoboMove(selectedRow) { + let {game} = this.state; + + let newGame = _.map(game, (row, j) => { + return _.map(row, (column, i) => { + if (column === 'h') { + return 0; + } + + return column; + }); + }); + + newGame[selectedRow][0] = 'h'; + + this.setState({game: newGame}); + } + + + render() { + let {mode, game, score} = this.state; + let title = this._getTitle(mode); + let block = this._getBlock(mode, game, score); + + return ( +
+
+
+
+
+ {title} +
+
+ {block} +
+
+
+
+ ); + } + + + _getTitle(mode) { + switch (mode) { + case 'start': + return 'Become a Hobo and Start a Life from The Scratch'; + + case 'game': + return 'Control your inner hobo!'; + + default: + return null; + } + } + + + _getHoboRow(field) { + return _.filter(field, column => (column.indexOf('h') !== -1))[0].indexOf('h'); + } + + + _getHoboRowNew(field) { + return _.reduce(field, (memo, row, i) => { + if (row[0] === 'h') { + return i; + } + + return 0; + }, 0); + } + + + _generateEnemies() { + return _.map(_.range(0, 4), row => { + return _.map(_.range(0, 10), (column, i) => { + if (i === 9) { + return _.sample([0, 1]); + } + + return 0; + }); + }); + } + + + @autobind + _updateEnemies() { + let {game, score} = this.state; + let hoboIndex = this._getHoboRowNew(game); + + let newGame = _.map(game, row => { + return _.slice(row, 1).concat(_.sample([0, 0, 0, 0, 1])); + }); + + if (newGame[hoboIndex][0] === 1) { + score++; + } + + newGame[hoboIndex][0] = 'h'; + + this.setState({game: newGame, score}); + } + + + _getBlock(mode, field, score) { + switch (mode) { + case 'start': + return ( +
+
+
+ Start game +
+
+ ); + + case 'game': + let hoboIndex = this._getHoboRow(field); + + let rows = _.map(field, (row, j) => { + let columns = _.map(row, (column, i) => { + if (column === 1) { + return ( +
+ ); + } + + if (column === 'h') { + return ( +
+ ); + } + + return ( +
+ ); + }); + + return ( +
+ {columns} +
+ ); + }); + + return ( +
+
+ {rows} +
+
+ {`Yout score: ${score}`} +
+
+ ); + + default: + return null; + } + } + + + state = { + mode: 'start', + score: 0, + game: [ + ['h', 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ] + } + +} + diff --git a/app/components/_styles.scss b/app/components/_styles.scss new file mode 100644 index 0000000..6d84e2a --- /dev/null +++ b/app/components/_styles.scss @@ -0,0 +1,168 @@ +body, +body * { + margin: 0; + padding: 0; + border: none; + text-decoration: none; + font-family: Helvetica, serif; + box-sizing: border-box; +} + + +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + + +.hobo-container { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: 2; + overflow: hidden; + + .hobo { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 3; + background: url(images/hobo-sitting.jpg) no-repeat center center transparent; + background-size: cover; + transform: scale(1.4); + filter: blur(20px); + } + + .game-container { + font-size: 16px; + width: 1100px; + text-align: center; + height: auto; + position: absolute; + vertical-align: middle; + display: inline-block; + top: 50%; + left: 50%; + z-index: 4; + transform: translate(-50%, -50%); + + .game { + position: relative; + z-index: 5; + width: 100%; + height: auto; + color: #fff; + font-weight: lighter; + text-transform: uppercase; + + .game-title { + width: 100%; + text-align: center; + font-size: 36px; + padding: 30px; + font-weight: lighter; + cursor: default; + text-transform: none; + } + + .game-content { + width: 100%; + padding: 30px; + text-align: center; + + + .hobo-start-game { + width: auto; + transition: 0.1s transform ease-out; + + &:hover { + transform: scale(1.1); + } + + .hobo-face { + display: block; + margin: 0 auto; + width: 100px; + height: 100px; + background: url(images/hobo-face.jpg) no-repeat center center transparent; + background-size: contain; + border-radius: 2px; + cursor: pointer; + } + + .hobo-action { + padding: 30px; + display: block; + font-size: 20px; + cursor: pointer; + } + } + + .hobo-scope { + padding: 30px; + display: block; + font-size: 20px; + cursor: pointer; + } + + .hobo-game { + border-radius: 2px; + border: 1px solid #fff; + background: rgba(255, 255, 255, 0.2); + width: 100%; + height: 414px; + position: relative; + + .hobo-player-row { + width: 100%; + height: auto; + position: relative; + white-space: nowrap; + z-index: 8; + + .hobo-player { + // transition: background 0.1s ease-out; + + &.the-hobo { + background: url(images/hobo-face.jpg) no-repeat center center transparent !important; + background-size: cover !important; + } + + &.enemy { + background: url(images/beer-bottle.jpg) no-repeat center center transparent !important; + background-size: cover !important; + } + + &.empty { + &.highlighted { + background: rgba(0, 255, 255, 0.3) !important; + + &:hover { + background: rgba(255, 255, 120, 0.3) !important; + } + } + + background: rgba(255, 255, 255, 0.05) !important; + } + + margin: 0 2px; + width: 100px; + display: inline-block; + z-index: 10; + height: 100px; + border-radius: 2px; + background-size: cover; + cursor: pointer; + } + } + } + } + } + } +} diff --git a/app/components/index.js b/app/components/index.js new file mode 100644 index 0000000..7723fc8 --- /dev/null +++ b/app/components/index.js @@ -0,0 +1,16 @@ +import Layout from './layout'; +import Dialogs from './dialogs'; +import Controls from './controls'; +import Connected from './connected'; +import Common from './common'; +import Pages from './pages'; + + +export default { + Common, + Connected, + Controls, + Dialogs, + Layout, + Pages +}; diff --git a/app/constants/.gitkeep b/app/constants/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/constants/actionTypes.js b/app/constants/actionTypes.js new file mode 100644 index 0000000..e69de29 diff --git a/app/createDevToolsWindow.js b/app/createDevToolsWindow.js new file mode 100644 index 0000000..d28cf65 --- /dev/null +++ b/app/createDevToolsWindow.js @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {DevTools, DebugPanel, LogMonitor} from 'redux-devtools/lib/react'; + + +export default function createDevToolsWindow(store) { + // Window name. + const name = 'Redux DevTools'; + + // Give it a name so it reuses the same window + const win = window.open(null, name, + 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no,width=450,height=5000'); + + // Reload in case it's reusing the same window with the old content. + win.location.reload(); + + // Set visible Window title. + win.document.title = name; + + // Wait a little bit for it to reload, then render. + setTimeout(() => ReactDOM.render(( + + + + ), win.document.body.appendChild(document.createElement('div')) + ), 10); +} diff --git a/app/helmconfig.js b/app/helmconfig.js new file mode 100644 index 0000000..b3e2921 --- /dev/null +++ b/app/helmconfig.js @@ -0,0 +1,16 @@ +export default { + title: 'Hobo', + base: {target: '_blank'}, + meta: [ + {charset: 'utf-8'}, + {'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}, + {name: 'description', content: 'Hobo'}, + {name: 'viewport', content: 'width=device-width, initial-scale=1'}, + {name: 'mobile-web-app-capable', content: 'yes'}, + {name: 'apple-mobile-web-app-capable', content: 'yes'}, + {name: 'apple-mobile-web-app-status-bar-style', content: 'white'}, + {name: 'apple-mobile-web-app-title', content: 'Hobo'}, + {name: 'msapplication-TileColor', content: '#d0d0d0'}, + {name: 'theme-color', content: '#ffffff'} + ] +}; diff --git a/app/history.js b/app/history.js new file mode 100644 index 0000000..e1b140d --- /dev/null +++ b/app/history.js @@ -0,0 +1,4 @@ +import createBrowserHistory from 'history/lib/createBrowserHistory'; + + +export default createBrowserHistory(); diff --git a/app/images/.gitkeep b/app/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/reducers/.gitkeep b/app/reducers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/reducers/index.js b/app/reducers/index.js new file mode 100644 index 0000000..e6c450f --- /dev/null +++ b/app/reducers/index.js @@ -0,0 +1,12 @@ +import {combineReducers} from 'redux'; +import {routerReducer} from 'react-router-redux'; + + +const rootReducer = {}; + +const reducer = combineReducers(Object.assign({}, rootReducer, { + routing: routerReducer +})); + + +export default reducer; diff --git a/app/routes.jsx b/app/routes.jsx new file mode 100644 index 0000000..5f6da2b --- /dev/null +++ b/app/routes.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import {Route} from 'react-router'; + +import IndexPage from 'components/IndexPage'; + + +export default function getRoutes() { + return ( + + ); +} diff --git a/app/sprites/.gitkeep b/app/sprites/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/store/.gitkeep b/app/store/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/store/configureStore.js b/app/store/configureStore.js new file mode 100644 index 0000000..41c898b --- /dev/null +++ b/app/store/configureStore.js @@ -0,0 +1,27 @@ +import {createStore, compose, applyMiddleware} from 'redux'; +import rootReducer from 'reducers'; +import thunk from 'redux-thunk'; +import {routerMiddleware} from 'react-router-redux'; +import createLogger from 'redux-logger'; + + +export default function configureStore(initialState, browserHistory) { + let getComposed = compose( + applyMiddleware(thunk), + applyMiddleware(routerMiddleware(browserHistory)), + applyMiddleware(createLogger())); + + let createStoreWithMiddleware = getComposed(createStore); + + const store = createStoreWithMiddleware(rootReducer, initialState); + + if (module.hot) { + // Enable Webpack hot module replacement for reducers + module.hot.accept('reducers', () => { + const nextReducer = require('reducers'); + store.replaceReducer(nextReducer); + }); + } + + return store; +} diff --git a/app/utils/.gitkeep b/app/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/classnames.js b/app/utils/classnames.js new file mode 100644 index 0000000..e41264c --- /dev/null +++ b/app/utils/classnames.js @@ -0,0 +1,11 @@ +import _ from 'lodash'; +import classNames from 'classnames/bind'; + + +export default function classnames(...args) { + let securedArgs = [{}].concat(args); + let merged = _.extend.apply(_, securedArgs); + + return classNames.bind(merged); + +} diff --git a/app/utils/index.js b/app/utils/index.js new file mode 100644 index 0000000..7a14eb5 --- /dev/null +++ b/app/utils/index.js @@ -0,0 +1,3 @@ +import classNames from './classnames'; + +export {classNames}; diff --git a/jest/jsdom.js b/jest/jsdom.js new file mode 100644 index 0000000..ba803ff --- /dev/null +++ b/jest/jsdom.js @@ -0,0 +1,16 @@ +const jsdom = require('jsdom').jsdom; + +let exposedProperties = ['window', 'navigator', 'document']; + +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +}; diff --git a/jest/preprocessor.js b/jest/preprocessor.js new file mode 100644 index 0000000..a08ca1c --- /dev/null +++ b/jest/preprocessor.js @@ -0,0 +1,35 @@ +const appRoot = require('app-root-path'); + +const tsc = require('typescript'); +const babelJest = require('babel-jest'); + +const tsConfig = require(appRoot + '/tsconfig.json'); + +module.exports = { + process: function process(src, path) { + const isTypeScript = path.endsWith('.ts') || path.endsWith('.tsx'); + const isJavaScript = path.endsWith('.js') || path.endsWith('.jsx'); + const isSass = path.endsWith('.scss'); + const isJson = path.endsWith('.json'); + + if (isSass) { + return ''; + } + + if (isJson) { + return JSON.parse(src); + } + + let newSrc = src; + + if (isTypeScript) { + newSrc = tsc.transpile(newSrc, tsConfig.compilerOptions); + } + + if (isJavaScript || isTypeScript) { + newSrc = babelJest.process(newSrc, isJavaScript ? path : 'file.js'); + } + + return newSrc; + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a444294 --- /dev/null +++ b/package.json @@ -0,0 +1,255 @@ +{ + "name": "ui", + "version": "0.1.0", + "description": "Hobo game", + "main": "app.js", + "scripts": { + "start": "npm run dev", + "jest": "./node_modules/jest/bin/jest.js", + "test": "NODE_ENV=test npm run jest -- --no-cache --config .jestrc", + "clean:test": "NODE_ENV=test npm run jest -- -u --no-cache --config .jestrc", + "build:clean:client": "rm -rf public/assets/app.js public/assets/*.{png,jpg,gif,css} app/sprites/*", + "build:clean:server": "rm -rf public/assets/app.server.js app/sprites/*", + "build:clean": "npm run build:clean:client && npm run build:clean:server", + "build:webpack": "NODE_ENV=production ./node_modules/parallel-webpack/bin/run.js --progress --colors --config ./webpack/webpack.config.production.js", + "build": "npm run build:clean && npm run build:webpack", + "eslint": "./node_modules/eslint/bin/eslint.js", + "lint:path": "npm run eslint -- --config .eslintrc --ext .js --ext .jsx --ext .es6 --ext .es7", + "lint:app": "npm run lint:path -- ./app", + "lint:server": "npm run lint:path -- ./server", + "lint:mock": "npm run lint:path -- ./mock", + "lint": "npm run lint:app", + "pm2": "./node_modules/pm2/bin/pm2", + "run:env": "npm run pm2 -- start", + "run:dev": "npm run run:env -- pm2.development.json", + "run:preprod": "npm run run:env -- pm2.preproduction.json", + "run:prod": "npm run run:env -- pm2.production.json", + "run": "npm run run:dev", + "dev": "npm run run:dev", + "prod": "npm run run:prod", + "logs": "npm run pm2 -- logs", + "reload": "npm run pm2 -- reload all", + "stop": "npm run pm2 -- delete all", + "ps": "npm run pm2 -- ls", + "monitor": "npm run pm2 -- monit", + "autostart": "npm run pm2 -- startup", + "psdump": "npm run pm2 -- save", + "psrestore": "npm run pm2 -- resurrect", + "psinfo": "npm run pm2 -- show", + "publish": "npm run pm2 -- publish", + "pull": "git pull origin", + "update": "git pull && npm i", + "push": "npm run lint && npm run test && git push origin", + "commit": "npm run lint && git commit", + "status": "git status", + "nvm": ". ~/.nvm/nvm.sh && nvm", + "upgrade:node": "npm run nvm -- install 6.6.0 && npm run nvm -- alias default 6.9.1 && npm run nvm -- use default", + "upgrade:project": "rm -rf node_modules && npm i && npm run pm2 -- update", + "upgrade": "npm run stop || true && npm run upgrade:node && npm run upgrade:project", + "sync": "npm run pull && npm run upgrade" + }, + "keywords": [ + "hobo", + "game", + "bottle" + ], + "private": false, + "homepage": "https://devlato.com/hobo", + "repository": { + "type": "git", + "url": "https://github.com/devlato/hobo.git" + }, + "bugs": { + "url": "https://github.com/devlato/hobo/issues", + "email": "github@devlato.com" + }, + "authors": [ + "Denis Tokarev " + ], + "license": "MIT", + "dependencies": { + "attr-accept": "^1.0.3", + "auto-loader": "0.2.0", + "axios": "^0.15.0", + "babel-cli": "6.16.0", + "babel-core": "6.17.0", + "babel-eslint": "7.0.0", + "babel-loader": "6.2.5", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-react-transform": "2.0.2", + "babel-plugin-transform-class-properties": "6.16.0", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-transform-es2015-modules-commonjs": "6.16.0", + "babel-plugin-typecheck": "3.9.0", + "babel-polyfill": "6.8.0", + "babel-preset-es2015": "6.6.0", + "babel-preset-node5": "11.1.0", + "babel-preset-node6": "^11.0.0", + "babel-preset-react": "6.5.0", + "babel-preset-stage-0": "6.16.0", + "babel-register": "6.16.3", + "babel-root-import": "4.1.3", + "babel-runtime": "6.11.6", + "big.js": "3.1.3", + "body-parser": "1.15.2", + "classnames": "2.2.5", + "compression-webpack-plugin": "0.3.1", + "connect-redis": "3.1.0", + "cookie-parser": "1.4.1", + "cookie-session": "2.0.0-alpha.1", + "core-decorators": "0.12.3", + "css-loader": "0.25.0", + "cssnano": "3.7.7", + "csurf": "1.9.0", + "deep-equal-ident": "1.1.1", + "es5-shim": "4.5.9", + "es6-enum": "1.0.3", + "es6-promise": "4.0.5", + "es6-shim": "0.35.1", + "es7-shim": "6.0.0", + "eslint": "3.7.1", + "eslint-loader": "1.5.0", + "eslint-plugin-react": "6.4.1", + "express": "4.14.0", + "express-fileupload": "0.0.5", + "express-flash": "0.0.2", + "express-session": "1.14.1", + "extract-text-webpack-plugin": "1.0.1", + "file-loader": "0.9.0", + "file-type": "^3.8.0", + "font-loader": "0.1.2", + "fs-extra": "^0.30.0", + "glob": "^7.1.0", + "graceful-fs": "4.1.9", + "helmet": "2.3.0", + "highcharts": "5.0.0", + "history": "4.3.0", + "image-size": "^0.5.0", + "immutable": "3.8.1", + "imports-loader": "0.6.5", + "ioredis": "2.4.0", + "jade": "1.11.0", + "jade-loader": "0.8.0", + "json-loader": "0.5.4", + "json3": "3.3.2", + "less": "2.7.1", + "less-loader": "2.2.3", + "lodash": "4.16.4", + "lost": "7.1.0", + "mathjs": "3.5.3", + "minimist": "1.2.0", + "moment": "2.15.1", + "node-sass": "3.10.1", + "node-uuid": "^1.4.7", + "normalize.css": "5.0.0", + "parallel-webpack": "1.5.0", + "passport": "0.3.2", + "passport-local": "1.0.0", + "pixrem": "3.0.2", + "pm2": "2.0.18", + "postcss-animation": "0.0.11", + "postcss-aspect-ratio": "1.0.0", + "postcss-assets": "4.1.0", + "postcss-autoreset": "1.2.1", + "postcss-browser-reporter": "0.5.0", + "postcss-color-rgba-fallback": "2.2.0", + "postcss-cssnext": "2.8.0", + "postcss-font-magician": "1.4.0", + "postcss-inline-media": "0.0.1", + "postcss-loader": "0.13.0", + "postcss-property-lookup": "1.2.1", + "postcss-short": "2.0.1", + "precss": "1.4.0", + "pug": "2.0.0-beta6", + "ramda": "0.22.1", + "rc-progress": "^2.0.1", + "react": "15.3.2", + "react-addons-test-utils": "15.0.2", + "react-autofill": "1.1.4", + "react-click-outside": "2.2.0", + "react-copy-to-clipboard": "4.2.3", + "react-day-picker": "2.5.0", + "react-dom": "15.3.2", + "react-dropdown": "1.1.0", + "react-ga": "2.1.2", + "react-helmet": "3.1.0", + "react-highcharts": "10.0.0", + "react-modal": "1.5.2", + "react-motion": "^0.4.4", + "react-motion-ui-pack": "0.9.0", + "react-redux": "4.4.5", + "react-router": "2.8.1", + "react-router-redux": "4.0.6", + "react-s-alert": "1.2.0", + "react-select": "1.0.0-rc.2", + "react-tooltip": "3.2.1", + "react-transform-catch-errors": "1.0.2", + "react-transform-hmr": "1.0.4", + "react-treeview": "0.4.5", + "redbox-react": "1.3.1", + "redux": "3.6.0", + "redux-devtools": "3.3.1", + "redux-devtools-dock-monitor": "1.1.1", + "redux-devtools-log-monitor": "1.0.11", + "redux-logger": "2.7.0", + "redux-mock-store": "1.2.1", + "redux-thunk": "2.1.0", + "request": "^2.75.0", + "request-promise-native": "^1.0.3", + "resolve-url-loader": "1.6.0", + "rimraf": "2.5.4", + "rucksack-css": "0.8.6", + "sass-loader": "4.0.2", + "sendgrid": "4.5.0", + "serve-static": "1.11.1", + "sjcl": "1.0.6", + "sprite-webpack-plugin": "0.3.6", + "string-template": "1.0.0", + "style-loader": "0.13.1", + "svg-loader": "0.0.2", + "svg-sprite-loader": "0.0.29", + "svg-url-loader": "1.1.0", + "timeago.js": "^2.0.2", + "ts-loader": "0.9.0", + "typescript": "2.0.3", + "urijs": "1.18.2", + "url-loader": "0.5.7", + "utf8": "2.1.1", + "uuid": "^2.0.3", + "webpack": "1.13.2", + "webpack-dev-middleware": "1.8.4", + "webpack-hot-middleware": "2.13.0", + "webpack-node-externals": "^1.5.4", + "winston": "2.2.0" + }, + "engines": { + "node": ">= 6", + "npm": ">= 3.3.12" + }, + "devDependencies": { + "app-root-path": "^2.0.1", + "axios-mock-adapter": "^1.7.0", + "babel-jest": "^16.0.0", + "babel-polyfill": "^6.8.0", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", + "chai": "3.5.0", + "cookie-parser": "1.4.3", + "cors": "^2.8.0", + "deep-freeze": "0.0.1", + "dummy-json": "^1.0.1", + "enzyme": "^2.4.1", + "identity-obj-proxy": "^3.0.0", + "jasmine-collection-matchers": "^0.2.0", + "jasmine-enzyme": "^1.2.0", + "jest": "^16.0.1", + "jsdom": "^9.6.0", + "mocha": "3.1.2", + "mocha-jenkins-reporter": "0.2.6", + "react-addons-test-utils": "^15.0.2", + "react-test-renderer": "^15.3.2", + "superagent": "2.3.0", + "supertest": "2.0.0", + "uuid": "^2.0.3" + } +} diff --git a/pm2.development.json b/pm2.development.json new file mode 100644 index 0000000..62399bd --- /dev/null +++ b/pm2.development.json @@ -0,0 +1,27 @@ +{ + "apps": [{ + "name": "Server", + "exec_interpreter": "node", + "script": "./server/run.js", + "cwd": ".", + "args": ["--dev"], + "watch": ["server", "webpack", "package.json", "pm2.development.json"], + "ignore_watch": ["server/runtime", "server/templates", "webpack/webpack.production.js", "mock"], + "watch_options": { + "persistent": true, + "follow_symlinks": true + }, + "log_date_format" : "YYYY/MM/DD HH:mm Z", + "node_args": ["--harmony"], + "error_file": "runtime/logs/stderr.log", + "out_file": "runtime/logs/stdout.log", + "instances": 1, + "exec_mode": "fork", + "max_memory_restart": "1536M", + "autorestart": true, + "merge_logs": true, + "env": { + "NODE_ENV": "development" + } + }] +} diff --git a/pm2.preproduction.json b/pm2.preproduction.json new file mode 100644 index 0000000..2f952bc --- /dev/null +++ b/pm2.preproduction.json @@ -0,0 +1,41 @@ +{ + "apps": [{ + "name": "Server instance @8095", + "exec_interpreter": "node", + "script": "./public/assets/app.server.js", + "cwd": ".", + "args": ["--prod", "--port=8095", "--serve-static"], + "watch": false, + "log_date_format": "YYYY/MM/DD HH:mm Z", + "node_args": ["--harmony"], + "error_file": "runtime/logs/stderr.log", + "out_file": "runtime/logs/stdout.log", + "instances": 1, + "exec_mode": "fork_mode", + "max_memory_restart": "2048M", + "autorestart": true, + "merge_logs": true, + "env": { + "NODE_ENV": "production" + } + }, { + "name": "Server instance @8096", + "exec_interpreter": "node", + "script": "./public/assets/app.server.js", + "cwd": ".", + "args": ["--prod", "--port=8096", "--serve-static"], + "watch": false, + "log_date_format": "YYYY/MM/DD HH:mm Z", + "node_args": ["--harmony"], + "error_file": "runtime/logs/stderr.log", + "out_file": "runtime/logs/stdout.log", + "instances": 1, + "exec_mode": "fork_mode", + "max_memory_restart": "2048M", + "autorestart": true, + "merge_logs": true, + "env": { + "NODE_ENV": "production" + } + }] +} diff --git a/pm2.production.json b/pm2.production.json new file mode 100644 index 0000000..9994885 --- /dev/null +++ b/pm2.production.json @@ -0,0 +1,41 @@ +{ + "apps": [{ + "name": "Server instance @8095", + "exec_interpreter": "node", + "script": "./public/assets/app.server.js", + "cwd": ".", + "args": ["--prod", "--port=8095"], + "watch": false, + "log_date_format": "YYYY/MM/DD HH:mm Z", + "node_args": ["--harmony"], + "error_file": "runtime/logs/stderr.log", + "out_file": "runtime/logs/stdout.log", + "instances": 1, + "exec_mode": "fork_mode", + "max_memory_restart": "2048M", + "autorestart": true, + "merge_logs": true, + "env": { + "NODE_ENV": "production" + } + }, { + "name": "Server instance @8096", + "exec_interpreter": "node", + "script": "./public/assets/app.server.js", + "cwd": ".", + "args": ["--prod", "--port=8096"], + "watch": false, + "log_date_format": "YYYY/MM/DD HH:mm Z", + "node_args": ["--harmony"], + "error_file": "runtime/logs/stderr.log", + "out_file": "runtime/logs/stdout.log", + "instances": 1, + "exec_mode": "fork_mode", + "max_memory_restart": "2048M", + "autorestart": true, + "merge_logs": true, + "env": { + "NODE_ENV": "production" + } + }] +} diff --git a/runtime/.gitkeep b/runtime/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/runtime/logs/.gitkeep b/runtime/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/.babelrc b/server/.babelrc new file mode 100644 index 0000000..825ebeb --- /dev/null +++ b/server/.babelrc @@ -0,0 +1,23 @@ +{ + "presets": [ + "node5", + "es2015", + "stage-0", + "react" + ], + "plugins": [ + "transform-class-properties", + "add-module-exports", + "transform-es2015-modules-commonjs", + ["typecheck", { + "disable": { + "production": true + } + }], + ["babel-root-import", { + "rootPathPrefix": "@", + "rootPathSuffix": "server" + }], + "transform-decorators-legacy" + ] +} diff --git a/server/api-tests.js b/server/api-tests.js new file mode 100644 index 0000000..f99da4d --- /dev/null +++ b/server/api-tests.js @@ -0,0 +1,66 @@ +import Path from 'path'; +import Fs from 'fs'; +import Mocha from 'mocha'; +// import CookieParser from 'cookie-parser'; + + +export default class ApiTest { + + static newMocha(app) { + app.logger().info('Creating Mocha...'); + + let express = app.server(); + let options = app.config().options.tests; + let testPath = app.coreDirectory('TESTS'); + + app.logger().info(`Mocha will be started with timeout ${options.timeout}...`); + app.logger().info(`Mocha will log results to "${options.reporting.log}" file using "${options.reporting.name}" reporter...`); + app.logger().info(`Mocha will use "${testPath}" as tests directory...`); + + let mocha = new Mocha({ + timeout: options.timeout, + reporter: options.reporting.reporter, + reporterOptions: { + junit_report_name: options.reporting.name, + junit_report_path: options.reporting.log + } + }); + + mocha.suite.ctx.express = express; + + app.logger().info('Mocha has been initialized successfully...'); + app.logger().info(`Finding Mocha tests in test directory "${testPath}"...`); + + Fs + .readdirSync(testPath) + .filter(fileName => fileName.substr(-3) === '.js') + .forEach(fileName => { + app.logger().info(`Attaching Mocha test "${fileName}"...`); + mocha.addFile(Path.join(testPath, fileName)); + }); + + app.logger().info('Mocha tests were attached successfully...'); + + return mocha; + } + + + static run(app) { + app.logger().info('Starting API Tests...'); + + let mocha = this.newMocha(app); + + app.logger().info('Starting Mocha...'); + mocha.run((failures) => { + if (failures) { + app.logger().info('Mocha has detected some test failures: ', failures); + + if (!!failures.stack) { + app.logger().error('Mocha test failure details: ', failures.stack); + } + } + + app.logger().info('No failures in Mocha tests has been detected'); + }); + } +} diff --git a/server/app.js b/server/app.js new file mode 100755 index 0000000..794554c --- /dev/null +++ b/server/app.js @@ -0,0 +1,61 @@ +import App from '@/core/bootstrap'; +// import ApiTests from './api-tests'; + + +class AppFactory { + + static app() { + if (!this._app) { + this._app = AppFactory.newApp(); + } + + return this._app; + } + + + static server() { + return AppFactory.app().server(); + } + + + static newApp() { + return new App(); + } + + + static async run() { + return await AppFactory.app().run(); + } + + + static async handleSignals() { + this.server().on('SIGNIT', () => { + console.log('Stopping server...'); + this.server().close(); + }); + } + + + static async start() { + try { + let app = await AppFactory.run(); + this.handleSignals(); + return app; + } catch (e) { + console.log('Failed to start app: ', e.message || '[no message]'); + console.error(e.stack); + } + } +} + +(async() => { + try { + let app = await AppFactory.start(); + // if (app.environment() === 'development') { + // ApiTests.run(app); + // } + } catch (e) { + console.log('Failed to start app: ', e.message || '[no message]'); + console.error(e.stack); + } +})(); diff --git a/server/configuration/.gitignore b/server/configuration/.gitignore new file mode 100644 index 0000000..fff339e --- /dev/null +++ b/server/configuration/.gitignore @@ -0,0 +1,2 @@ +local.js + diff --git a/server/configuration/.gitkeep b/server/configuration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/configuration/config.js b/server/configuration/config.js new file mode 100644 index 0000000..adb8fb3 --- /dev/null +++ b/server/configuration/config.js @@ -0,0 +1,37 @@ +import Path from 'path'; +import Paths from '@/core/constants/paths'; + + +module.exports = { + server: { + environment: 'development', + port: 8095, + showPort: true + }, + logger: console, + csrf: { + path: '/api', + keyTtl: 3 * 60 * 60 * 1000 // Key per day + }, + session: { + name: 'uiSessionId', + routes: '*', + redisOptions: { + prefix: 'uiSessionId:', + ttl: 3 * 60 * 60, + redisConnectionCheckPeriod: 200 + }, + cookie: { + secure: false + }, + secret: 'secret shit' + }, + cookies: { + secret: 'secret shit' + }, + routing: { + routes: { + '[GET] *': 'EntryPointController.getEntryPoint' + } + } +}; diff --git a/server/configuration/development.js b/server/configuration/development.js new file mode 100644 index 0000000..a81ad09 --- /dev/null +++ b/server/configuration/development.js @@ -0,0 +1,50 @@ +'use strict'; + + +import _ from 'lodash'; +import Webpack from 'webpack'; +import configs from '../../webpack'; + + +export default (config, cmdArgs) => { + let webpackConfig; + let Logger = config.options.logger; + let configPath = config.paths.files.WEBPACK_CONFIGURATION; + + try { + webpackConfig = configs[config.options.server.environment]; + Logger.info(`Webpack configuration has been found by the path "${configPath}"...`); + } catch (e) { + Logger.info(`No webpack configuration with the path "${configPath}" found, skipping"...`); + Logger.error(e.stack); + webpackConfig = {}; + } + + return { + webpack: _.merge({}, config.options.webpack, { + options: webpackConfig, + compiler: _.isEmpty(webpackConfig) ? {} : Webpack(webpackConfig) + }), + server: { + environment: 'development' + }, + middlewares: { + before: [ + 'appToRequest', + 'webpackDev', + 'webpackHot', + 'pureSend', + 'jsonRequest', + 'safeRequest', + 'requestTime', + 'cookieParser', + 'session', + // 'csrf', + 'serveStatic' + ], + after: [ + 'requestException' + ] + } + }; +}; diff --git a/server/configuration/index.js b/server/configuration/index.js new file mode 100644 index 0000000..4913981 --- /dev/null +++ b/server/configuration/index.js @@ -0,0 +1,12 @@ +import config from './config'; +import development from './development'; +import production from './production'; +import target from './target'; + + +export default { + config, + development, + production, + target +}; diff --git a/server/configuration/production.js b/server/configuration/production.js new file mode 100644 index 0000000..ee2376b --- /dev/null +++ b/server/configuration/production.js @@ -0,0 +1,31 @@ +'use strict'; + + +export default (config, cmdArgs) => { + let middlewaresBefore = [ + 'appToRequest', + 'pureSend', + 'jsonRequest', + 'safeRequest', + 'requestTime', + 'cookieParser', + 'session' + ]; + + if (cmdArgs['serve-static']) { + middlewaresBefore.push('serveStatic'); + } + + return { + server: { + environment: 'production', + showPort: false + }, + middlewares: { + before: middlewaresBefore, + after: [ + 'requestException' + ] + } + }; +} diff --git a/server/configuration/target.js b/server/configuration/target.js new file mode 100644 index 0000000..5c688ed --- /dev/null +++ b/server/configuration/target.js @@ -0,0 +1,6 @@ +'use strict'; + + +module.exports = { + // Here are the settings which will be redefined on deploy +}; diff --git a/server/controllers/.gitkeep b/server/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/controllers/EntryPointController.js b/server/controllers/EntryPointController.js new file mode 100644 index 0000000..f9db9fc --- /dev/null +++ b/server/controllers/EntryPointController.js @@ -0,0 +1,14 @@ +'use strict'; + + +import Controller from '@/core/base/Controller'; + + +export default class EntryPointController extends Controller { + + async getEntryPoint(request, response) { + response.render('index', { + state: 'hobo' + }); + } +} diff --git a/server/controllers/index.js b/server/controllers/index.js new file mode 100644 index 0000000..f10ab1e --- /dev/null +++ b/server/controllers/index.js @@ -0,0 +1,6 @@ +import EntryPointController from './EntryPointController'; + + +export default { + EntryPointController +}; diff --git a/server/core/.gitkeep b/server/core/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/core/base/.gitkeep b/server/core/base/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/core/base/Component.js b/server/core/base/Component.js new file mode 100644 index 0000000..ebdc076 --- /dev/null +++ b/server/core/base/Component.js @@ -0,0 +1,258 @@ +'use strict'; + + +import _ from 'lodash'; +import ComponentAutowireException from '@/core/exceptions/ComponentAutowireException'; +import ComponentNotFoundException from '@/core/exceptions/ComponentNotFoundException'; +import {autobind} from 'core-decorators'; + + +class Component { + + constructor(app) :Component { + this._app = app; + + this.preConstruct(app); + + return this; + } + + + preConstruct(app) { + return this; + } + + + @autobind + postConstruct() { + this._processAutowired(); + } + + + @autobind + autowired() { + let wire; + + if ((arguments.length === 1) + && (_.isArray(arguments[0]) || _.isPlainObject(arguments[0]))) { + wire = arguments[0]; + } else { + wire = arguments; + } + + this._itemsToAutowire = wire; + + return this; + } + + + @autobind + _processAutowired() { + let autowireItems = this.autowireItems(); + if (!_.isEmpty(autowireItems)) { + this.logger().info(`Trying to autowire required components...`); + this._autowire(this.autowireItems()); + } + + return this; + } + + + @autobind + autowireItems() { + return this._itemsToAutowire; + } + + + @autobind + _toLowerCamelCase(string) { + return string.replace(/^[a-z]/i, (letter) => { + return letter.toLowerCase(); + }); + } + + + @autobind + _autowire(items) { + let logger = this.logger(); + logger.info(`Items to autowire found: [${_.values(items).join(', ')}]`); + + _.each(items, (item, key) => { + let autoWiredName = key; + if (_.isNumber(+autoWiredName) && !_.isNaN(+autoWiredName)) { + autoWiredName = this._toLowerCamelCase(item); + } + + try { + let instance = this.getComponent(item); + + if (!instance) { + throw new ComponentAutowireException(item, autoWiredName, this.constructor.name); + } + + this[autoWiredName] = instance; + + logger.info(`Autowired ${item} instance as ${this.constructor.name}.${autoWiredName}`); + } catch (e) { + logger.info(`Cannot autowire item "${autoWiredName}": `, e.message || '[no message]'); + logger.error(e.stack || '[no stack]'); + throw e; + } + }); + } + + + @autobind + app() { + return this._app; + } + + + @autobind + config() { + return this.app().config(); + } + + + @autobind + models() { + return this.app().models(); + } + + + @autobind + controllers() { + return this.app().controllers(); + } + + + @autobind + components() { + return this.app().components(); + } + + + @autobind + services() { + return this.app().services(); + } + + + @autobind + validators() { + return this.app().validators(); + } + + + @autobind + validations() { + return this.app().validations(); + } + + + @autobind + middlewares() { + return this.app().middlewares(); + } + + + @autobind + logger() { + return this.app().logger(); + } + + + @autobind + getModel(modelId :string) { + let model = this.models()[modelId]; + + if (!model) { + throw new ComponentNotFoundException(modelId, 'No model found'); + } + + return model; + } + + + @autobind + getController(controllerId :string) { + let controller = this.controllers()[controllerId]; + + if (!controller) { + throw new ComponentNotFoundException(controllerId, 'No controller found'); + } + + return controller; + } + + + @autobind + getComponent(componentId :string) { + let component = this.components()[componentId]; + + if (!component) { + throw new ComponentNotFoundException(componentId, 'No component found'); + } + + return component; + } + + + @autobind + getService(serviceId :string) { + let service = this.services()[serviceId]; + + if (!service) { + throw new ComponentNotFoundException(serviceId, 'No service found'); + } + + return service; + } + + + @autobind + getValidator(validatorId :string) { + let validator = this.validators()[validatorId]; + + if (!validator) { + throw new ComponentNotFoundException(validatorId, 'No validator found'); + } + + return validator; + } + + + @autobind + getValidation(validationId :string) { + let validation = this.validations()[validationId]; + + if (!validation) { + throw new ComponentNotFoundException(validationId, 'No validation found'); + } + + return validation; + } + + + @autobind + getMiddleware(middlewareName :string) { + let middleware = this.middlewares()[middlewareName]; + + if (!middleware) { + throw new ComponentNotFoundException(middlewareName, 'No middleware found'); + } + + return middleware; + } + + + @autobind + log() { + let logger = this.logger(); + + return logger.log.apply(logger, arguments); + } +} + + +export default Component; diff --git a/server/core/base/Controller.js b/server/core/base/Controller.js new file mode 100644 index 0000000..cd8358e --- /dev/null +++ b/server/core/base/Controller.js @@ -0,0 +1,32 @@ +'use strict'; + + +import Component from '@/core/base/Component'; +import NotImplementedMethodException from '@/core/exceptions/NotImplementedMethodException'; + + +class Controller extends Component { + + async get(request, response) { + throw new NotImplementedMethodException(this.name || 'Controller', 'GET'); + } + + + async post (request, response) { + throw new NotImplementedMethodException(this.name || 'Controller', 'POST'); + } + + + async put (request, response) { + throw new NotImplementedMethodException(this.name || 'Controller', 'PUT'); + } + + + async delete(request, response) { + throw new NotImplementedMethodException(this.name || 'Controller', 'DELETE'); + } + +} + + +export default Controller; diff --git a/server/core/base/Exception.js b/server/core/base/Exception.js new file mode 100644 index 0000000..22c5589 --- /dev/null +++ b/server/core/base/Exception.js @@ -0,0 +1,55 @@ +'use strict'; + + +import format from 'string-template'; + + +class Exception { + + constructor( + message :string, + data = {} + ) :Exception { + this._error = new Error(message); + this._data = data; + + // Compatibility + this.message = this.formattedMessage(); + this.stack = this.formattedStackTrace(); + + return this; + } + + + error() :Error { + return this._error; + } + + + data() :Object { + return this._data; + } + + + formattedMessage() :string { + return format(this.rawMessage(), this.data()); + } + + + formattedStackTrace() { + return format(this.rawStackTrace(), this.data()); + } + + + rawMessage() { + return this.error().message; + } + + + rawStackTrace() { + return this.error().stack; + } +} + + +export default Exception; diff --git a/server/core/base/Service.js b/server/core/base/Service.js new file mode 100644 index 0000000..a8904fb --- /dev/null +++ b/server/core/base/Service.js @@ -0,0 +1,12 @@ +'use strict'; + + +import Component from '@/core/base/Component'; + + +class Service extends Component { + +} + + +export default Service; diff --git a/server/core/bootstrap.js b/server/core/bootstrap.js new file mode 100644 index 0000000..394e6ce --- /dev/null +++ b/server/core/bootstrap.js @@ -0,0 +1,676 @@ +import Fs from 'graceful-fs'; +import Controller from '@/core/base/Controller'; +import Express from 'express'; +import _ from 'lodash'; +import Passport from 'passport'; +import { + DEFAULT_CONFIG_PATH, + DEFAULT_ENVIRONMENT, + DEFAULT_HOST, + DEFAULT_PORT, + EMPTY_ROUTING_PREFIX, + EMPTY_REGEX_DELIMITERS, + DEFAULT_ROUTING_AUTH_ROUTE_REGEXP, + DEFAULT_ROUTING_AUTH_REPLACE_PATTERN, + DEFAULT_ROUTING_AUTH_EMPTY_PATTERN, + EXIT_STATUS_INITIALIZATION_FAILED +} from '@/core/constants'; +import ServerParameters from '@/core/enumerations/ServerParameters'; +import ComponentConflictException from '@/core/exceptions/ComponentConflictException'; +import SessionExpiredException from '@/core/exceptions/SessionExpiredException'; +import HttpNotFoundException from '@/core/exceptions/HttpNotFoundException'; + +import { controllers, models, middlewares, services, schemas, validators } from '../index'; +import config from '@/core/skeleton-config'; + +import {autobind} from 'core-decorators'; + + +export default class App { + + constructor( + configPath :string = DEFAULT_CONFIG_PATH, + environment :string = DEFAULT_ENVIRONMENT + ) :App { + this._setConfiguration(environment); + this._init(); + + return this; + } + + + @autobind + _setConfiguration(environment :string) :App { + this._config = config(environment, this); + + return this; + } + + + _init() :App { + this.server(); + + this._initServer(); + this._setBeforeMiddlewares(); + this._setServices(); + this._setControllers(); + this._setRenderEngine(); + this._bind(); + this._setAfterMiddlewares(); + this._postBind(); + + return this; + } + + + @autobind + _newServer() :Object { + this.logger().info('Creating new Express server instance...'); + + let server = Express(); + server._startTime = new Date(); + + return server; + } + + + @autobind + _postBind() { + this.logger().info('Performing post-construction and injection...'); + _.each(this.components(), (component) => { + if (_.isFunction(component.postConstruct)) { + component.postConstruct(); + } + }); + + return this; + } + + + @autobind + config() :Object { + return this._config; + } + + + @autobind + server() :Object { + if (!this._server) { + this._server = this._newServer(); + } + + return this._server; + } + + + @autobind + router() :Router { + return server().router(); + } + + + @autobind + _initServer() :App { + this.logger().info('Initializing server...'); + //this.server()(ServerParameters.PORT, this.config().options.port); + this._performInitialHealthcheck(); + + return this; + } + + + @autobind + _addComponent(component, name) :App { + this.logger().info(`Trying to add component "${name}" of type "${component.constructor.name}"...`); + + let server = this.server(); + + if (!server._components) { + server._components = {}; + } + + if (_.has(server._components, name)) { + throw new ComponentConflictException(name, server._components[name], component); + } + + server._components[name] = component; + + this.logger().info(`Component "${name}" of type "${component.constructor.name}" has been added`); + + return this; + } + + + @autobind + _addComponents(components) { + _.each(components, this._addComponent); + + return this; + } + + + @autobind + _setBeforeMiddlewares() :App { + this.logger().info('Setting middlewares to call before request...'); + this.logger().info('Enabled middlewares to execute before request are ' + + `[${_.values(this.config().options.middlewares.before).join(', ')}]`); + _.each( + this.config().options.middlewares.before, + (middlewareName) => this._addMiddleware(middlewareName, true)); + + return this; + } + + + @autobind + _setAfterMiddlewares() :App { + this.logger().info('Setting middlewares to call after request...'); + this.logger().info('Enabled middlewares to execute after request are ' + + `[${_.values(this.config().options.middlewares.after).join(', ')}]`); + _.each( + this.config().options.middlewares.after, + (middlewareName) => this._addMiddleware(middlewareName, false)); + + return this; + } + + + @autobind + _addMiddleware(middlewareName :string) { + this.logger().info(`Adding middleware ${middlewareName}...`); + + // let basePath = isBeforeRequest + // ? this.coreDirectory('MIDDLEWARES_BEFORE_REQUEST') + // : this.coreDirectory('MIDDLEWARES_AFTER_REQUEST'); + let middleware = middlewares[middlewareName]; + let server = this.server(); + if (!server._middlewares) { + server._middlewares = {}; + } + + this.logger().info(`Middleware ${middlewareName} loaded, attaching...`); + + if (_.isFunction(middleware.register)) { + this.logger().info(`Trying to attach middleware ${middlewareName} with .register() method`); + middleware.register(this); + server._middlewares[middlewareName] = middleware.register; + } else if (middleware.length === 3) { + this.logger().info(`Trying to attach middleware ${middlewareName} via .use() method`); + server.use(middleware); + server._middlewares[middlewareName] = middleware; + } else if (middleware.length === 1) { + this.logger().info(`Trying to attach parametrized middleware ${middlewareName} via .use() method`); + server.use(middleware(this)); + server._middlewares[middlewareName] = middleware; + } + + return this; + } + + + @autobind + _setRenderEngine() :App { + this.logger().info('Setting view engine...'); + + let options = this.config().options.views; + let server = this.server(); + + let viewsDir = this.coreDirectory('VIEWS'); + + this.logger().info(`Adding "${options.engineName}" as view engine option...`); + server.set(ServerParameters.VIEW_ENGINE, options.engineName); + + this.logger().info(`Setting "${viewsDir}" as home folder for views...`); + server.set(ServerParameters.VIEWS_DIR, viewsDir); + + this.logger().info(`${options.cached ? 'Enabling' : 'Disabling'} view caching...`); + server.set(ServerParameters.VIEW_CACHE, options.cached); + + this.logger().info(`Setting "${options.engineName}" as default view engine...`); + server.engine(options.engineName, options.engine); + + return this; + } + + + @autobind + _setControllers() :App { + this.logger().info('Setting controllers...'); + + this.logger().info('Creating controller instances...'); + this.server()._controllers = _.reduce( + controllers, + (memo, ControllerClass, key :string) => { + if (this._isController(ControllerClass)) { + memo[key] = new ControllerClass(this); + this.logger().info(`${ControllerClass.name} controller has been attached successfully`); + } + + return memo; + }, {}); + this._addComponents(this.server()._controllers); + + return this; + } + + + @autobind + _isController(ControllerClass) :boolean { + let isController = ControllerClass instanceof Controller.constructor; + + if (isController) { + this.logger().info(`Controller ${ControllerClass.name} has been found`); + } + + return isController; + } + + + @autobind + _setServices() :App { + this.logger().info('Setting services...'); + + this.logger().info('Creating service instances...'); + this.server()._services = _.reduce( + services, + (memo, ServiceClass, key :string) => { + if (this._isService(ServiceClass)) { + memo[key] = new ServiceClass(this); + this.logger().info(`${ServiceClass.name} service has been attached successfully`); + } + + return memo; + }, {}); + this._addComponents(this.server()._services); + + return this; + } + + + @autobind + _isService(ServiceClass) :boolean { + let isService = ServiceClass instanceof Service.constructor; + + if (isService) { + this.logger().info(`Service ${ServiceClass.name} has been found`); + } + + return isService; + } + + + @autobind + environment() :string { + return this.config().options.server.environment; + } + + + @autobind + controllers() :Object { + return this.server()._controllers || {}; + } + + + @autobind + components() :Object { + return this.server()._components || {}; + } + + + @autobind + services() :Object { + return this.server()._services || {}; + } + + + @autobind + middlewares() :Object { + return this.server()._middlewares || {}; + } + + + @autobind + logger() { + return this.config().options.logger; + } + + + @autobind + _bind() :App { + this._bindControllers(); + } + + + @autobind + _bindControllers() :App { + this.logger().info('Binding server for usage...'); + let routing = this.config().options.routing; + let _controllers = this.controllers(); + let supportedMethods = this.config().options.routing.supportedMethods; + + this.logger().info('Setting routes...'); + this.logger().info('ROUTING', routing); + this.logger().info('ROUTES', routing.routes); + _.each( + routing.routes, + (controllerMethodJoined, routeMethodJoined) => { + let {authProvider, controllerId, controllerMethod} = this._extractControllerMethod(controllerMethodJoined); + let {route, httpMethod} = this._extractRouteMethod(routeMethodJoined); + if (supportedMethods.indexOf(httpMethod) === -1) { + this.logger().info(`${httpMethod} Method is not supported, skipping binding...`); + return false; + } + this.logger().info(`[${httpMethod}] ${route} wired for controller ${controllerId}@${controllerMethod}`); + this._bindControllerSafely(_controllers[controllerId], controllerMethod, + route, httpMethod, authProvider); + }); + + return this; + } + + + @autobind + _extractControllerMethod(controllerMethodJoined :string) { + this.logger().info(`Extracting target controller name and method from ${controllerMethodJoined}...`); + let [controllerIdPrepared, controllerMethod] = _.filter( + _.map( + controllerMethodJoined.split(this.config().options.routing.controllerMethodDelimiter), + (item) => { + return item.trim(); + }), + (item) => !_.isEmpty(item)); + + let parts = _.reverse( + _.filter( + _.map( + controllerIdPrepared.split(/\s+/ig), + (item) => { + return item.trim(); + }), + (item) => { + return !!item; + })); + + let [controllerId, authProvider] = parts; + + if (this._isAuthProvider(authProvider)) { + authProvider = this._extractAuthProvider(authProvider); + } + + this.logger().info(`Extracted controller name "${controllerId}" and method name "${controllerMethod}"`); + + return {authProvider, controllerId, controllerMethod}; + } + + + @autobind + _isAuthProvider(authProvider) { + return authProvider && authProvider.match(DEFAULT_ROUTING_AUTH_ROUTE_REGEXP); + } + + + @autobind + _extractAuthProvider(authProvider) { + return authProvider.replace(DEFAULT_ROUTING_AUTH_REPLACE_PATTERN, + DEFAULT_ROUTING_AUTH_EMPTY_PATTERN); + } + + + @autobind + _extractRouteMethod(routeMethodJoined :string) { + this.logger().info(`Extracting target route name HTTP method from ${routeMethodJoined}...`); + let _config = this.config().options.routing; + let result = _.filter( + _.map( + routeMethodJoined.split(_config.routeMethodDelimiter), + (item) => { + return item.trim().replace( + _config.routeMethodCleanupPattern, + _config.routeMethodCleanupReplace); + }), + (item) => !_.isEmpty(item)); + + if (result.length === 1) { + result = [_config.defaultHttpMethod].concat(result); + } + + let [httpMethod, route] = result; + + httpMethod = httpMethod.toUpperCase(); + + this.logger().info(`Extracted route "${route}" and HTTP method "${httpMethod}"`); + + return {route, httpMethod}; + } + + + @autobind + _bindControllerSafely( + controllerInstance :Controller, + controllerMethod :string, + route :string, + httpMethod :string, + authProvider :mixed + ) :App { + this.logger().info(`Binding route handler for ${route} route...`); + let server = this.server(); + let routeToBind = route; + + if (this._isRegexRoute(routeToBind)) { + this.logger().info(`Route "${routeToBind}" is a regular expression, converting...`); + routeToBind = this._getRegexRoute(routeToBind); + this.logger().info(`Route "${route}" has been converted to RegExp instance ("${routeToBind}")`); + } + + if (!authProvider) { + this.logger().info( + `Binding ${controllerInstance.constructor.name}.${controllerMethod} ` + + `route handler for ${route} route and ${httpMethod} method...`); + server[httpMethod.toLowerCase()](routeToBind, + this._getRequestHandler(controllerInstance, controllerMethod)); + } else { + this.logger().info( + `Binding ${controllerInstance.constructor.name}.${controllerMethod} ` + + `route handler for ${route} route and ${httpMethod} method with passport ` + + `preauthorize "${authProvider}" strategy...`); + + let authProviderInstance = this._getAuthProvider(authProvider); + + server[httpMethod.toLowerCase()](routeToBind, authProviderInstance, + this._getRequestHandler(controllerInstance, controllerMethod)); + } + + return this; + } + + + @autobind + _getAuthProvider(authProvider) { + switch (authProvider) { + case 'local': + return Passport.authenticate(authProvider); + + case 'authenticated': + return (request, response, next) => { + if (_.isEmpty(request.user) || _.isEmpty(request.user.id)) { + return next(new SessionExpiredException('Not authenticated')); + } + + next(); + }; + + case 'account-manager': + return (request, response, next) => { + let {user} = request; + + if (_.isEmpty(user) || _.isEmpty(user.id)) { + return next(new SessionExpiredException('Not authenticated')); + } + + let isAccountManager = user.roles && (user.roles.indexOf('ROLE_ADMIN') !== -1); + + if (!isAccountManager) { + return next(new HttpNotFoundException('API endpoint is not found')); + } + + next(); + }; + + default: + return (request, response, next) => { + this.logger().info('Empty authentication provider worked'); + next(); + }; + } + } + + + @autobind + _getRegexRoute(route :string) { + let _config = this.config().options.routing; + let cleanRoute = route + .replace(_config.regexPrefix, EMPTY_ROUTING_PREFIX) + .replace(_config.regexDelimiters, EMPTY_REGEX_DELIMITERS); + + return new RegExp(cleanRoute, 'ig'); + } + + + @autobind + _isRegexRoute(route :string) { + return route.startsWith(this.config().options.routing.regexPrefix); + } + + + @autobind + _getRequestHandler(controllerInstance :Controller, method :string) :Function { + let logger = this.logger(); + + logger.info(`Getting controller method safe proxy for ${method} method...`); + return _.bind(async (request, response, next) => { + try { + logger.info(`Processing request to uri "${request.url}"...`); + return await controllerInstance[method](request, response); + } catch (e) { + logger.info('Exception while processing request occurred: ', e.message || '[no error message]'); + logger.error(e.stack); + next(e); + } + }, controllerInstance); + } + + + @autobind + _corePaths() { + return this.config().paths; + } + + + @autobind + _coreDirectories() { + return this._corePaths().directories; + } + + + @autobind + _coreWebPaths() { + return this._corePaths().web; + } + + + @autobind + _coreFiles() { + return this._corePaths().files; + } + + + @autobind + coreDirectory(key) { + return this._coreDirectories()[key]; + } + + + @autobind + coreWebPath(key) { + return this._coreWebPaths()[key]; + } + + + @autobind + coreFile(key) { + return this._coreFiles()[key]; + } + + + @autobind + _getPathsSorted(paths) { + return _.sortBy(paths); + } + + + @autobind + _performInitialHealthcheck() { + let logger = this.logger(); + logger.info('Performing app structure self-check...'); + + let failed = _.some( + this._getPathsSorted(this.config().healthcheck), + (path) => { + logger.info(`Checking directory "${path}" existence...`); + try { + Fs.mkdirSync(path); + logger.info(`Directory "${path}" had not been found so it was created automatically`); + } catch (error) { + if (error) { + if (error.code !== 'EEXIST') { + logger.info(`Cannot create required directory "${path}": `, + error.message || error || '[no message]'); + logger.error(error.stack || '[no trace]'); + + return true; + } + } + } + + return false; + }); + + if (failed) { + logger.info("Cannot start application because it's structure is corrupted.\nPlease check log to correct errors."); + process.exit(EXIT_STATUS_INITIALIZATION_FAILED); + } else { + logger.info('App structure is correct, all the required paths exist'); + } + } + + + @autobind + uptime() { + return new Date() - this.server()._startTime; + } + + + @autobind + async run() :Promise { + let _config = this.config().options.server; + let host = '127.0.0.1'; + let port = _config.port || DEFAULT_PORT; + + this.logger().info(`Starting app on http://${host}:${port}/...`); + + //this._bind(); + this.logger().info('Server handlers has been successfully bound'); + return new Promise((resolve, reject) => { + this.server().listen(port, host, () => { + this.logger().log(`Server has been started successfully at http://${host}:${port}/`); + this.logger().log(`Start elapsed ${this.uptime()}ms`); + + resolve(this); + }); + + this.server().on('error', (e) => { + reject(e); + }); + }); + } +} diff --git a/server/core/components/.gitkeep b/server/core/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/core/components/Converter.js b/server/core/components/Converter.js new file mode 100644 index 0000000..71c2659 --- /dev/null +++ b/server/core/components/Converter.js @@ -0,0 +1,29 @@ +'use strict'; + + +import _ from 'lodash'; + +import Component from '@/core/base/Component'; + + +export default class Converter extends Component { + + fields = { + // fieldName: 'Converter' or {converter: 'Converter', args: {}} + }; + + + constructor(app, fields) :Converter { + super(app); + + this.fields = fields; + + return this; + } + + + convert(field, args) { + + } + +} diff --git a/server/core/components/Pager.js b/server/core/components/Pager.js new file mode 100644 index 0000000..61f9eda --- /dev/null +++ b/server/core/components/Pager.js @@ -0,0 +1,85 @@ +'use strict'; + + +import _ from 'lodash'; +import { + DEFAULT_PAGE, + DEFAULT_ITEMS_PER_PAGE_AMOUNT, + DEFAULT_SORT_DIRECTION, + DEFAULT_SORT_FIELDS +} from '@/core/constants'; + + +export default class Pager { + + constructor( + page :mixed = DEFAULT_PAGE, + itemsPerPage :number = DEFAULT_ITEMS_PER_PAGE_AMOUNT, + sortDirection :string = DEFAULT_SORT_DIRECTION, + sortColumns :Array = DEFAULT_SORT_FIELDS + ) :Pager { + if (_.isObject(page) && (arguments.length === 1)) { + return this._fromQueryObject(page); + } + + this._page = page; + this._itemsPerPage = itemsPerPage; + this._sortDirection = sortDirection; + this._sortColumns = !_.isArray(sortColumns) + ? sortColumns + ? [sortColumns] + : undefined + : sortColumns; + + return this; + } + + + _fromQueryObject(options) :Pager { + this._page = options.page; + this._itemsPerPage = options.itemsPerPage; + this._sortDirection = options.sortDirection; + this._sortColumns = !_.isArray(options.sortColumns) + ? options.sortColumns + ? [options.sortColumns] + : undefined + : options.sortColumns; + + return this; + } + + + static fromRequest(request) { + return new Pager(request.query); + } + + + page() { + return this._page; + } + + + itemsPerPage() { + return this._itemsPerPage; + } + + + sortDirection() { + return this._sortDirection; + } + + + sortColumns() { + return this._sortColumns; + } + + + toQueryObject() :Object { + return { + page: this.page(), + itemsPerPage: this.itemsPerPage(), + sortDirection: this.sortDirection(), + sortColumns: this.sortColumns() + }; + } +} diff --git a/server/core/components/Validation.js b/server/core/components/Validation.js new file mode 100644 index 0000000..78e2baf --- /dev/null +++ b/server/core/components/Validation.js @@ -0,0 +1,94 @@ +'use strict'; + + +import _ from 'lodash'; + +import Component from '@/core/base/Component'; +import Validator from '@/core/components/Validator'; +import ValidationException from '@/core/exceptions/ValidationException'; +import SchemaValidationException from '@/core/exceptions/SchemaValidationException'; + + +export default class Validation extends Component { + + fields = {}; + + _validators = {}; + + + constructor(app, fields) :Validation { + super(app); + + this.fields = fields; + // this.setFieldsValidators(); + + return this; + } + + + postConstruct() { + this.setFieldsValidators(); + } + + + validateRequest(request) :Object { + let data = _.merge({}, request.params, request.query, request.body); + + return this.validateData(data); + } + + + validateData(data) :Object { + try { + return this.validate(data); + } catch (e) { + throw new SchemaValidationException(e.message, e.suppressedErrors); + } + } + + + getFieldValidators(validators) { + let validatorItems = validators; + + if (!_.isArray(validatorItems)) { + validatorItems = [validatorItems]; + } + + return new Validator(this.app(), validatorItems).setDependencies(); + } + + + setFieldsValidators() { + this._validators = _.mapValues(this.fields, (validators, fieldName) => { + return this.getFieldValidators(validators); + }); + + return this; + } + + + validate(data) :Object { + let validFields = {}; + + let validated = _.reduce( + _.keys(this.fields), + (memo, fieldName, key) => { + try { + let validator = this._validators[fieldName]; + validator.validate(data[fieldName], data); + validFields[fieldName] = data[fieldName]; + } catch (e) { + memo[fieldName] = e.suppressedErrors; + } + + return memo; + }, + {}); + + if (!_.isEmpty(validated)) { + throw new ValidationException(validated, 'Validation error'); + } + + return validFields; + } +} diff --git a/server/core/components/Validator.js b/server/core/components/Validator.js new file mode 100644 index 0000000..2275f7d --- /dev/null +++ b/server/core/components/Validator.js @@ -0,0 +1,154 @@ +'use strict'; + + +import _ from 'lodash'; + +import Component from '@/core/base/Component'; +import ValidationException from '@/core/exceptions/ValidationException'; + + +export default class Validator extends Component { + + dependencies = []; + + _validators = []; + + + constructor(app, dependencies = {}) { + super(app); + + this.dependencies = dependencies || {}; + + return this; + } + + + preConstruct(app) { + // this.setDependencies(); + } + + + postConstruct() { + this.setDependencies(); + } + + + setDependencies() :Validator { + this._validators = _.map(this.getDependencies(), (dependency) => { + let validator = dependency; + + if (_.isPlainObject(dependency)) { + validator = dependency.validator; + } + + if (_.isString(validator)) { + validator = this.getValidator(validator); + } else if (!this.app()._isValidatorInstance(validator) && this.app()._isValidatorFunction(validator)) { + validator = this.getWrappedFunctionValidator(validator); + } + + return validator; + }); + + return this; + } + + + getWrappedFunctionValidator(fn) { + let validator = new Validator(this.app()); + + validator.doValidation = function(source, data = {}, args = {}) { + return fn(source, data, args); + }; + + return validator; + } + + + preValidate(source, data = {}) { + let dependencies = this.getDependencies(); + + let errors = _.map(this.getValidators(), (validator, key) => { + let args = {}; + let dependency = dependencies[key]; + + if (_.isPlainObject(dependency)) { + args = dependency.args || {}; + } + + try { + validator.validate(source, data, args); + return null; + } catch (e) { + return e; + } + }); + + return _.reduce( + errors, + (memo, error) => { + if (!_.isEmpty(error)) { + return memo.concat(error); + } + return memo; + }, + []); + } + + + doValidation(source, data = {}, args = {}) { + // Validation code here + return true; + } + + + validate(source, data = {}, args = {}) { + let errors = []; + let validated = []; + + try { + errors = this.preValidate(source, data, args); + if (!this.doValidation(source, data, args)) { + throw new ValidationException([], this.getMessage(source, args)); + } + } catch (e) { + if (!(e instanceof ValidationException)) { + this.logger().info('Error happened while validating: ', e.message || e || '[no message]'); + this.logger().error(e.stack || '[no stack]'); + } + + if (e.suppressedErrors && !_.isArray(e.suppressedErrors)) { + validated = e.suppressedErrors; + } else { + validated = [e]; + } + } + + errors = _.uniq( + _.map( + errors.concat(validated), + (e) => e.message || this.getMessage(source, args))); + + if (!_.isEmpty(errors)) { + throw new ValidationException(errors, this.getMessage(source, args)); + } + + return true; + } + + + getMessage(value, args) { + return `Validation error for value "${value}"`; + } + + + getDependencies() { + return this.dependencies; + } + + + getValidators() { + return this._validators; + } + +} diff --git a/server/core/constants/index.js b/server/core/constants/index.js new file mode 100644 index 0000000..391beb6 --- /dev/null +++ b/server/core/constants/index.js @@ -0,0 +1,74 @@ +'use strict'; + + +import _ from 'lodash'; +import PagerSortDirections from '@/core/enumerations/PagerSortDirections'; +import Environments from '@/core/enumerations/Environments'; +import HttpMethods from '@/core/enumerations/HttpMethods'; + +import Constants from './paths'; + + +_.each(Constants, (value, key) => { + module.exports[key] = value; +}); + + +export const DEFAULT_ENVIRONMENT :string = process.env.NODE_ENV || Environments.DEVELOPMENT; + +export const DEFAULT_WEBPACK_DEV_SERVER_PORT :number = 3000; +export const DEFAULT_WEBPACK_HOT_MIDDLEWARE_SCRIPT :string = + 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true'; + +export const DEFAULT_HOST :string = '127.0.0.1'; +export const DEFAULT_DISPLAY_HOST :string = 'localhost'; +export const DEFAULT_PORT :number = 3000; +export const DEFAULT_PROTOCOL :string = 'http'; + +export const DEFAULT_ROUTER_PREFIX :string = '/'; +export const DEFAULT_SUPPORTED_METHODS :Array = _.values(HttpMethods); +export const EMPTY_ROUTE_CONFIGURATION :Object = {}; +export const EMPTY_ROUTING_PREFIX :string = ''; +export const DEFAULT_ROUTING_REGEX_PREFIX :string = 'regex:'; +export const EMPTY_REGEX_DELIMITERS :string = ''; +export const DEFAULT_ROUTING_REGEX_DELIMITER: RegExp = /(^\/|\/$)/ig; +export const DEFAULT_ROUTE_METHOD_DELIMITER: RegExp = /(\s|\t)/ig; +export const DEFAULT_ROUTE_CLEANUP_PATTERN: RegExp = /(^\[|\]$)/ig; +export const DEFAULT_ROUTE_CLEANUP_REPLACE :string = ''; +export const DEFAULT_HTTP_METHOD :string = HttpMethods.GET; +export const DEFAULT_CONTROLLER_METHOD_DELIMITER :string = '.'; +export const DEFAULT_ROUTING_AUTH_ROUTE_REGEXP :RegExp = /^\@auth\(.+?\)$/ig; +export const DEFAULT_ROUTING_AUTH_REPLACE_PATTERN :RegExp = /(^\@[a-z]+?\(|\)$)/ig; +export const DEFAULT_ROUTING_AUTH_EMPTY_PATTERN :string = ''; + +export const DEFAULT_MODEL_HOST :string = 'localhost'; +export const DEFAULT_MOCK_HOST :string = 'localhost'; + +export const DEFAULT_PAGE :number = 0; +export const DEFAULT_ITEMS_PER_PAGE_AMOUNT :number = 20; +export const DEFAULT_SORT_DIRECTION :string = PagerSortDirections.ASC; +export const DEFAULT_SORT_FIELDS :Array = ['id']; + +export const DEFAULT_RELEASE_VERSION :string = 'latest'; +export const RELEASE_TIMESTAMP :number = new Date().getTime(); + +export const DEFAULT_COOKIE_SECRET :string = 'verysecretcookie'; + +export const DEFAULT_SESSION_SECRET :string = DEFAULT_COOKIE_SECRET; +export const DEFAULT_SESSION_COOKIE_DOMAIN :string = `${DEFAULT_HOST}:${DEFAULT_PORT}`; +export const DEFAULT_SESSION_COOKIE_LIFETIME :number = 365 * 24 * 60 * 60 * 1000; + +export const DEFAULT_REDIS_HOST :string = '127.0.0.1'; +export const DEFAULT_REDIS_PORT :number = 6379; +export const DEFAULT_REDIS_IP_FAMILY :number = 4; +export const DEFAULT_REDIS_DB :number = 0; +export const DEFAULT_REDIS_TTL :number = 250; + +export const DEFAULT_ERROR_HTTP_STATUS :number = 400; +export const HTTP_STATUS_NOT_FOUND :number = 404; +export const HTTP_STATUS_FORBIDDEN :number = 403; +export const HTTP_STATUS_SERVER_ERROR :number = 500; +export const HTTP_STATUS_CONFLICT :number = 409; +export const HTTP_STATUS_SERVICE_UNAVAILABLE :number = 503; + +export const EXIT_STATUS_INITIALIZATION_FAILED = 1; diff --git a/server/core/constants/paths.js b/server/core/constants/paths.js new file mode 100644 index 0000000..229e794 --- /dev/null +++ b/server/core/constants/paths.js @@ -0,0 +1,80 @@ +'use strict'; + + +var _ = require('lodash'); +var Path = require('path'); + + +exports.PROJECT_ROOT = process.cwd(); +exports.BASE_DIR = Path.resolve(exports.PROJECT_ROOT, './server'); + +exports.DEFAULT_GENERATOR_DIR = Path.resolve(exports.PROJECT_ROOT, './generator'); +exports.DEFAULT_GENERATOR_TEMPLATES_DIR = Path.resolve(exports.DEFAULT_GENERATOR_DIR, './templates'); + +exports.DEFAULT_MOCK_DIR = Path.resolve(exports.PROJECT_ROOT, './mock'); +exports.DEFAULT_MOCK_TEMPLATES_DIR = Path.resolve(exports.DEFAULT_MOCK_DIR, './templates'); +exports.DEFAULT_MOCK_ASSETS_DIR = Path.resolve(exports.DEFAULT_MOCK_DIR, './assets'); +exports.DEFAULT_MOCK_IMAGES_DIR = Path.resolve(exports.DEFAULT_MOCK_ASSETS_DIR, './images'); +exports.DEFAULT_MOCK_IMAGES_SAMPLES_DIR = Path.resolve(exports.DEFAULT_MOCK_IMAGES_DIR, './samples'); +exports.DEFAULT_MOCK_IMAGES_UPLOAD_DIR = Path.resolve(exports.DEFAULT_MOCK_IMAGES_DIR, './upload'); + +exports.DEFAULT_CONFIG_PATH = Path.resolve(exports.BASE_DIR, './core/skeleton-config'); +exports.DEFAULT_ENVIRONMENT = process.env.NODE_ENV || 'development'; + +exports.DEFAULT_RUNTIME_DIR = Path.resolve(exports.BASE_DIR, './runtime'); +exports.DEFAULT_LOGS_DIR = Path.resolve(exports.DEFAULT_RUNTIME_DIR, './logs'); + +exports.DEFAULT_SERVER_APP_DIR = exports.BASE_DIR; +exports.DEFAULT_MODELS_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './models'); +exports.DEFAULT_CONTROLLERS_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './controllers'); +exports.DEFAULT_SERVICES_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './services'); +exports.DEFAULT_ENUMERATIONS_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './enumerations'); +exports.DEFAULT_MIDDLEWARES_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './middlewares'); +exports.DEFAULT_MIDDLEWARES_BEFORE_DIR = Path.resolve(exports.DEFAULT_MIDDLEWARES_DIR, './before'); +exports.DEFAULT_MIDDLEWARES_AFTER_DIR = Path.resolve(exports.DEFAULT_MIDDLEWARES_DIR, './after'); +exports.DEFAULT_VALIDATIONS_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './validations'); +exports.DEFAULT_VALIDATORS_DIR = Path.resolve(exports.DEFAULT_VALIDATIONS_DIR, './validators'); +exports.DEFAULT_VALIDATION_SCHEMAS_DIR = Path.resolve(exports.DEFAULT_VALIDATIONS_DIR, './schemas'); +exports.DEFAULT_TESTS_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './tests'); + +exports.DEFAULT_CLIENT_APP_DIR = Path.resolve(exports.PROJECT_ROOT, './app'); +exports.DEFAULT_NODE_MODULES_DIR = Path.resolve(exports.PROJECT_ROOT, './node_modules'); +exports.DEFAULT_CLIENT_ENTRY_POINT_PATH = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './client'); +exports.DEFAULT_CLIENT_APP_IMAGES_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './images'); +exports.DEFAULT_CLIENT_APP_SPRITES_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './sprites'); +exports.DEFAULT_CLIENT_APP_FONTS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './fonts'); +exports.DEFAULT_CLIENT_APP_SCSS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR); +exports.DEFAULT_CLIENT_APP_CSS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR); +exports.DEFAULT_CLIENT_APP_LESS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR); +exports.DEFAULT_CLIENT_APP_COMPONENTS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './components'); +exports.DEFAULT_CLIENT_APP_SERVICE_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './services'); +exports.DEFAULT_CLIENT_APP_ACTION_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './actions'); +exports.DEFAULT_CLIENT_APP_REDUCER_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_DIR, './reducers'); +exports.DEFAULT_CLIENT_APP_CONTROL_COMPONENT_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_COMPONENTS_DIR, './controls'); +exports.DEFAULT_CLIENT_APP_FORM_COMPONENT_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_CONTROL_COMPONENT_DIR, './forms'); +exports.DEFAULT_CLIENT_APP_ITEM_COMPONENT_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_CONTROL_COMPONENT_DIR, './items'); +exports.DEFAULT_CLIENT_APP_PAGE_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_COMPONENTS_DIR, './pages'); +exports.DEFAULT_CLIENT_APP_DEVELOPMENT_PAGE_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_PAGE_DIR, './development'); +exports.DEFAULT_CLIENT_APP_REDUX_CONNECTORS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_COMPONENTS_DIR, './connected'); +exports.DEFAULT_CLIENT_APP_REDUX_PAGE_CONNECTORS_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_REDUX_CONNECTORS_DIR, './pages'); +exports.DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR = Path.resolve(exports.DEFAULT_CLIENT_APP_COMPONENTS_DIR, './_styles/sprites/'); + +exports.DEFAULT_CONFIGURATION_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './configuration'); +exports.DEFAULT_MAIN_CONFIGURATION_PATH = Path.resolve(exports.DEFAULT_CONFIGURATION_DIR, './config'); +exports.DEFAULT_TARGET_SPECIFIC_CONFIGURATION_PATH = Path.resolve(exports.DEFAULT_CONFIGURATION_DIR, './target'); +exports.DEFAULT_ENVIRONMENT_CONFIGURATION_PATH = Path.resolve(exports.DEFAULT_CONFIGURATION_DIR, './' + exports.DEFAULT_ENVIRONMENT); +exports.DEFAULT_LOCAL_CONFIGURATION_PATH = Path.resolve(exports.DEFAULT_CONFIGURATION_DIR, './local'); + +exports.DEFAULT_STATIC_FILES_DIR = Path.resolve(exports.PROJECT_ROOT, './public'); + +exports.DEFAULT_ASSETS_DIR = Path.resolve(exports.DEFAULT_STATIC_FILES_DIR, './assets'); +exports.DEFAULT_UPLOAD_DIR = Path.resolve(exports.DEFAULT_STATIC_FILES_DIR, './upload'); +exports.DEFAULT_RELATIVE_UPLOAD_DIR = '/upload'; +exports.DEFAULT_VIEWS_COMMON_DIR = Path.resolve(exports.DEFAULT_SERVER_APP_DIR, './templates'); +exports.DEFAULT_VIEWS_DIR = Path.resolve(exports.DEFAULT_VIEWS_COMMON_DIR, './views'); +exports.DEFAULT_EMAIL_TEMPLATES_DIR = Path.resolve(exports.DEFAULT_VIEWS_COMMON_DIR, './emails'); + +exports.DEFAULT_WEBPACK_CONFIG_DIR = Path.resolve(exports.PROJECT_ROOT, './webpack'); +exports.DEFAULT_WEBPACK_DEVELOPMENT_CONFIG_PATH = Path.resolve(exports.DEFAULT_WEBPACK_CONFIG_DIR, './webpack.config.' + exports.DEFAULT_ENVIRONMENT); + +exports.DEFAULT_TESTS_OUTPUT_FILE_PATH = Path.resolve(exports.DEFAULT_LOGS_DIR, './test-results.xml'); diff --git a/server/core/enumerations/.gitkeep b/server/core/enumerations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/core/enumerations/ContentTypes.js b/server/core/enumerations/ContentTypes.js new file mode 100644 index 0000000..76d0ad4 --- /dev/null +++ b/server/core/enumerations/ContentTypes.js @@ -0,0 +1,11 @@ +'use strict'; + + +class ContentTypes { + static JSON :string = 'application/json'; + static MULTIPART :string = 'multipart/form-data'; + static URLENCODED :string = 'application/x-www-form-urlencoded'; +} + + +export default ContentTypes; diff --git a/server/core/enumerations/Environments.js b/server/core/enumerations/Environments.js new file mode 100644 index 0000000..d2d38ee --- /dev/null +++ b/server/core/enumerations/Environments.js @@ -0,0 +1,10 @@ +'use strict'; + + +class Environments { + static DEVELOPMENT :string = 'development'; + static PRODUCTION :string = 'production'; +} + + +export default Environments; diff --git a/server/core/enumerations/HttpMethods.js b/server/core/enumerations/HttpMethods.js new file mode 100644 index 0000000..c7fc561 --- /dev/null +++ b/server/core/enumerations/HttpMethods.js @@ -0,0 +1,12 @@ +'use strict'; + + +class HttpMethods { + static GET :string = 'GET'; + static POST :string = 'POST'; + static PUT :string = 'PUT'; + static DELETE :string = 'DELETE'; +} + + +export default HttpMethods; diff --git a/server/core/enumerations/PagerSortDirections.js b/server/core/enumerations/PagerSortDirections.js new file mode 100644 index 0000000..3931854 --- /dev/null +++ b/server/core/enumerations/PagerSortDirections.js @@ -0,0 +1,10 @@ +'use strict'; + + +class SortDirections { + static ASC :string = 'ASC'; + static DESC :string = 'DESC'; +} + + +export default SortDirections; diff --git a/server/core/enumerations/ResponseTypes.js b/server/core/enumerations/ResponseTypes.js new file mode 100644 index 0000000..debc43e --- /dev/null +++ b/server/core/enumerations/ResponseTypes.js @@ -0,0 +1,10 @@ +'use strict'; + + +class ResponseTypes { + static JSON :string = 'application/json'; + static BINARY :string = 'binary'; +} + + +export default ResponseTypes; diff --git a/server/core/enumerations/ServerParameters.js b/server/core/enumerations/ServerParameters.js new file mode 100644 index 0000000..53a58e2 --- /dev/null +++ b/server/core/enumerations/ServerParameters.js @@ -0,0 +1,12 @@ +'use strict'; + + +class ServerParameters { + static PORT :string = 'port'; + static VIEW_CACHE :string = 'view cache'; + static VIEW_ENGINE :string = 'view engine'; + static VIEWS_DIR :string = 'views'; +} + + +export default ServerParameters; diff --git a/server/core/exceptions/.gitkeep b/server/core/exceptions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/core/exceptions/ComponentAutowireException.js b/server/core/exceptions/ComponentAutowireException.js new file mode 100644 index 0000000..5fd0f60 --- /dev/null +++ b/server/core/exceptions/ComponentAutowireException.js @@ -0,0 +1,23 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class ComponentAutowireException extends Exception { + + constructor( + autowireItem = '[unknown]', + autowireAs = '[unknown]', + autowireWhere = '[unknown]' + ) :ComponentAutowireException { + super('Cannot autowire instance of type "{autowireItem}" as {autowireWhere}.{autowireAs}', + {autowireItem, autowireAs, autowireWhere}); + + return this; + } + +} + + +export default ComponentAutowireException; diff --git a/server/core/exceptions/ComponentConflictException.js b/server/core/exceptions/ComponentConflictException.js new file mode 100644 index 0000000..6413ce9 --- /dev/null +++ b/server/core/exceptions/ComponentConflictException.js @@ -0,0 +1,27 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class ComponentConflictException extends Exception { + + constructor( + componentId :string = '[no componentId]', + componentInstance :string = '[no componentInstance]', + existingComponentInstance :string = '[no existing componentInstance]' + ) :ComponentConflictException { + super('Cannot add component with id "{componentId}" of type {newType} because this id is bound ' + + 'by another instance of type "{existingType}"', { + componentId, + newType: componentInstance.constructor.name, + existingType: existingComponentInstance.constructor.name + }); + + return this; + } + +} + + +export default ComponentConflictException; diff --git a/server/core/exceptions/ComponentNotFoundException.js b/server/core/exceptions/ComponentNotFoundException.js new file mode 100644 index 0000000..4aea36e --- /dev/null +++ b/server/core/exceptions/ComponentNotFoundException.js @@ -0,0 +1,25 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class ComponentNotFoundException extends Exception { + + constructor( + componentId, + message + ) :ComponentNotFoundException { + let exceptionMessage = 'Cannot find required component "{componentId}"'; + if (message) { + exceptionMessage += `: ${message}`; + } + super(exceptionMessage, {componentId}); + + return this; + } + +} + + +export default ComponentNotFoundException; diff --git a/server/core/exceptions/CoreException.js b/server/core/exceptions/CoreException.js new file mode 100644 index 0000000..baff66b --- /dev/null +++ b/server/core/exceptions/CoreException.js @@ -0,0 +1,13 @@ +'use strict'; + + +import Exception from '@/core/base/Exception'; +import {DEFAULT_ERROR_HTTP_STATUS} from '@/core/constants'; + + +class CoreException extends Exception { + httpStatus = DEFAULT_ERROR_HTTP_STATUS; +} + + +export default CoreException; diff --git a/server/core/exceptions/CsrfViolationException.js b/server/core/exceptions/CsrfViolationException.js new file mode 100644 index 0000000..2ae5901 --- /dev/null +++ b/server/core/exceptions/CsrfViolationException.js @@ -0,0 +1,18 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class CsrfViolationException extends Exception { + + constructor(token = '[no csrf token]') :CsrfViolationException { + super('Invalid CSRF token "{token}" has been passed', {token}); + + return this; + } + +} + + +export default CsrfViolationException; diff --git a/server/core/exceptions/HttpForbiddenException.js b/server/core/exceptions/HttpForbiddenException.js new file mode 100644 index 0000000..680789b --- /dev/null +++ b/server/core/exceptions/HttpForbiddenException.js @@ -0,0 +1,13 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; +import {HTTP_STATUS_FORBIDDEN} from '@/core/constants'; + + +class HttpForbiddenException extends Exception { + httpStatus = HTTP_STATUS_FORBIDDEN; +} + + +export default HttpForbiddenException; diff --git a/server/core/exceptions/HttpNotFoundException.js b/server/core/exceptions/HttpNotFoundException.js new file mode 100644 index 0000000..56cbf4f --- /dev/null +++ b/server/core/exceptions/HttpNotFoundException.js @@ -0,0 +1,13 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; +import {HTTP_STATUS_NOT_FOUND} from '@/core/constants'; + + +class HttpNotFoundException extends Exception { + httpStatus = HTTP_STATUS_NOT_FOUND; +} + + +export default HttpNotFoundException; diff --git a/server/core/exceptions/HttpServerErrorException.js b/server/core/exceptions/HttpServerErrorException.js new file mode 100644 index 0000000..cb2bd99 --- /dev/null +++ b/server/core/exceptions/HttpServerErrorException.js @@ -0,0 +1,13 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; +import {HTTP_STATUS_SERVER_ERROR} from '@/core/constants'; + + +class HttpServerErrorException extends Exception { + httpStatus = HTTP_STATUS_SERVER_ERROR; +} + + +export default HttpServerErrorException; diff --git a/server/core/exceptions/ModelRequestException.js b/server/core/exceptions/ModelRequestException.js new file mode 100644 index 0000000..bed181e --- /dev/null +++ b/server/core/exceptions/ModelRequestException.js @@ -0,0 +1,25 @@ +'use strict'; + + +import Exception from '@/core/base/Exception'; +import {DEFAULT_ERROR_HTTP_STATUS} from '@/core/constants'; + + +class ModelRequestException extends Exception { + + httpStatus = DEFAULT_ERROR_HTTP_STATUS; + + + constructor(readyState, error) { + super(`Request failed, response message = "${error.message}"`, readyState); + + this.httpStatus = readyState.status; + this.errorType = error.type; + this.readyState = readyState; + this.originalMessage = error.message; + } + +} + + +export default ModelRequestException; diff --git a/server/core/exceptions/NotImplementedMethodException.js b/server/core/exceptions/NotImplementedMethodException.js new file mode 100644 index 0000000..0a6d8e6 --- /dev/null +++ b/server/core/exceptions/NotImplementedMethodException.js @@ -0,0 +1,22 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; +import {HTTP_STATUS_NOT_FOUND} from '@/core/constants'; + + +class NotImplementedMethodException extends Exception { + + httpStatus = HTTP_STATUS_NOT_FOUND; + + + constructor(className = '[Unknown Class]', method) :NotImplementedMethodException { + super('"{className}" class "{method}" method is not implemented', {className, method}); + + return this; + } + +} + + +export default NotImplementedMethodException; diff --git a/server/core/exceptions/SchemaValidationException.js b/server/core/exceptions/SchemaValidationException.js new file mode 100644 index 0000000..3391751 --- /dev/null +++ b/server/core/exceptions/SchemaValidationException.js @@ -0,0 +1,20 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class SchemaValidationException extends Exception { + + constructor(message, errors) :SchemaValidationException { + super(message); + + this.errors = errors; + + return this; + } + +} + + +export default SchemaValidationException; diff --git a/server/core/exceptions/SessionException.js b/server/core/exceptions/SessionException.js new file mode 100644 index 0000000..14d9b8e --- /dev/null +++ b/server/core/exceptions/SessionException.js @@ -0,0 +1,18 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class SessionException extends Exception { + + constructor(sessionId = '[no session id]') :SessionException { + super('Cannot perform session operation with session id "{sessionId}"', {sessionId}); + + return this; + } + +} + + +export default SessionException; diff --git a/server/core/exceptions/SessionExpiredException.js b/server/core/exceptions/SessionExpiredException.js new file mode 100644 index 0000000..2b8362a --- /dev/null +++ b/server/core/exceptions/SessionExpiredException.js @@ -0,0 +1,21 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +export default class SessionExpiredException extends Exception { + + constructor(reason = null) :SessionExpiredException { + let message = 'Session expired'; + + if (reason) { + message = `${message}: ${reason}`; + } + + super(message); + + return this; + } + +} diff --git a/server/core/exceptions/SessionSerializationException.js b/server/core/exceptions/SessionSerializationException.js new file mode 100644 index 0000000..7dd12e3 --- /dev/null +++ b/server/core/exceptions/SessionSerializationException.js @@ -0,0 +1,18 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class SessionSerializationException extends Exception { + + constructor(userId = '[no user id]') :SessionSerializationException { + super('Cannot serialize or deserialize user with id "{userId}"', {userId}); + + return this; + } + +} + + +export default SessionSerializationException; diff --git a/server/core/exceptions/UnsupportedRequestOptionException.js b/server/core/exceptions/UnsupportedRequestOptionException.js new file mode 100644 index 0000000..e549e5a --- /dev/null +++ b/server/core/exceptions/UnsupportedRequestOptionException.js @@ -0,0 +1,20 @@ +'use strict'; + + +import _ from 'lodash'; +import Exception from '@/core/exceptions/CoreException'; + + +export default class UnsupportedRequestOptionException extends Exception { + + constructor(optionValue = '[no option provided]', optionName = '[unknown option name]') { + super(`Remote request option "${optionName}" = "` + + _.isPlainObject(optionValue) || _.isArray(optionValue) + ? JSON.stringify(optionValue) + : optionValue + + ' is not supported'); + + return this; + } + +} diff --git a/server/core/exceptions/UnsupportedViewEngineException.js b/server/core/exceptions/UnsupportedViewEngineException.js new file mode 100644 index 0000000..6ee3ef9 --- /dev/null +++ b/server/core/exceptions/UnsupportedViewEngineException.js @@ -0,0 +1,15 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +export default class UnsupportedViewEngineException extends Exception { + + constructor(engineName = '[unknown engine name]') :UnsupportedViewEngineException { + super('View engine "{engineName}" is not supported', {engineName}); + + return this; + } + +} diff --git a/server/core/exceptions/ValidationException.js b/server/core/exceptions/ValidationException.js new file mode 100644 index 0000000..a9a537c --- /dev/null +++ b/server/core/exceptions/ValidationException.js @@ -0,0 +1,20 @@ +'use strict'; + + +import Exception from '@/core/exceptions/CoreException'; + + +class ValidationException extends Exception { + + constructor(suppressedFields, message) :ValidationException { + super(message); + + this.suppressedErrors = suppressedFields; + + return this; + } + +} + + +export default ValidationException; diff --git a/server/core/skeleton-config.js b/server/core/skeleton-config.js new file mode 100644 index 0000000..cc66604 --- /dev/null +++ b/server/core/skeleton-config.js @@ -0,0 +1,264 @@ +import Minimist from 'minimist'; +import _ from 'lodash'; +const Constants = require('./constants'); +import Pug from 'pug'; +import UUID from 'uuid'; +import configs from '../configuration'; + +export default (environment) => { + let Logger = console; + let cmdArgs = Minimist(process.argv.slice(2)); + + let config = { + constants: Constants, + paths: { + directories: { + BASE_DIR: Constants.BASE_DIR, + MODELS: Constants.DEFAULT_MODELS_DIR, + CONTROLLERS: Constants.DEFAULT_CONTROLLERS_DIR, + SERVICES: Constants.DEFAULT_SERVICES_DIR, + VALIDATIONS: Constants.DEFAULT_VALIDATION_SCHEMAS_DIR, + VALIDATORS: Constants.DEFAULT_VALIDATORS_DIR, + CONVERTERS: Constants.DEFAULT_CONVERTERS_DIR, + MIDDLEWARES: Constants.DEFAULT_MIDDLEWARES_DIR, + MIDDLEWARES_BEFORE_REQUEST: Constants.DEFAULT_MIDDLEWARES_BEFORE_DIR, + MIDDLEWARES_AFTER_REQUEST: Constants.DEFAULT_MIDDLEWARES_AFTER_DIR, + CONFIGURATION: Constants.DEFAULT_CONFIGURATION_DIR, + STATIC_FILES: Constants.DEFAULT_STATIC_FILES_DIR, + VIEWS: Constants.DEFAULT_VIEWS_DIR, + UPLOADED_FILES: Constants.DEFAULT_UPLOAD_DIR, + EMAIL_TEMPLATES: Constants.DEFAULT_EMAIL_TEMPLATES_DIR, + TESTS: Constants.DEFAULT_TESTS_DIR + }, + files: { + MAIN_CONFIGURATION: Constants.DEFAULT_MAIN_CONFIGURATION_PATH, + TARGET_SPECIFIC_CONFIGURATION: Constants.DEFAULT_TARGET_SPECIFIC_CONFIGURATION_PATH, + ENVIRONMENT_CONFIGURATION: Constants.DEFAULT_ENVIRONMENT_CONFIGURATION_PATH, + LOCAL_CONFIGURATION: Constants.DEFAULT_LOCAL_CONFIGURATION_PATH, + WEBPACK_CONFIGURATION: Constants.DEFAULT_WEBPACK_DEVELOPMENT_CONFIG_PATH, + MOCHA_OUTPUT: Constants.DEFAULT_TESTS_OUTPUT_FILE_PATH + }, + web: { + UPLOADED_FILES: Constants.DEFAULT_RELATIVE_UPLOAD_DIR + } + }, + options: { + release: { + version: Constants.DEFAULT_RELEASE_VERSION, + timestamp: Constants.RELEASE_TIMESTAMP + }, + cookies: { + secret: Constants.DEFAULT_COOKIE_SECRET, + options: {} + }, + server: { + environment, + host: Constants.DEFAULT_HOST, + displayHost: Constants.DEFAULT_DISPLAY_HOST, + protocol: Constants.DEFAULT_PROTOCOL, + port: Constants.DEFAULT_PORT, + showPort: true + }, + webApp: {}, + helmet: {}, + csrf: { + genid: () => { + return UUID.v4(); + }, + keyTtl: 62000, + excudeMethods: ['GET', 'HEAD', 'OPTIONS'], + path: '*', + headerKey: 'x-crsf-token', + sessionKey: 'csrfSecret' + }, + views: { + engineName: 'pug', + engine: Pug.__express, + cached: false + }, + session: { + genid: () => { + return UUID.v4(); + }, + proxy: true, + name: 'sessionId', + resave: true, + saveUninitialized: true, + cookie: { + path: '/', + httpOnly: false, + secure: false, + // domain: Constants.DEFAULT_SESSION_COOKIE_DOMAIN, + maxAge: Constants.DEFAULT_SESSION_COOKIE_LIFETIME + }, + redisOptions: { + prefix: 'session:', + host: Constants.DEFAULT_REDIS_HOST, + port: Constants.DEFAULT_REDIS_PORT, + ttl: Constants.DEFAULT_REDIS_TTL, + clientOptions: { + host: Constants.DEFAULT_REDIS_HOST, + port: Constants.DEFAULT_REDIS_PORT, + family: Constants.DEFAULT_REDIS_IP_FAMILY, + db: Constants.DEFAULT_REDIS_DB + } + }, + secret: Constants.DEFAULT_SESSION_SECRET + }, + integrations: {}, + logger: Logger, + routing: { + apiPrefix: Constants.DEFAULT_ROUTER_PREFIX, + routes: Constants.EMPTY_ROUTE_CONFIGURATION, + supportedMethods: Constants.DEFAULT_SUPPORTED_METHODS, + regexPrefix: Constants.DEFAULT_ROUTING_REGEX_PREFIX, + regexDelimiters: Constants.DEFAULT_ROUTING_REGEX_DELIMITER, + controllerMethodDelimiter: Constants.DEFAULT_CONTROLLER_METHOD_DELIMITER, + routeMethodDelimiter: Constants.DEFAULT_ROUTE_METHOD_DELIMITER, + routeMethodCleanupPattern: Constants.DEFAULT_ROUTE_CLEANUP_PATTERN, + routeMethodCleanupReplace: Constants.DEFAULT_ROUTE_CLEANUP_REPLACE, + defaultHttpMethod: Constants.DEFAULT_HTTP_METHOD + }, + middlewares: { + before: [], + after: [] + }, + tests: { + timeout: 60000, + reporting: { + name: 'API Tests', + log: Constants.DEFAULT_TESTS_OUTPUT_FILE_PATH, + reporter: 'mocha-jenkins-reporter' + } + }, + email: { + apiKey: null, + defaultFrom: 'noreply@your-domain.com', + compilerEngine: 'pug', + compilerOptions: { + doctype: 'html', + pretty: true, + self: false, + debug: true, + compileDebug: true, + cache: false + }, + aliases: {} + }, + services: {}, + models: { + url: Constants.DEFAULT_MODEL_HOST, + forceMessageRewrite: false, + maskCharacter: '*', + options: { + contentType: 'application/json' + } + }, + mockModels: { + url: Constants.DEFAULT_MOCK_HOST, + forceMessageRewrite: true + }, + fileUpload: {}, + bodyParser: { + urlencoded: { + limit: '10mb', + extended: true + }, + json: { + limit: '10mb' + } + }, + logRequestParams: { + maxBodySize: 2000 + }, + webpack: { + compiler: {}, + options: {} + } + } + }; + + Logger.log(`Default configuration found by path "${__filename}`); + + config.healthcheck = [ + config.paths.directories.BASE_DIR, + config.paths.directories.CONFIGURATION, + config.paths.directories.CONTROLLERS, + config.paths.directories.MIDDLEWARES, + config.paths.directories.MODELS, + config.paths.directories.SERVICES, + config.paths.directories.UPLOADED_FILES, + config.paths.directories.VIEWS, + config.paths.directories.EMAIL_TEMPLATES + ]; + + let mainConfig; + let targetSpecificConfig; + let envConfig; + let localConfig; + + let mainConfigPath = config.paths.files.MAIN_CONFIGURATION; + let targetSpecificConfigPath = config.paths.files.TARGET_SPECIFIC_CONFIGURATION; + let envConfigPath = config.paths.files.ENVIRONMENT_CONFIGURATION; + let localConfigPath = config.paths.files.LOCAL_CONFIGURATION; + + try { + mainConfig = configs.config; + if (typeof mainConfig === 'function') { + mainConfig = mainConfig(config, cmdArgs); + } + Logger.info(`User-defined configuration has been found by the path "${mainConfigPath}"...`); + } catch (e) { + Logger.info(`No user-defined configuration with the path "${mainConfigPath}" found, skipping...`); + Logger.error(e.stack); + mainConfig = {}; + } + + try { + targetSpecificConfig = require('../configuration/target'); + if (typeof targetSpecificConfig === 'function') { + targetSpecificConfig = targetSpecificConfig(config, cmdArgs); + } + Logger.info(`Target-specific configuration has been found by the path "${targetSpecificConfigPath}"...`); + } catch (e) { + Logger.info(`No target-specific configuration with the path "${targetSpecificConfigPath}" found, skipping...`); + Logger.error(e.stack); + targetSpecificConfig = {}; + } + + try { + envConfig = configs[environment]; + if (typeof envConfig === 'function') { + envConfig = envConfig(config, cmdArgs); + } + Logger.info(`Environment-based configuration has been found by the path "${envConfigPath}"...`); + } catch (e) { + Logger.info(`No environment-based configuration with the path "${envConfigPath}" found, skipping...`); + Logger.error(e.stack); + envConfig = {}; + } + + try { + localConfig = require('../configuration/local'); + if (typeof localConfig === 'function') { + localConfig = localConfig(config, cmdArgs); + } + Logger.info(`Local configuration has been found by the path "${localConfigPath}"...`); + } catch (e) { + Logger.info(`No local configuration with the path "${localConfigPath}" found, skipping...`); + Logger.error(e.stack); + localConfig = {}; + } + + const merged = _.merge( + {}, + config, + {options: mainConfig}, + {options: targetSpecificConfig}, + {options: envConfig}, + {options: localConfig}, + {options: {server: cmdArgs}} + ); + + return merged; +}; diff --git a/server/enumerations/.gitkeep b/server/enumerations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/exceptions/.gitkeep b/server/exceptions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..2e5c6da --- /dev/null +++ b/server/index.js @@ -0,0 +1,9 @@ +export middlewares from './middlewares'; + +import controllers from './controllers'; +import services from './services'; + +export { + controllers, + services +}; diff --git a/server/middlewares/.gitkeep b/server/middlewares/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/middlewares/after/.gitkeep b/server/middlewares/after/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/middlewares/after/index.js b/server/middlewares/after/index.js new file mode 100644 index 0000000..dd9186f --- /dev/null +++ b/server/middlewares/after/index.js @@ -0,0 +1,5 @@ +import requestException from './requestException'; + +export default { + requestException +}; diff --git a/server/middlewares/after/requestException.js b/server/middlewares/after/requestException.js new file mode 100644 index 0000000..fcee7af --- /dev/null +++ b/server/middlewares/after/requestException.js @@ -0,0 +1,40 @@ +'use strict'; + + +import _ from 'lodash'; +import {DEFAULT_ERROR_HTTP_STATUS} from '@/core/constants'; +import Environments from '@/core/enumerations/Environments'; + + +export default (app) => { + app.logger().info('Applying forced json response middleware...'); + + return (error, request, response, next :Function) => { + let e = error || { + message: '[no message]', + stack: '[no stack]', + errors: [], + constructor: { + name: 'UnknownException' + } + }; + + app.logger().info('Exception thrown to response: ', e.message || e); + app.logger().error(e.stack || '[no stack'); + + let responseData = { + status: 'error', + details: e.message, + type: e.errorType || e.constructor.name, + errors: e.errors + }; + + if (app.environment() !== Environments.PRODUCTION) { + responseData.trace = e.stack; + } + + response + .status(error.httpStatus || DEFAULT_ERROR_HTTP_STATUS) + .sendJson(responseData); + }; +}; diff --git a/server/middlewares/before/.gitkeep b/server/middlewares/before/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/middlewares/before/appToRequest.js b/server/middlewares/before/appToRequest.js new file mode 100644 index 0000000..777ab6f --- /dev/null +++ b/server/middlewares/before/appToRequest.js @@ -0,0 +1,16 @@ +'use strict'; + + +import _ from 'lodash'; + + +export default (app) => { + app.logger().info('Applying app to request.palo mapper middleware...'); + + return (request, response, next :Function) => { + request.palo = app; + response.palo = app; + + next(); + }; +}; diff --git a/server/middlewares/before/cookieParser.js b/server/middlewares/before/cookieParser.js new file mode 100644 index 0000000..b119af5 --- /dev/null +++ b/server/middlewares/before/cookieParser.js @@ -0,0 +1,15 @@ +'use strict'; + + +import ExpressCookieParser from 'cookie-parser'; + + +module.exports = { + register(app) { + app.logger().info('Applying cookie parsing middleware...'); + + let config = app.config().options.cookies; + + return app.server().use(ExpressCookieParser(config.secret, config.options)); + } +}; diff --git a/server/middlewares/before/index.js b/server/middlewares/before/index.js new file mode 100644 index 0000000..49c8b9e --- /dev/null +++ b/server/middlewares/before/index.js @@ -0,0 +1,25 @@ +import appToRequest from './appToRequest'; +import cookieParser from './cookieParser'; +import jsonRequest from './jsonRequest'; +import logRequestParams from './logRequestParams'; +import pureSend from './pureSend'; +import requestTime from './requestTime'; +import safeRequest from './safeRequest'; +import serveStatic from './serveStatic'; +import session from './session'; +import webpackDev from './webpackDev'; +import webpackHot from './webpackHot'; + +export default { + appToRequest, + cookieParser, + jsonRequest, + logRequestParams, + pureSend, + requestTime, + safeRequest, + serveStatic, + session, + webpackDev, + webpackHot +}; diff --git a/server/middlewares/before/jsonRequest.js b/server/middlewares/before/jsonRequest.js new file mode 100644 index 0000000..07537b4 --- /dev/null +++ b/server/middlewares/before/jsonRequest.js @@ -0,0 +1,76 @@ +'use strict'; + + +import _ from 'lodash'; +import {DEFAULT_ERROR_HTTP_STATUS} from '@/core/constants'; +import Environments from '@/core/enumerations/Environments'; + + +export default (app) => { + app.logger().info('Applying forced json response middleware...'); + + return (request, response, next :Function) => { + + response.asRendered = function() { + response._asRendered = true; + return response; + }; + + + response.sendingAsRendered = function() { + return response._asRendered; + }; + + + response.cleanRender = function() { + response.asRendered(); + return response.pureRender.apply(response, arguments); + }; + + + response.sendJson = function(data) { + if (!response.sendingAsRendered()) { + try { + app.logger().info('Converting response to JSON...'); + return response.pureSend(JSON.stringify(data)); + } catch (error) { + let e = error || { + message: '[no message]', + stack: '[no stack]', + errors: [], + errorType: 'UnknownException', + constructor: { + name: 'UnknownException' + } + }; + + app.logger().info('Error occurred while trying to send response: ', e.message || '[no message]'); + app.logger().error(e.stack); + + let responseData = { + status: 'error', + details: e.message, + type: e.errorType || e.constructor.name, + errors: e.errors + }; + if (app.environment() !== Environments.PRODUCTION) { + responseData.trace = e.stack; + } + return response + .status(e.httpStatus || DEFAULT_ERROR_HTTP_STATUS) + .pureSend(JSON.stringify(responseData)); + } + } else { + app.logger().info('Sending rendered view...'); + return response.pureSend(data); + } + }; + + + response.send = response.sendJson; + response.render = response.cleanRender; + + + next(); + }; +}; diff --git a/server/middlewares/before/logRequestParams.js b/server/middlewares/before/logRequestParams.js new file mode 100644 index 0000000..26313ea --- /dev/null +++ b/server/middlewares/before/logRequestParams.js @@ -0,0 +1,27 @@ +'use strict'; + + +import _ from 'lodash'; + + +export default (app) => { + app.logger().info('Applying request params logger middleware...'); + + let config = app.config().options.logRequestParams; + + return (request, response, next :Function) => { + let requestBody = JSON.stringify(request.body); + if (requestBody.length > config.maxBodySize) { + requestBody = `${requestBody.substr(0, config.maxBodySize)}...`; + } + + app.logger().info(`Logging request attributes...`); + app.logger().info(`Params: ${JSON.stringify(request.params)}`); + app.logger().info(`Query: ${JSON.stringify(request.query)}`); + app.logger().info(`Cookies: ${JSON.stringify(_.merge({}, request.cookies, request.signedCookies))}`); + app.logger().info(`Body: ${requestBody}`); + app.logger().info(`Headers: ${JSON.stringify(request.headers)}`); + + next(); + }; +}; diff --git a/server/middlewares/before/pureSend.js b/server/middlewares/before/pureSend.js new file mode 100644 index 0000000..344a91b --- /dev/null +++ b/server/middlewares/before/pureSend.js @@ -0,0 +1,22 @@ +'use strict'; + + +export default (app) => { + app.logger().info('Applying original send backup middleware...'); + + return (request, response, next :Function) => { + let send = response.send; + let render = response.render; + + response.pureSend = function() { + return send.apply(response, arguments); + }; + + response.pureRender = function() { + response.send = send; + return render.apply(response, arguments); + }; + + next(); + }; +}; diff --git a/server/middlewares/before/requestTime.js b/server/middlewares/before/requestTime.js new file mode 100644 index 0000000..7d747ff --- /dev/null +++ b/server/middlewares/before/requestTime.js @@ -0,0 +1,29 @@ +'use strict'; + + +export default (app) => { + app.logger().info('Applying request processing time measure middleware...'); + + return (request, response, next :Function) => { + let sendResponse = response.send; + request._startTime = new Date(); + + app.logger().info(`Request processing started at ${request._startTime}`); + + response.timedSend = function(data) { + request._endTime = new Date(); + app.logger().info(`Request processing finished at ${request._endTime}`); + + request._timeElapsed = request._endTime - request._startTime; + app.logger().info(`Request processing elapsed ${request._timeElapsed}ms`); + + return sendResponse.call(response, data); + }; + + + response.send = response.timedSend; + + + next(); + }; +}; diff --git a/server/middlewares/before/safeRequest.js b/server/middlewares/before/safeRequest.js new file mode 100644 index 0000000..c74edc2 --- /dev/null +++ b/server/middlewares/before/safeRequest.js @@ -0,0 +1,46 @@ +'use strict'; + + +export default (app) => { + app.logger().info('Applying request handler async wrapper middleware...'); + + return async (request, response, next :Function) => { + let sendResponse = response.send; + + response.safeSend = function(data) { + try { + app.logger().info('Sending response...'); + return sendResponse.call(response, { + status: 'ok', + data: data + }); + } catch (e) { + app.logger().info('Error occurred while trying to send response: ', e.message || '[no message]'); + app.logger().error(e.stack); + return sendResponse.call(response, { + status: 'error', + details: e.message, + trace: e.stack, + errors: [] + }); + } + }; + + + response.send = response.safeSend; + + + try { + await next(); + } catch (e) { + app.logger().info('Error occurred while trying to process request: ', e.message); + app.logger().error(e.stack); + return sendResponse.call(response, { + status: 'error', + details: e.message, + trace: e.stack, + errors: [] + }); + } + }; +}; diff --git a/server/middlewares/before/serveStatic.js b/server/middlewares/before/serveStatic.js new file mode 100644 index 0000000..90dc675 --- /dev/null +++ b/server/middlewares/before/serveStatic.js @@ -0,0 +1,13 @@ +'use strict'; + + +import Express from 'express'; + + +module.exports = { + register(app) { + app.logger().info('Applying static files serving middleware...'); + + return app.server().use(Express.static(app.coreDirectory('STATIC_FILES'))); + } +}; diff --git a/server/middlewares/before/session.js b/server/middlewares/before/session.js new file mode 100644 index 0000000..c546fcb --- /dev/null +++ b/server/middlewares/before/session.js @@ -0,0 +1,26 @@ +'use strict'; + + +import _ from 'lodash'; +import Session from 'express-session'; + + +module.exports = { + register(app) { + app.logger().info('Applying session middleware...'); + + let config = app.config().options.session; + let sessionOptions = _.omit(config, ['redisOptions']); + + let session = Session(sessionOptions); + + let sessionRoutes = config.routes; + + app.server().use(sessionRoutes, session); + + return app.server().use((request, response, next :Function) => { + app.logger().info(`Request with session id "${request.session && request.session.id || '[no session started]'}"`); + next(); + }); + } +}; diff --git a/server/middlewares/before/webpackDev.js b/server/middlewares/before/webpackDev.js new file mode 100644 index 0000000..cb0e1f1 --- /dev/null +++ b/server/middlewares/before/webpackDev.js @@ -0,0 +1,18 @@ +import middleware from 'webpack-dev-middleware'; + + +module.exports = { + register(app) { + app.logger().info('Applying Webpack dev middleware...'); + + const config = app.config().options.webpack || {}; + const compiler = config.compiler; + + const publicPath = config.options && config.options.output ? config.options.output.publicPath : {}; + + app.server().use(middleware(compiler, { + noInfo: true, + publicPath + })); + } +}; diff --git a/server/middlewares/before/webpackHot.js b/server/middlewares/before/webpackHot.js new file mode 100644 index 0000000..dcc3ae2 --- /dev/null +++ b/server/middlewares/before/webpackHot.js @@ -0,0 +1,15 @@ +'use strict'; + + +import middleware from 'webpack-hot-middleware'; + + +module.exports = { + register(app) { + app.logger().info('Applying Webpack hot module replacement middleware...'); + + let compiler = app.config().options.webpack.compiler; + + app.server().use(middleware(compiler)); + } +}; diff --git a/server/middlewares/index.js b/server/middlewares/index.js new file mode 100644 index 0000000..444c07a --- /dev/null +++ b/server/middlewares/index.js @@ -0,0 +1,4 @@ +import before from './before'; +import after from './after'; + +export default Object.assign({}, before, after); diff --git a/server/models/.gitkeep b/server/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/run.js b/server/run.js new file mode 100755 index 0000000..2cae991 --- /dev/null +++ b/server/run.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node + + +require('babel-register'); +require('babel-polyfill'); +require('./app'); diff --git a/server/runtime/.gitkeep b/server/runtime/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/services/.gitkeep b/server/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/services/index.js b/server/services/index.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/server/services/index.js @@ -0,0 +1 @@ +export default {}; diff --git a/server/templates/views/index.pug b/server/templates/views/index.pug new file mode 100644 index 0000000..d4859f2 --- /dev/null +++ b/server/templates/views/index.pug @@ -0,0 +1,16 @@ +doctype html +html(lang='en') + head + title Hobo + meta(charset='UTF-8') + meta(name='viewport', content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') + meta(name='description', content='') + meta(name='author', content='') + meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') + body + #app + + script#app-init + var __INITIAL_STATE__ = {options: {}}; + + script(src='/assets/app.js?' + Date.now()) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3364d6f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "sourceMap": true, + "module": "commonjs", + "noImplicitAny": false, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/typings.json b/typings.json new file mode 100644 index 0000000..4fca17f --- /dev/null +++ b/typings.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "lodash": "registry:npm/lodash#4.0.0+20160416211519", + "ramda": "registry:npm/ramda#0.21.0+20160531162943" + } +} diff --git a/webpack/index.js b/webpack/index.js new file mode 100644 index 0000000..62023e1 --- /dev/null +++ b/webpack/index.js @@ -0,0 +1,5 @@ +import development from './webpack.config.development'; + +export default { + development +}; diff --git a/webpack/settings.js b/webpack/settings.js new file mode 100644 index 0000000..082d0d3 --- /dev/null +++ b/webpack/settings.js @@ -0,0 +1,155 @@ +const Path = require('path'); + + +exports.PROJECT_ROOT = Path.join(__dirname, '..'); +exports.BASE_PATH = Path.join(exports.PROJECT_ROOT, './server'); +exports.PATH_CONSTANTS_PATH = Path.join(exports.BASE_PATH, './core/constants/paths'); + + +const PostCssNext = require('postcss-cssnext'); +const PostCssPreCss = require('precss'); +const PostCssLost = require('lost'); +const PostCssNano = require('cssnano'); +const PostCssFontMagician = require('postcss-font-magician'); +const PostCssPropertyLookup = require('postcss-property-lookup'); +const PostCssShort = require('postcss-short'); +const PostCssPixrem = require('pixrem'); +const PostCssAspectRatio = require('postcss-aspect-ratio'); +const PostCssAnimation = require('postcss-animation'); +const PostCssRucksack = require('rucksack-css'); + +const Constants = require('../server/core/constants/paths'); + + +exports.DEFAULT_ASSETS_DIR = Constants.DEFAULT_ASSETS_DIR; +exports.DEFAULT_CLIENT_APP_DIR = Constants.DEFAULT_CLIENT_APP_DIR; +exports.DEFAULT_SERVER_APP_DIR = Constants.DEFAULT_SERVER_APP_DIR; +exports.DEFAULT_CLIENT_APP_IMAGES_DIR = Constants.DEFAULT_CLIENT_APP_IMAGES_DIR; +exports.DEFAULT_CLIENT_APP_SPRITES_DIR = Constants.DEFAULT_CLIENT_APP_SPRITES_DIR; +exports.DEFAULT_NODE_MODULES_DIR = Constants.DEFAULT_NODE_MODULES_DIR; +exports.DEFAULT_CLIENT_APP_FONTS_DIR = Constants.DEFAULT_CLIENT_APP_FONTS_DIR; +exports.DEFAULT_CLIENT_APP_CSS_DIR = Constants.DEFAULT_CLIENT_APP_CSS_DIR; +exports.DEFAULT_CLIENT_APP_SCSS_DIR = Constants.DEFAULT_CLIENT_APP_SCSS_DIR; +exports.DEFAULT_CLIENT_APP_LESS_DIR = Constants.DEFAULT_CLIENT_APP_LESS_DIR; +exports.DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR = Constants.DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR; +exports.DEFAULT_WEBPACK_CONFIG_DIR = Constants.DEFAULT_WEBPACK_CONFIG_DIR; + +exports.IMAGES_INCLUDE = [ + exports.DEFAULT_CLIENT_APP_IMAGES_DIR, + exports.DEFAULT_CLIENT_APP_SPRITES_DIR +]; + +exports.BABEL_LOADER = ['babel']; +exports.TYPESCRIPT_LOADER = ['ts']; +exports.ESLINT_LOADER = ['eslint']; +exports.URL_LOADER = 'url'; +exports.FILE_LOADER = 'file'; +exports.JSON_LOADER = 'json'; +exports.PUG_LOADER = 'pug'; +exports.STYLE_LOADER = 'style'; +exports.CSS_LOADER_MODULE = 'css?module'; +exports.CSS_LOADER = exports.CSS_LOADER_MODULE + '&localIdentName=[local]__[hash:base64:5]'; +exports.CSS_LOADER_CLEAN = exports.CSS_LOADER_MODULE + '&localIdentName=[local]'; +exports.POSTCSS_LOADER = 'postcss'; +exports.RESOLVE_URL_LOADER = 'resolve-url'; +exports.LESS_LOADER = + 'less?sourceMap&outputStyle=expanded' + + '&includePaths[]=' + encodeURIComponent(exports.DEFAULT_CLIENT_APP_LESS_DIR) + + '&includePaths[]=' + encodeURIComponent(exports.DEFAULT_CLIENT_APP_CSS_DIR); +exports.SCSS_LOADER = + 'sass?sourceMap&outputStyle=expanded' + + '&includePaths[]=' + encodeURIComponent(exports.DEFAULT_CLIENT_APP_SCSS_DIR) + + '&includePaths[]=' + encodeURIComponent(exports.DEFAULT_CLIENT_APP_CSS_DIR); + +exports.IMAGES_LOADER = exports.URL_LOADER; +exports.GIF_LOADER = exports.URL_LOADER + '?limit=10000&mimetype=image/gif'; +exports.WOFF_LOADER = exports.URL_LOADER + '?limit=10000&mimetype=application/font-woff'; +exports.TTF_LOADER = exports.URL_LOADER + '?limit=10000&mimetype=application/octet-stream'; +exports.EOT_LOADER = exports.FILE_LOADER; +exports.SVG_LOADER = 'svg-url-loader'; +exports.STYLE_CSS_LOADER = exports.STYLE_LOADER + '!' + exports.CSS_LOADER; +exports.STYLE_CSS_LOADER_MODULE = exports.STYLE_LOADER + '!' + exports.CSS_LOADER_CLEAN; +exports.CSS_PREPROCESSOR_LOADER_CLEAN = exports.STYLE_CSS_LOADER + '&importLoaders=1'; +exports.CSS_PREPROCESSOR_LOADER = exports.CSS_PREPROCESSOR_LOADER_CLEAN + '&sourceMap'; +exports.PREPROCESSOR_TO_CSS_LOADER = exports.CSS_PREPROCESSOR_LOADER + '!' + + exports.POSTCSS_LOADER + '!' + exports.RESOLVE_URL_LOADER; +exports.LESS_TO_CSS_LOADER = exports.PREPROCESSOR_TO_CSS_LOADER + '!' + exports.LESS_LOADER; +exports.SCSS_TO_CSS_LOADER = exports.PREPROCESSOR_TO_CSS_LOADER + '!' + exports.SCSS_LOADER; + +exports.RESOLVED_EXTENSIONS = ['', '.js', '.jsx', '.scss', '.css', '.less', '.ts']; +exports.RESOLVED_MODULE_DIRECTORIES = ['app', 'node_modules']; + +exports.RESOLVED_SERVER_EXTENSIONS = ['', '.js', '.json', '.pug']; +exports.RESOLVED_SERVER_MODULE_DIRECTORIES = ['./server', 'node_modules', './webpack']; + +exports.SPRITE_STYLE_PROCESSOR = 'scss'; +exports.SPRITE_STYLE_CLASS_PREFIX = 'icon'; +exports.SPRITE_STYLE_FILE_NAME_PREFIX = 'sprites'; +exports.SPRITE_IMAGE_FORMAT = 'png'; +exports.SPRITE_BUNDLE_MODE = 'multiple'; + +exports.PUG_FILES_PATTERN = /\.pug$/; +exports.ES6_JS_FILES_PATTERN = /\.js$|\.jsx$/; +exports.TYPESCRIPT_FILES_PATTERN = /\.ts$/; +exports.JSON_FILES_PATTERN = /\.json$/; +exports.IMAGE_FILES_PATTERN = /\.(png|jpg)$/; +exports.GIF_FILES_PATTERN = /\.gif$/; +exports.SVG_IMAGE_FILES_PATTERN = /\.svg/; +exports.WOFF_FONT_FILES_PATTERN = /\.woff(\?v=\d+\.\d+\.\d+)?$/; +exports.TTF_FONT_FILES_PATTERN = /\.ttf(\?v=\d+\.\d+\.\d+)?$/; +exports.EOT_FONT_FILES_PATTERN = /\.eot(\?v=\d+\.\d+\.\d+)?$/; +exports.CSS_FILES_PATTERN = /\.css$/; +exports.LESS_FILES_PATTERN = /\.less$/; +exports.SCSS_FILES_PATTERN = /\.scss$/; +exports.NODE_MODULES_DIR_PATTERN = /node_modules/; +exports.ASSETS_DIR_PATTERN = '/assets/'; + +exports.CLIENT_APP_ENTRY = './client'; +exports.SERVER_APP_ENTRY = './app'; +exports.CLIENT_APP_COMPILED_OUTPUT_FILE_PATTERN = '[name].js'; +exports.SERVER_APP_COMPILED_OUTPUT_FILE_PATTERN = '[name].server.js'; +exports.SERVER_APP_LIBRARY_TARGET_FORMAT = 'commonjs2'; + +exports.SHIMS = [ + 'es5-shim/es5-shim', + 'es5-shim/es5-sham', + 'json3/lib/json3', + 'es6-shim/es6-shim', + 'es6-shim/es6-sham', + 'es7-shim/es7-shim', + 'babel-polyfill' +]; + + +const postCssFontMagician = PostCssFontMagician(); // {protocol: 'https'} +const postCssNext = PostCssNext({ + warnForDuplicates: false +}); +const postCssNano = PostCssNano({ + discardComments: { + removeAll: true + }, + calc: true, + convertValues: true, + core: true, + discardDuplicates: true, + discardEmpty: true, + discardOverridden: true, + mergeIdents: true, + mergeRules: true, + minifyFontValues: true, + minifyGradients: true, + minifySelectors: true, + orderedValues: true, + styleCache: true, + svgo: true, + uniqueSelectors: true +}); + + +exports.POSTCSS_ENABLED_MODULES = [ + PostCssRucksack(), postCssNext, PostCssPreCss(), postCssFontMagician, + PostCssLost(), postCssNano, PostCssShort(), PostCssPixrem(), + PostCssPropertyLookup(), PostCssAspectRatio(), + PostCssAnimation() +]; diff --git a/webpack/webpack.config.development.js b/webpack/webpack.config.development.js new file mode 100644 index 0000000..01aa57e --- /dev/null +++ b/webpack/webpack.config.development.js @@ -0,0 +1,164 @@ +import Webpack from 'webpack'; +import WebpackSpritePlugin from 'sprite-webpack-plugin'; + +import { + DEFAULT_ASSETS_DIR, + DEFAULT_CLIENT_APP_DIR, + DEFAULT_CLIENT_APP_FONTS_DIR, + DEFAULT_CLIENT_APP_CSS_DIR, + DEFAULT_CLIENT_APP_SCSS_DIR, + DEFAULT_CLIENT_APP_LESS_DIR, + IMAGES_INCLUDE, + BABEL_LOADER, + TYPESCRIPT_LOADER, + JSON_LOADER, + IMAGES_LOADER, + WOFF_LOADER, + TTF_LOADER, + EOT_LOADER, + SVG_LOADER, + GIF_LOADER, + STYLE_CSS_LOADER, + LESS_TO_CSS_LOADER, + SCSS_TO_CSS_LOADER, + RESOLVED_EXTENSIONS, + RESOLVED_MODULE_DIRECTORIES, + ES6_JS_FILES_PATTERN, + TYPESCRIPT_FILES_PATTERN, + JSON_FILES_PATTERN, + IMAGE_FILES_PATTERN, + GIF_FILES_PATTERN, + SVG_IMAGE_FILES_PATTERN, + WOFF_FONT_FILES_PATTERN, + TTF_FONT_FILES_PATTERN, + EOT_FONT_FILES_PATTERN, + CSS_FILES_PATTERN, + STYLE_CSS_LOADER_MODULE, + LESS_FILES_PATTERN, + SCSS_FILES_PATTERN, + NODE_MODULES_DIR_PATTERN, + ASSETS_DIR_PATTERN, + CLIENT_APP_ENTRY, + CLIENT_APP_COMPILED_OUTPUT_FILE_PATTERN, + POSTCSS_ENABLED_MODULES, + DEFAULT_CLIENT_APP_IMAGES_DIR, + DEFAULT_CLIENT_APP_SPRITES_DIR, + DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR, + SPRITE_IMAGE_FORMAT, + SPRITE_STYLE_CLASS_PREFIX, + SPRITE_STYLE_FILE_NAME_PREFIX, + SPRITE_STYLE_PROCESSOR, + SPRITE_BUNDLE_MODE, + SHIMS, + PUG_FILES_PATTERN, + PUG_LOADER +} from './settings'; + + +import { + DEFAULT_WEBPACK_HOT_MIDDLEWARE_SCRIPT +} from '@/core/constants'; + + +let commonLoaders = [{ + test: ES6_JS_FILES_PATTERN, + loaders: BABEL_LOADER, + include: DEFAULT_CLIENT_APP_DIR, + exclude: NODE_MODULES_DIR_PATTERN +}, { + test: TYPESCRIPT_FILES_PATTERN, + loaders: TYPESCRIPT_LOADER, + include: DEFAULT_CLIENT_APP_DIR, + exclude: NODE_MODULES_DIR_PATTERN +}, { + test: PUG_FILES_PATTERN, + loader: PUG_LOADER +}, { + test: JSON_FILES_PATTERN, + loader: JSON_LOADER +}, { + test: IMAGE_FILES_PATTERN, + // include: IMAGES_INCLUDE, + loader: IMAGES_LOADER +}, { + test: GIF_FILES_PATTERN, + // include: IMAGES_INCLUDE, + loader: GIF_LOADER +}, { + test: WOFF_FONT_FILES_PATTERN, + // include: DEFAULT_CLIENT_APP_FONTS_DIR, + loader: WOFF_LOADER +}, { + test: TTF_FONT_FILES_PATTERN, + // include: DEFAULT_CLIENT_APP_FONTS_DIR, + loader: TTF_LOADER +}, { + test: EOT_FONT_FILES_PATTERN, + // include: DEFAULT_CLIENT_APP_FONTS_DIR, + loader: EOT_LOADER +}, { + test: SVG_IMAGE_FILES_PATTERN, + // include: IMAGES_INCLUDE, + loader: SVG_LOADER +}]; + + +let CLIENT_COMPILATION_DEVTOOL = 'eval'; +let WEBPACK_ENABLED_PLUGINS = [ + new Webpack.HotModuleReplacementPlugin(), + new Webpack.NoErrorsPlugin(), + new WebpackSpritePlugin({ + source: DEFAULT_CLIENT_APP_IMAGES_DIR, + imgPath: DEFAULT_CLIENT_APP_SPRITES_DIR, + cssPath: DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR, + format: SPRITE_IMAGE_FORMAT, + prefix: SPRITE_STYLE_CLASS_PREFIX, + spriteName: SPRITE_STYLE_FILE_NAME_PREFIX, + processor: SPRITE_STYLE_PROCESSOR, + bundleMode: SPRITE_BUNDLE_MODE, + useImport: true + }) +]; + + + +let webpack = { + devtool: CLIENT_COMPILATION_DEVTOOL, + name: 'Client-Side Bundle', + context: DEFAULT_CLIENT_APP_DIR, + entry: { + app: SHIMS.concat([ + CLIENT_APP_ENTRY, + DEFAULT_WEBPACK_HOT_MIDDLEWARE_SCRIPT + ]) + }, + output: { + path: DEFAULT_ASSETS_DIR, + filename: CLIENT_APP_COMPILED_OUTPUT_FILE_PATTERN, + publicPath: ASSETS_DIR_PATTERN + }, + module: { + loaders: commonLoaders.concat([{ + test: SCSS_FILES_PATTERN, + include: DEFAULT_CLIENT_APP_SCSS_DIR, + loader: SCSS_TO_CSS_LOADER + }, { + test: LESS_FILES_PATTERN, + include: DEFAULT_CLIENT_APP_LESS_DIR, + loader: LESS_TO_CSS_LOADER + }, { + test: CSS_FILES_PATTERN, + // include: [DEFAULT_CLIENT_APP_CSS_DIR], + loader: STYLE_CSS_LOADER_MODULE + }]) + }, + postcss: POSTCSS_ENABLED_MODULES, + resolve: { + extensions: RESOLVED_EXTENSIONS, + modulesDirectories: RESOLVED_MODULE_DIRECTORIES + }, + plugins: WEBPACK_ENABLED_PLUGINS +}; + + +module.exports = webpack; diff --git a/webpack/webpack.config.production.js b/webpack/webpack.config.production.js new file mode 100644 index 0000000..1bef416 --- /dev/null +++ b/webpack/webpack.config.production.js @@ -0,0 +1,201 @@ +const Webpack = require('webpack'); +const WebpackSpritePlugin = require('sprite-webpack-plugin'); +const WebpackExtractTextPlugin = require('extract-text-webpack-plugin'); +const WebpackCompressionPlugin = require('compression-webpack-plugin'); +const webpackNodeExternals = require('webpack-node-externals'); +// const Fs = require('fs'); + + +const Settings = require('./settings'); + +const PNG_FILES_PATTERN = /\.png$/; +const JPG_FILES_PATTERN = /\.jpg$/; + +const URL_IMAGE_LOADER = Settings.URL_LOADER + '?limit=10000'; +const PNG_LOADER = URL_IMAGE_LOADER + '&mimetype=image/png'; +const JPG_LOADER = URL_IMAGE_LOADER + '&mimetype=image/jpeg'; + + +const CSS_LOADER = Settings.CSS_LOADER + '&importLoaders=1&minimize'; +const CSS_LOADER_CLEAN = Settings.CSS_LOADER_CLEAN + '&importLoaders=1'; +const PREPROCESSOR_TO_CSS_LOADER = CSS_LOADER + '!' + Settings.POSTCSS_LOADER + '!' + + Settings.RESOLVE_URL_LOADER; +const PREPROCESSOR_TO_CSS_LOADER_CLEAN = CSS_LOADER_CLEAN + '!' + Settings.POSTCSS_LOADER + '!' + + Settings.RESOLVE_URL_LOADER; +const LESS_TO_CSS_LOADER = PREPROCESSOR_TO_CSS_LOADER + '!' + Settings.LESS_LOADER; +const SCSS_TO_CSS_LOADER = PREPROCESSOR_TO_CSS_LOADER + '!' + Settings.SCSS_LOADER; + +const CLIENT_COMPILATION_DEVTOOL = 'source-map'; +const CLIENT_OUTPUT_STYLES_NAME = 'styles/main.css'; + +const WEBPACK_ENABLED_PLUGINS = [ + new Webpack.optimize.OccurenceOrderPlugin(), + new WebpackExtractTextPlugin(CLIENT_OUTPUT_STYLES_NAME), + new Webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"' + }), + new Webpack.optimize.UglifyJsPlugin({ + minimize: true, + output: { + comments: false + }, + compressor: { + warnings: false, + sequences: true, + dead_code: true, + drop_debugger: true, + conditionals: true, + evaluate: true, + booleans: true, + loops: true, + unused: true, + if_return: true, + cascade: true + } + } + ), + new Webpack.optimize.DedupePlugin(), + new WebpackSpritePlugin({ + source: Settings.DEFAULT_CLIENT_APP_IMAGES_DIR, + imgPath: Settings.DEFAULT_CLIENT_APP_SPRITES_DIR, + cssPath: Settings.DEFAULT_CLIENT_APP_SCSS_SPRITES_DIR, + format: Settings.SPRITE_IMAGE_FORMAT, + prefix: Settings.SPRITE_STYLE_CLASS_PREFIX, + spriteName: Settings.SPRITE_STYLE_FILE_NAME_PREFIX, + processor: Settings.SPRITE_STYLE_PROCESSOR, + bundleMode: Settings.SPRITE_BUNDLE_MODE, + useImport: true + }), + new WebpackCompressionPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: /\.js$|\.css$|\.html$/, + threshold: 10240, + minRatio: 0.8 + })]; + +const WEBPACK_ENABLED_SERVER_PLUGINS = [ + new Webpack.optimize.OccurenceOrderPlugin(), + new Webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"' + }), + new Webpack.optimize.DedupePlugin() +]; + + +const clientLoaders = [{ + test: Settings.ES6_JS_FILES_PATTERN, + loaders: Settings.BABEL_LOADER, + include: Settings.DEFAULT_CLIENT_APP_DIR, + exclude: Settings.NODE_MODULES_DIR_PATTERN +}, { + test: Settings.TYPESCRIPT_FILES_PATTERN, + loaders: Settings.TYPESCRIPT_LOADER, + include: Settings.DEFAULT_CLIENT_APP_DIR, + exclude: Settings.NODE_MODULES_DIR_PATTERN +}, { + test: Settings.PUG_FILES_PATTERN, + loader: Settings.PUG_LOADER +}, { + test: Settings.JSON_FILES_PATTERN, + loader: Settings.JSON_LOADER +}, { + test: PNG_FILES_PATTERN, + include: Settings.IMAGES_INCLUDE, + loader: PNG_LOADER +}, { + test: JPG_FILES_PATTERN, + include: Settings.IMAGES_INCLUDE, + loader: JPG_LOADER +}, { + test: Settings.GIF_FILES_PATTERN, + include: Settings.IMAGES_INCLUDE, + loader: Settings.GIF_LOADER +}, { + test: Settings.CSS_FILES_PATTERN, + // include: CSS_DIRS, + loader: WebpackExtractTextPlugin.extract(Settings.STYLE_LOADER, PREPROCESSOR_TO_CSS_LOADER_CLEAN) +}, { + test: Settings.SCSS_FILES_PATTERN, + include: Settings.DEFAULT_CLIENT_APP_SCSS_DIR, + loader: WebpackExtractTextPlugin.extract(Settings.STYLE_LOADER, SCSS_TO_CSS_LOADER) +}, { + test: Settings.LESS_FILES_PATTERN, + include: Settings.DEFAULT_CLIENT_APP_LESS_DIR, + loader: WebpackExtractTextPlugin.extract(Settings.STYLE_LOADER, LESS_TO_CSS_LOADER) +}, { + test: Settings.WOFF_FONT_FILES_PATTERN, + include: Settings.DEFAULT_CLIENT_APP_FONTS_DIR, + loader: Settings.WOFF_LOADER +}, { + test: Settings.TTF_FONT_FILES_PATTERN, + include: Settings.DEFAULT_CLIENT_APP_FONTS_DIR, + loader: Settings.TTF_LOADER +}, { + test: Settings.EOT_FONT_FILES_PATTERN, + include: Settings.DEFAULT_CLIENT_APP_FONTS_DIR, + loader: Settings.EOT_LOADER +}, { + test: Settings.SVG_IMAGE_FILES_PATTERN, + include: Settings.IMAGES_INCLUDE, + loader: Settings.SVG_LOADER +}]; + + +const serverLoaders = [{ + test: Settings.ES6_JS_FILES_PATTERN, + loaders: Settings.BABEL_LOADER, + include: [Settings.DEFAULT_SERVER_APP_DIR, Settings.DEFAULT_WEBPACK_CONFIG_DIR] +}, { + test: Settings.PUG_FILES_PATTERN, + loader: Settings.PUG_LOADER +}, { + test: Settings.JSON_FILES_PATTERN, + loader: Settings.JSON_LOADER +}]; + + +module.exports = [{ + name: 'Client-Side Bundle', + devtool: CLIENT_COMPILATION_DEVTOOL, + context: Settings.DEFAULT_CLIENT_APP_DIR, + entry: { + app: Settings.SHIMS.concat([Settings.CLIENT_APP_ENTRY]) + }, + output: { + path: Settings.DEFAULT_ASSETS_DIR, + filename: Settings.CLIENT_APP_COMPILED_OUTPUT_FILE_PATTERN, + publicPath: Settings.ASSETS_DIR_PATTERN + }, + module: { + loaders: clientLoaders + }, + postcss: Settings.POSTCSS_ENABLED_MODULES, + resolve: { + extensions: Settings.RESOLVED_EXTENSIONS, + modulesDirectories: Settings.RESOLVED_MODULE_DIRECTORIES + }, + plugins: WEBPACK_ENABLED_PLUGINS +}, { + name: 'Server-Side Bundle', + devtool: CLIENT_COMPILATION_DEVTOOL, + context: Settings.DEFAULT_SERVER_APP_DIR, + target: 'node', + entry: { + app: Settings.SHIMS.concat([Settings.SERVER_APP_ENTRY]) + }, + output: { + path: Settings.DEFAULT_ASSETS_DIR, + filename: Settings.SERVER_APP_COMPILED_OUTPUT_FILE_PATTERN, + publicPath: Settings.ASSETS_DIR_PATTERN + }, + module: { + loaders: serverLoaders + }, + externals: [webpackNodeExternals()], + resolve: { + extensions: Settings.RESOLVED_SERVER_EXTENSIONS, + modulesDirectories: Settings.RESOLVED_SERVER_MODULE_DIRECTORIES + }, + plugins: WEBPACK_ENABLED_SERVER_PLUGINS +}];