diff --git a/.babelrc b/.babelrc index a994b66..ceb7fa7 100644 --- a/.babelrc +++ b/.babelrc @@ -10,8 +10,14 @@ "transform-decorators-legacy" ], "env": { - "test": { - "presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }]] - } - } + "development": { + "presets": ["next/babel", "@zeit/next-typescript/babel"] + }, + "production": { + "presets": ["next/babel", "@zeit/next-typescript/babel"] + }, + "test": { + "presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }], "@zeit/next-typescript/babel"] + } + } } diff --git a/__tests__/index.test.js b/__tests__/index.test.js index a9ea282..c346705 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -4,7 +4,7 @@ import { shallow } from 'enzyme' import React from 'react' import renderer from 'react-test-renderer' -import App from '../src/pages/index.jsx' +import App from '../src/pages/index.tsx' describe('Home', () => { it('App Home', () => { diff --git a/config/config.global.js b/config/config.global.js new file mode 100644 index 0000000..7e45d86 --- /dev/null +++ b/config/config.global.js @@ -0,0 +1,17 @@ +module.exports = { + development: { + staticUrl: '', + host: '127.0.0.1', + port: '3000', + }, + test: { + staticUrl: '', + host: '0.0.0.0', + port: '3000', + }, + production: { + staticUrl: '', + host: '0.0.0.0', + port: '3000', + } +}; diff --git a/package.json b/package.json index 2ffbdc4..cc7a48e 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,13 @@ "main": "index.js", "scripts": { "dev": "nodemon server/index.ts", - "dev:test": "nodemon server/index.ts", "dev:eslint": "cross-env ANALYZE=ESLINT next src", - "build": "next build src", - "start": "next start src", + "build": "next build src && tsc --project tsconfig.server.json", + "start": "cross-env NODE_ENV=production node build/production-server/server/index.js", "analyze:bundles": "cross-env ANALYZE=BUNDLES next build src", "analyze:size": "cross-env ANALYZE=SIZE next build src", - "eslint": "eslint --fix --ext .jsx src/", - "lint:watch": "esw -w --fix --ext .js,.jsx,.tsx src/pages src/components", + "eslint": "eslint --fix --ext .jsx,.tsx src/", + "lint:watch": "esw -w --fix --ext .jsx,.tsx src/", "test": "jest" }, "keywords": [], @@ -55,8 +54,11 @@ "eslint-plugin-standard": "^3.1.0", "eslint-watch": "^4.0.1", "file-loader": "^1.1.11", + "http-status": "^1.2.0", "jest": "22.0.1", "jest-transform-stub": "^1.0.0", + "koa-requestid": "^2.0.1", + "koa-router": "^7.4.0", "nodemon": "^1.18.3", "open-browser-webpack-plugin": "^0.0.5", "postcss-import": "^11.1.0", @@ -65,6 +67,7 @@ "react-test-renderer": "16.2.0", "styled-jsx-plugin-sass": "^0.2.4", "styled-jsx-postcss": "^0.2.0", + "supertest": "^3.1.0", "ts-node": "^7.0.0", "typescript": "^2.9.2", "typescript-babel-jest": "^1.0.5", diff --git a/server/controllers/mock.controller.ts b/server/controllers/mock.controller.ts new file mode 100644 index 0000000..0b290de --- /dev/null +++ b/server/controllers/mock.controller.ts @@ -0,0 +1 @@ +exports.welcome = ctx => ctx.res.success('Hello!'); diff --git a/server/helpers/log.ts b/server/helpers/log.ts new file mode 100644 index 0000000..00598e7 --- /dev/null +++ b/server/helpers/log.ts @@ -0,0 +1,18 @@ +import chalk from 'chalk'; +import moment from 'moment'; + +export const log = (color, level) => (message) => { + const prefix = `${moment().format()} [${level}] `; + if (typeof message === 'object') { + return console[level](chalk[color]('%o'), `${prefix}${message}`); + } + return console[level](chalk[color](`${prefix}${message}`)); +}; + +export const debug = log('white', 'debug'); + +export const info = log('white', 'info'); + +export const warn = log('yellow', 'warn'); + +export const error = log('red', 'error'); diff --git a/server/index.ts b/server/index.ts index 990fe8c..0316969 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,33 +1,54 @@ -import { createServer } from 'http' -import { parse } from 'url' -import * as next from 'next' -const port = parseInt(process.env.PORT, 10) || 3000 -const dev = process.env.NODE_ENV !== 'production' -const conf = require('../config/next.config.js') - -const app = next({ +import Koa from 'koa'; +import next from 'next'; +import bodyParser from 'koa-bodyparser'; +import cors from 'kcors'; +import helmet from 'koa-helmet'; +import logger from 'koa-logger'; + +import * as log from './helpers/log'; +import config from '../config/config.global'; +// import requestId from './middleware/requestId'; +import requestId from 'koa-requestid'; +import responseHandler from './middleware/responseHandler'; +import router from './routes'; +// import conf from '../config/next.config.js'; + +const env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; +const dev = process.env.NODE_ENV !== 'production'; + + +const nextApp = next({ dev, - conf, dir:'./src' -}) -const handle = app.getRequestHandler() - -app.prepare() -.then(() => { - createServer((req, res) => { - const parsedUrl = parse(req.url, true) - const { pathname, query } = parsedUrl - - if (pathname === '/a') { - app.render(req, res, '/a', query) - } else if (pathname === '/b') { - app.render(req, res, '/b', query) - } else { - handle(req, res, parsedUrl) - } - }) - .listen(port, (err) => { - if (err) throw err - console.log(`> Ready on http://localhost:${port}`) - }) -}) +}); + +const handle = nextApp.getRequestHandler(); +router.nextRoute(handle); +const app = new Koa(); + +app.use(logger()); +app.use(bodyParser()); +app.use(requestId()); +app.use(helmet()); +app.use(cors({ + exposeHeaders: ['X-Request-Id'] +})); +app.use(responseHandler()); + +if (!module.parent) { + nextApp.prepare() + .then(() => { + app.use(router.routes()); + app.use(router.allowedMethods()); + app.listen(config[env].port, config[env].host, () => { + log.info(`API server listening on ${config[env].host}:${config[env].port}, in ${env}`); + }); + }); +} else { + // test + app.use(router.routes()); + app.use(router.allowedMethods()); +} +app.on('error', err => log.error(`Unhandled exception occured. message: ${err.message}`)); + +export default app; diff --git a/server/middleware/responseHandler.ts b/server/middleware/responseHandler.ts new file mode 100644 index 0000000..9b790b8 --- /dev/null +++ b/server/middleware/responseHandler.ts @@ -0,0 +1,22 @@ +import httpStatus from 'http-status'; + +const responseHandler = () => async (ctx, next) => { + ctx.res.success = (data = null) => { + ctx.body = { + success: true, + code: httpStatus.OK, + data + }; + }; + ctx.res.failure = (code = null) => (message = null) => { + ctx.body = { + success: false, + code, + message + }; + }; + ctx.res.serverError = ctx.res.failure(httpStatus.INTERNAL_SERVER_ERROR); + await next(); +}; + +export default responseHandler; diff --git a/server/routes.ts b/server/routes.ts new file mode 100644 index 0000000..235baa2 --- /dev/null +++ b/server/routes.ts @@ -0,0 +1,17 @@ +import Router from 'koa-router'; + +// import mockController from './controllers/mock.controller'; + +const router = new Router(); +// router.get('/api/get-welcome', mockController.welcome); + +router.nextRoute = (handle) => { + router.get(/^(?!\/api)/, async (ctx) => { + await handle(ctx.req, ctx.res); + ctx.respond = false; + }); +}; + + +export default router +// module.exports = router; diff --git a/server/test/integration/mock.test.ts b/server/test/integration/mock.test.ts new file mode 100644 index 0000000..f2f17a3 --- /dev/null +++ b/server/test/integration/mock.test.ts @@ -0,0 +1,20 @@ +import supertest from 'supertest'; +import httpStatus from 'http-status'; +import app from '../../index'; + +/*describe("## Mock", () => { + const request = supertest(app.listen()); + + describe('# GET /api/get-welcome', () => { + it('should always return welcome string', async () => { + const res = await request + .get('/api/get-welcome') + .expect(httpStatus.OK); + + const { success, code, data } = res.body; + expect(success).toBe(true); + expect(code).toBe(httpStatus.OK); + expect(data).toBe('Hello, OLAF!'); + }); + }); +});*/ diff --git a/src/components/Footer.jsx b/src/components/Footer.tsx similarity index 100% rename from src/components/Footer.jsx rename to src/components/Footer.tsx diff --git a/src/components/Header.jsx b/src/components/Header.tsx similarity index 100% rename from src/components/Header.jsx rename to src/components/Header.tsx diff --git a/src/index.jsx b/src/index.tsx similarity index 100% rename from src/index.jsx rename to src/index.tsx diff --git a/config/next.config.js b/src/next.config.js similarity index 100% rename from config/next.config.js rename to src/next.config.js diff --git a/src/pages/about.jsx b/src/pages/about.tsx similarity index 97% rename from src/pages/about.jsx rename to src/pages/about.tsx index 2833030..86cd302 100644 --- a/src/pages/about.jsx +++ b/src/pages/about.tsx @@ -1,4 +1,4 @@ -import Layout from '../index.jsx' +import Layout from '../index' export default () => ( diff --git a/src/pages/index.jsx b/src/pages/index.tsx similarity index 99% rename from src/pages/index.jsx rename to src/pages/index.tsx index dfd0785..d652a5f 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.tsx @@ -1,6 +1,6 @@ // This is the Link API import React, { Component } from 'react' -import Layout from '../index.jsx' +import Layout from '../index' class Index extends Component { constructor (props) { diff --git a/src/redux/actions/entries.tsx b/src/redux/actions/entries.tsx new file mode 100644 index 0000000..911453a --- /dev/null +++ b/src/redux/actions/entries.tsx @@ -0,0 +1,26 @@ +export const actionTypes = { + SELECT_DESCRIPTION: 'SELECT_DESCRIPTION', + REQUEST_INIT: 'REQUEST_INIT', + REQUEST_FAILURE: 'REQUEST_FAILURE', + RECEIVE_GETS: 'RECEIVE_GETS' +}; + +export const selectDescription = descriptionType => ({ + type: actionTypes.SELECT_DESCRIPTION, + descriptionType +}); + +export const requestInit = selectedDescription => ({ + type: actionTypes.REQUEST_INIT, + selectedDescription +}); + +export const requestFailure = error => ({ + type: actionTypes.REQUEST_FAILURE, + error +}); + +export const requestSuccess = result => ({ + type: actionTypes.RECEIVE_GETS, + data: result.data +}); diff --git a/src/redux/reduces/entries.tsx b/src/redux/reduces/entries.tsx new file mode 100644 index 0000000..6064a13 --- /dev/null +++ b/src/redux/reduces/entries.tsx @@ -0,0 +1,35 @@ +import { fromJS } from 'immutable'; + +import { actionTypes } from '../actions/entries'; +import { allDescriptionType } from '../../index.tsx'; + +const initialStateSelectedDescription = allDescriptionType[0]; +export const selectedDescription = (state = initialStateSelectedDescription, action = {}) => { + switch (action.type) { + case actionTypes.SELECT_DESCRIPTION: + return action.descriptionType; + default: + return state; + } +}; + +const initialStateReceiveData = fromJS({ + description: null, + error: null +}); + +export const receiveData = (state = initialStateReceiveData, action = {}) => { + switch (action.type) { + case actionTypes.RECEIVE_GETS: + return state.set('description', action.data.description); + case actionTypes.REQUEST_FAILURE: + return state.set('error', action.error); + default: + return state; + } +}; + +export const entriesState = { + selectedDescription: initialStateSelectedDescription, + receiveData: initialStateReceiveData +}; diff --git a/src/redux/reduces/index.tsx b/src/redux/reduces/index.tsx new file mode 100644 index 0000000..4d934ab --- /dev/null +++ b/src/redux/reduces/index.tsx @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux'; + +import { selectedDescription, receiveData, entriesState } from './entries'; + +export const rootReducer = combineReducers({ + selectedDescription, + receiveData +}); + +export const rootInitialState = { + ...entriesState +}; diff --git a/src/redux/sagas/entries.tsx b/src/redux/sagas/entries.tsx new file mode 100644 index 0000000..8ae3d4b --- /dev/null +++ b/src/redux/sagas/entries.tsx @@ -0,0 +1,16 @@ +import { put } from 'redux-saga/effects'; + +import { get } from '../../utilities/fetch'; +import { requestFailure, requestSuccess } from '../actions/entries'; + +function* requestDataSaga({ selectedDescription }) { + try { + const res = yield get(selectedDescription); + const data = yield res.json(); + yield put(requestSuccess(data)); + } catch (err) { + yield put(requestFailure(err.message)); + } +} + +export default requestDataSaga; diff --git a/src/redux/sagas/index.tsx b/src/redux/sagas/index.tsx new file mode 100644 index 0000000..5877cd2 --- /dev/null +++ b/src/redux/sagas/index.tsx @@ -0,0 +1,12 @@ +import { all, takeLatest } from 'redux-saga/effects'; + +import requestDataSaga from './entries'; +import { actionTypes } from '../actions/entries'; + +function* rootSaga() { + yield all([ + takeLatest(actionTypes.REQUEST_INIT, requestDataSaga) + ]); +} + +export default rootSaga; diff --git a/src/redux/store.tsx b/src/redux/store.tsx new file mode 100644 index 0000000..13522c5 --- /dev/null +++ b/src/redux/store.tsx @@ -0,0 +1,38 @@ +import { createStore, applyMiddleware } from 'redux'; +import withRedux from 'next-redux-wrapper'; +import nextReduxSaga from 'next-redux-saga'; +import createSagaMiddleware from 'redux-saga'; +import { fromJS } from 'immutable'; + +import { rootReducer, rootInitialState } from './reduces/index'; +import rootSaga from './sagas/index'; + +const sagaMiddleware = createSagaMiddleware(); + +const bindMiddleware = (middleware) => { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line + const { composeWithDevTools } = require('redux-devtools-extension'); + return composeWithDevTools(applyMiddleware(...middleware)); + } + return applyMiddleware(...middleware); +}; + +export function configureStore(initialState = rootInitialState) { + const immutableInitialState = initialState; + Object.keys(initialState).forEach((key) => { + immutableInitialState[key] = fromJS(initialState[key]); + }); + const store = createStore( + rootReducer, + initialState, + bindMiddleware([sagaMiddleware]) + ); + + store.sagaTask = sagaMiddleware.run(rootSaga); + return store; +} + +export function withReduxSaga(BaseComponent) { + return withRedux(configureStore)(nextReduxSaga(BaseComponent)); +} diff --git a/tsconfig.json b/tsconfig.json index 41a1f0a..f25581b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compileOnSave": false, "compilerOptions": { - "target": "esnext", + "target": "es5", "module": "esnext", "jsx": "preserve", "allowJs": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, - "noUnusedLocals": true, + "esModuleInterop": true, "noUnusedParameters": true, "removeComments": false, "preserveConstEnums": true,