├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── integration.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── logo.svg ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── components ├── App.js ├── Error.js ├── Faker.js ├── Head.js ├── Logger.js ├── Middleware.js ├── Res.js ├── Router.js ├── Routes.js ├── Static.js ├── constants.js └── index.js ├── context.js ├── index.js ├── renderer ├── generateRoute.js ├── helpers.js ├── index.js ├── renderHTML.js └── renderPage.js └── utils ├── common.js ├── fakerUtil.js └── propsUtil.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "esmodules": true 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["@babel/plugin-transform-react-jsx"] 13 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"], 6 | "react/prop-types": "warn", 7 | "react/jsx-filename-extension": 0, 8 | "react/jsx-one-expression-per-line": 0, 9 | "react/jsx-props-no-spreading": 0, 10 | "react/require-default-props": 0, 11 | "react/forbid-prop-types": 0, 12 | "jsx-a11y/alt-text": 0, 13 | "import/prefer-default-export": 0, 14 | "import/no-extraneous-dependencies": 0, 15 | "no-param-reassign": 0, 16 | "no-restricted-syntax": 0, 17 | "guard-for-in": 0, 18 | "no-plusplus": 0 19 | }, 20 | "ignorePatterns": "/dist" 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Run linter and build 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | lint_and_test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install dependencies 14 | run: npm ci 15 | - name: Validate 16 | run: npm run validate 17 | - name: Build 18 | run: npm run build 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .npmrc 4 | dist 5 | app -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | app -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactend / Express 2 | 3 | React-like http-server on Nodejs
4 | 5 | ### 🕹 [Playground on repl.it](https://repl.it/@orkhanjafarov/reactend-playground?v=1) 6 | 7 | ### 📄 [Reactend Template](https://github.com/gigantz/reactend-template) 8 | 9 |
10 | 11 | ![Planet Express](./logo.svg) 12 |
13 | 14 | ## What's that? 15 | 16 | - Node.js http-server based on React-Components 17 | - Express.js inside 18 | - Get, Post, Delete and etc. components to use router method 19 | - `Get(render)` and `Res.Render` to render your regular React DOM Components 20 | - useContext(ReqResContext) hook to access `req, res` 21 | - Support `styled-components` 22 | - Built-in logger (morgan) 23 | - Middleware component in Router and its Routes 24 | - `handler` prop in Route components to use as regular controller 25 | 26 | _and many many features that should be documented..._ 27 |

28 | 29 | ## Get started 30 | 31 | Run this to create reactend project on your local machine 32 | 33 | ``` 34 | npx create-reactend my-app 35 | ``` 36 | 37 |
38 | 39 | You choose template (default: basic) 40 | 41 | ``` 42 | npx create-reactend my-app --template faker 43 | ``` 44 | 45 |
46 | 47 | ## Code Example 48 | 49 | ```js 50 | import React from 'react'; 51 | import { resolve } from 'path'; 52 | 53 | import { registerApp, App, Static, Router, Get, Post, Res, Logger } from '@reactend/express'; 54 | 55 | const ExpressApp = () => ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | console.log(req.originalUrl)} 70 | /> 71 | 72 | 73 | 74 | 75 | ); 76 | 77 | registerApp(ExpressApp); 78 | ``` 79 | 80 |
81 | 82 | ## You can use this way too 83 | 84 | ```js 85 | import cors from 'cors'; 86 | ; 87 | ``` 88 | 89 | ```js 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |

Shut Up And Take My Money!

} /> 105 |
106 | 107 | 108 | 117 | 118 | ``` 119 | 120 |
121 | 122 | ## Components 123 | 124 | _This minor description for now (Docs is on the way)_

125 | `` - App Instance (props: port)
126 | `` - Static route (props: publicPath, path, options)
127 | `` - Router-Provider (props: path)
128 | `, and ...` - Route component (props: path, content,
handler, status)
129 | `` - Middleware (props: handler)
130 | `` - morgan logger (props: mode, disabled)
131 | `` - Response components
132 | `` - Render (props: component)
133 | `` - Response send (props: json, text, contentType)
134 | `` - Response Status (props: statusCode)
135 | `` - Response Send File (props: path, options,
onError)
136 | `` - Redirect (props: path, statusCode)
137 | `` - Redirect (props: length, locale, map)
138 |
139 |
140 | 141 | --- 142 | 143 | ## Contact me 144 | 145 | Email me if you have any idea and you would like to be contributor [orkhanjafarovr@gmail.com](mailto:orkhanjafarovr@gmail.com) 146 | 147 | Cheers ✨ 148 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactend/express", 3 | "version": "1.0.17", 4 | "description": "React-like http-server on Nodejs", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "url": "git@github.com:reactend/reactend-express.git" 8 | }, 9 | "scripts": { 10 | "dev": "nodemon ./app/index.js", 11 | "build": "NODE_ENV=production rollup -c rollup.config.js", 12 | "prepublishOnly": "npm run build", 13 | "validate": "eslint '*.js' && prettier '*.js' --write" 14 | }, 15 | "keywords": [ 16 | "reactend", 17 | "express", 18 | "react", 19 | "http-server" 20 | ], 21 | "author": "Orkhan Jafarov", 22 | "license": "ISC", 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": [ 26 | "lint-staged" 27 | ] 28 | } 29 | }, 30 | "lint-staged": { 31 | "*.js": [ 32 | "eslint", 33 | "prettier --write" 34 | ] 35 | }, 36 | "dependencies": { 37 | "compression": "^1.7.4", 38 | "cookie-parser": "^1.4.5", 39 | "express": "^4.17.1", 40 | "faker": "^5.4.0", 41 | "morgan": "^1.10.0", 42 | "prop-types": "^15.7.2", 43 | "react-reconciler": "^0.26.1" 44 | }, 45 | "peerDependencies": { 46 | "react": "^17.0.1", 47 | "react-dom": "^17.0.1", 48 | "react-helmet": "^6.1.0", 49 | "styled-components": "^5.2.1" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.12.13", 53 | "@babel/plugin-transform-react-jsx": "^7.12.13", 54 | "@babel/preset-env": "^7.12.13", 55 | "@rollup/plugin-babel": "^5.2.3", 56 | "@rollup/plugin-commonjs": "^17.1.0", 57 | "eslint": "^7.19.0", 58 | "eslint-config-airbnb": "^18.2.1", 59 | "eslint-config-prettier": "^7.2.0", 60 | "eslint-plugin-import": "^2.22.1", 61 | "eslint-plugin-jsx-a11y": "^6.4.1", 62 | "eslint-plugin-prettier": "^3.3.1", 63 | "eslint-plugin-react": "^7.22.0", 64 | "eslint-plugin-react-hooks": "^4.2.0", 65 | "husky": "^4.3.8", 66 | "lint-staged": "^10.5.4", 67 | "nodemon": "^2.0.7", 68 | "npm-run-all": "^4.1.5", 69 | "prettier": "^2.2.1", 70 | "rollup": "^2.38.5" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: { 7 | file: 'dist/index.js', 8 | format: 'cjs', 9 | }, 10 | external: ['react', 'react-dom', 'prop-types', 'styled-components'], 11 | plugins: [ 12 | babel({ 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | targets: { 18 | esmodules: true, 19 | }, 20 | }, 21 | ], 22 | ], 23 | plugins: ['@babel/plugin-transform-react-jsx'], 24 | }), 25 | commonjs(), 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const App = ({ children, port }) => {children}; 5 | 6 | App.propTypes = { 7 | port: PropTypes.number, 8 | children: PropTypes.node, 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const isDev = process.env.NODE_ENV !== 'production'; 5 | const color = '#83CD29'; 6 | const msgStyle = { backgroundColor: '#222', color, padding: 5, borderRadius: 4, fontSize: 15 }; 7 | 8 | export const Error = ({ title, msg, error }) => ( 9 |
19 |
40 |

{title || '🐛 Reactend Error'}

41 | {isDev && ( 42 | <> 43 | {msg} 44 |
45 | message: 46 | {error.message && {error.message}} 47 |
48 | stack: 49 | {error.stack && {error.stack}} 50 | 51 | )} 52 |
53 |
54 | ); 55 | 56 | Error.propTypes = { 57 | title: PropTypes.string, 58 | msg: PropTypes.string.isRequired, 59 | error: PropTypes.any, 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/Faker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { transformFakerMap } from '../utils/fakerUtil'; 5 | import { Middleware } from './Middleware'; 6 | import { passValues } from '../utils/propsUtil'; 7 | 8 | export const Faker = ({ map, length, locale }) => ( 9 | { 11 | let json; 12 | const params = passValues(req, { length, locale }); 13 | 14 | if (length) { 15 | json = Array.from({ length: +(params.length || 5) }, () => 16 | transformFakerMap(map, params.locale || 'en') 17 | ); 18 | } else { 19 | json = transformFakerMap(map, params.locale); 20 | } 21 | 22 | res.send(json); 23 | }} 24 | /> 25 | ); 26 | 27 | Faker.propTypes = { 28 | map: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, 29 | length: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 30 | locale: PropTypes.oneOfType([ 31 | PropTypes.string, 32 | PropTypes.oneOf([ 33 | 'az', 34 | 'cz', 35 | 'de', 36 | 'de_AT', 37 | 'de_CH', 38 | 'en', 39 | 'en_AU', 40 | 'en_AU_ocker', 41 | 'en_BORK', 42 | 'en_CA', 43 | 'en_GB', 44 | 'en_IE', 45 | 'en_IND', 46 | 'en_US', 47 | 'en_ZA', 48 | 'es', 49 | 'es_MX', 50 | 'fa', 51 | 'fi', 52 | 'fr', 53 | 'fr_CA', 54 | 'fr_CH', 55 | 'ge', 56 | 'hy', 57 | 'hr', 58 | 'id_ID', 59 | 'it', 60 | 'ja', 61 | 'ko', 62 | 'nb_NO', 63 | 'ne', 64 | 'nl', 65 | 'nl_BE', 66 | 'pl', 67 | 'pt_BR', 68 | 'pt_PT', 69 | 'ro', 70 | 'ru', 71 | 'sk', 72 | 'sv', 73 | 'tr', 74 | 'uk', 75 | 'vi', 76 | 'zh_CN', 77 | 'zh_TW', 78 | ]), 79 | ]), 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/Head.js: -------------------------------------------------------------------------------- 1 | import { Helmet } from 'react-helmet'; 2 | 3 | export const Head = Helmet; 4 | -------------------------------------------------------------------------------- /src/components/Logger.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * @param {{ 6 | * mode: 'skip' | 'stream' | 'combined' | 'common' | 'dev' | 'short' | 'tiny' 7 | * disabled: Boolean 8 | * }} props 9 | */ 10 | export const Logger = ({ mode, disabled }) => ; 11 | Logger.propTypes = { 12 | mode: PropTypes.oneOf(['skip', 'stream', 'combined', 'common', 'dev', 'short', 'tiny']) 13 | .isRequired, 14 | disabled: PropTypes.bool, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Middleware.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const Middleware = ({ handler }) => ; 5 | Middleware.propTypes = { 6 | handler: PropTypes.func.isRequired, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Res.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Faker } from './Faker'; 4 | 5 | /** 6 | * @param {{ name: 'Accept-Patch' | 'Accept-Ranges' | 'Age' | 'Allow' | 'Alt-Svc' | 'Cache-Control' | 'Connection' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Type' | 'Date' | 'Delta-Base' | 'ETag' | 'Expires' | 'IM' | 'Last-Modified' | 'Link' | 'Location' | 'Pragma' | 'Proxy-Authenticate' | 'Public-Key-Pins' | 'Retry-After' | 'Server' | 'Set-Cookie' | 'Strict-Transport-Security' | 'Trailer' | 'Transfer-Encoding' | 'Tk' | 'Upgrade' | 'Vary' | 'Via' | 'Warning' | 'WWW-Authenticate' | 'Content-Security-Policy' | 'Refresh' | 'X-Powered-By' | 'X-Request-ID' | 'X-UA-Compatible' | 'X-XSS-Protection' }} props 7 | */ 8 | const Header = ({ name, value }) => ; 9 | 10 | Header.propTypes = { 11 | name: PropTypes.string.isRequired, 12 | value: PropTypes.any, 13 | }; 14 | 15 | const Render = ({ component }) => ; 16 | 17 | Render.propTypes = { 18 | component: PropTypes.func, 19 | }; 20 | 21 | const Content = ({ json, contentType, text }) => { 22 | const ComponentsArray = []; 23 | 24 | if (contentType) 25 | ComponentsArray.push(); 26 | if (json) ComponentsArray.push(); 27 | if (text) ComponentsArray.push(); 28 | 29 | return ComponentsArray; 30 | }; 31 | 32 | Content.propTypes = { 33 | json: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 34 | contentType: PropTypes.string, 35 | text: PropTypes.string, 36 | }; 37 | 38 | const Status = ({ statusCode }) => ; 39 | Status.propTypes = { 40 | statusCode: PropTypes.number, 41 | }; 42 | 43 | const Redirect = ({ path, statusCode }) => ( 44 | 45 | ); 46 | 47 | Redirect.propTypes = { 48 | path: PropTypes.string.isRequired, 49 | statusCode: PropTypes.number, 50 | }; 51 | 52 | const SendFile = ({ path, options, onError = () => {} }) => ( 53 | 54 | ); 55 | 56 | SendFile.propTypes = { 57 | path: PropTypes.string.isRequired, 58 | options: PropTypes.object, 59 | onError: PropTypes.func, 60 | }; 61 | 62 | export const Res = { 63 | Header, 64 | Render, 65 | Content, 66 | Status, 67 | Redirect, 68 | SendFile, 69 | Faker, 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const Router = ({ path, caseSensitive, mergeParams, strict, children }) => ( 5 | 11 | {children} 12 | 13 | ); 14 | 15 | Router.propTypes = { 16 | path: PropTypes.string.isRequired, 17 | caseSensitive: PropTypes.bool, 18 | mergeParams: PropTypes.bool, 19 | strict: PropTypes.bool, 20 | children: PropTypes.node, 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Res } from './Res'; 4 | 5 | const BaseRoute = (method) => { 6 | const RouteComponent = ({ children, path, handler, render, status, json, text }) => ( 7 | 8 | {children} 9 | {render && } 10 | {status && } 11 | {text && } 12 | {json && } 13 | 14 | ); 15 | 16 | RouteComponent.propTypes = { 17 | children: PropTypes.node, 18 | path: PropTypes.string, 19 | handler: PropTypes.func, 20 | render: PropTypes.func, 21 | status: PropTypes.number, 22 | json: PropTypes.any, 23 | text: PropTypes.string, 24 | }; 25 | 26 | return RouteComponent; 27 | }; 28 | 29 | export const Get = BaseRoute('get'); 30 | export const Post = BaseRoute('post'); 31 | export const Put = BaseRoute('put'); 32 | export const HeadRoute = BaseRoute('head'); 33 | export const Delete = BaseRoute('delete'); 34 | export const Options = BaseRoute('options'); 35 | export const Trace = BaseRoute('trace'); 36 | export const Copy = BaseRoute('copy'); 37 | export const Lock = BaseRoute('lock'); 38 | export const Mkcol = BaseRoute('mkcol'); 39 | export const Move = BaseRoute('move'); 40 | export const Purge = BaseRoute('purge'); 41 | export const Propfind = BaseRoute('propfind'); 42 | export const Proppatch = BaseRoute('proppatch'); 43 | export const Unlock = BaseRoute('unlock'); 44 | export const Report = BaseRoute('report'); 45 | export const Mkactivity = BaseRoute('mkactivity'); 46 | export const Checkout = BaseRoute('checkout'); 47 | export const Merge = BaseRoute('merge'); 48 | export const Msearch = BaseRoute('m-search'); 49 | export const Notify = BaseRoute('notify'); 50 | export const Subscribe = BaseRoute('subscribe'); 51 | export const Unsubscribe = BaseRoute('unsubscribe'); 52 | export const Patch = BaseRoute('patch'); 53 | export const Search = BaseRoute('search'); 54 | export const Connect = BaseRoute('connect'); 55 | -------------------------------------------------------------------------------- /src/components/Static.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const Static = ({ publicPath, path, options }) => ( 5 | 6 | ); 7 | 8 | Static.propTypes = { 9 | publicPath: PropTypes.string.isRequired, 10 | path: PropTypes.string, 11 | options: PropTypes.object, 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/constants.js: -------------------------------------------------------------------------------- 1 | export const CTYPES = { 2 | app: 'app$', 3 | router: 'router$', 4 | route: 'route$', 5 | middleware: 'middleware$', 6 | param: 'param$', 7 | logger: 'logger$', 8 | static: 'static$', 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './App'; 2 | export * from './Head'; 3 | export * from './Static'; 4 | export * from './Router'; 5 | export * from './Routes'; 6 | export * from './Res'; 7 | export * from './Logger'; 8 | export * from './Middleware'; 9 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const ReqResContext = createContext({ req: null, res: null }); 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './renderer'; 2 | export * from './components'; 3 | export * from './context'; 4 | -------------------------------------------------------------------------------- /src/renderer/generateRoute.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-return-await */ 2 | import React from 'react'; 3 | import { replaceValues } from '../utils/propsUtil'; 4 | import { renderPage } from './renderPage'; 5 | import { log } from './helpers'; 6 | import { Error } from '../components/Error'; 7 | 8 | async function paramfn(sq, req, res, next, options) { 9 | // eslint-disable-next-line no-restricted-syntax 10 | for (const param of sq) { 11 | switch (param.type) { 12 | case 'header': 13 | res.setHeader(param.content.name, param.content.value); 14 | break; 15 | case 'json': 16 | res.send(param.content); 17 | break; 18 | case 'text': 19 | res.send(replaceValues(req, param.content)); 20 | break; 21 | case 'status': 22 | res.statusCode = param.content; 23 | break; 24 | case 'contentType': 25 | res.setHeader('Content-Type', param.content); 26 | break; 27 | case 'redirect': 28 | if (param.content.statusCode) res.redirect(param.content.statusCode, param.content.path); 29 | else res.redirect(param.content.path); 30 | // res.end(); 31 | break; 32 | case 'render': 33 | // eslint-disable-next-line no-await-in-loop 34 | res.send(await renderPage(param.content, { req, res }, options)); 35 | break; 36 | case 'send-file': 37 | res.sendFile(param.content.path, param.content.options, (err) => { 38 | if (err) { 39 | param.content.onError(err); 40 | next(); 41 | } 42 | }); 43 | break; 44 | default: 45 | } 46 | } 47 | } 48 | 49 | export function generateRoute(router, props, options = {}) { 50 | router[props.method]( 51 | props.path || '/', 52 | ...[ 53 | ...(props.middlewares || []), 54 | async (req, res, next) => { 55 | if (props.handler) 56 | try { 57 | await props.handler( 58 | req, 59 | res, 60 | next, 61 | async (Component) => await renderPage(Component, { req, res }, options) 62 | ); 63 | } catch (error) { 64 | const msg = `Error in the handler passed to this route <${props.method[0].toUpperCase()}${props.method.slice( 65 | 1 66 | )} path="${props.path || '/'}">`; 67 | 68 | log('error', msg); 69 | 70 | res.writableEnded = true; 71 | res.statusCode = 500; 72 | if (props.method === 'get') { 73 | res.end( 74 | await renderPage(() => , { req, res }, options) 75 | ); 76 | } else { 77 | res.end(msg); 78 | } 79 | } 80 | 81 | if (props.paramsSeq && !res.writableEnded) { 82 | await paramfn(props.paramsSeq, req, res, next, options); 83 | } 84 | }, 85 | ] 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/helpers.js: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | reset: '\x1b[0m', 3 | bright: '\x1b[1m', 4 | dim: '\x1b[2m', 5 | underscore: '\x1b[4m', 6 | blink: '\x1b[5m', 7 | reverse: '\x1b[7m', 8 | hidden: '\x1b[8m', 9 | 10 | fg: { 11 | black: '\x1b[30m', 12 | red: '\x1b[31m', 13 | green: '\x1b[32m', 14 | yellow: '\x1b[33m', 15 | blue: '\x1b[34m', 16 | magenta: '\x1b[35m', 17 | cyan: '\x1b[36m', 18 | white: '\x1b[37m', 19 | crimson: '\x1b[38m', // Scarlet 20 | }, 21 | bg: { 22 | black: '\x1b[40m', 23 | red: '\x1b[41m', 24 | green: '\x1b[42m', 25 | yellow: '\x1b[43m', 26 | blue: '\x1b[44m', 27 | magenta: '\x1b[45m', 28 | cyan: '\x1b[46m', 29 | white: '\x1b[47m', 30 | crimson: '\x1b[48m', 31 | }, 32 | }; 33 | 34 | const libName = '⚡️ reactend'; 35 | 36 | export function log(type, msg) { 37 | switch (type) { 38 | case 'success': 39 | console.log(`${colors.fg.green}${colors.bright}[${libName}] ${msg}${colors.reset}`); 40 | break; 41 | case 'warn': 42 | console.log(`${colors.fg.yellow}[${libName}] ${msg}${colors.reset}`); 43 | break; 44 | case 'error': 45 | console.log(`${colors.fg.red}[${libName}] ${msg}${colors.reset}`); 46 | break; 47 | default: 48 | console.log(`[${libName}] ${msg}`); 49 | break; 50 | } 51 | } 52 | 53 | export const METHODS = [ 54 | 'get', 55 | 'post', 56 | 'put', 57 | 'head', 58 | 'delete', 59 | 'options', 60 | 'trace', 61 | 'copy', 62 | 'lock', 63 | 'mkcol', 64 | 'move', 65 | 'purge', 66 | 'propfind', 67 | 'proppatch', 68 | 'unlock', 69 | 'report', 70 | 'mkactivity', 71 | 'checkout', 72 | 'merge', 73 | 'm-search', 74 | 'notify', 75 | 'subscribe', 76 | 'unsubscribe', 77 | 'patch', 78 | 'search', 79 | 'connect', 80 | ]; 81 | -------------------------------------------------------------------------------- /src/renderer/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable no-useless-return */ 3 | /* eslint-disable no-unused-vars */ 4 | import React from 'react'; 5 | import ReactReconciler from 'react-reconciler'; 6 | import express from 'express'; 7 | import cookieParser from 'cookie-parser'; 8 | import compression from 'compression'; 9 | import logger from 'morgan'; 10 | 11 | import { log } from './helpers'; 12 | import { generateRoute } from './generateRoute'; 13 | import { renderHTML } from './renderHTML'; 14 | import { CTYPES } from '../components/constants'; 15 | 16 | let options = { 17 | appHOC: (Component) => , 18 | renderHTML, 19 | }; 20 | 21 | const reconciler = ReactReconciler({ 22 | getRootHostContext(rootContainerInstance) {}, 23 | getChildHostContext(parentHostContext, type, rootContainerInstance) {}, 24 | getPublicInstance(instance) {}, 25 | prepareForCommit(containerInfo) {}, 26 | resetAfterCommit(containerInfo) {}, 27 | createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) { 28 | if (type === CTYPES.app) { 29 | const app = express(); 30 | app.use(compression()); 31 | app.use(express.json()); 32 | app.use(express.urlencoded({ extended: true })); 33 | app.use(cookieParser()); 34 | app.listen(props.port || 8080, () => 35 | log('success', `app is running on ${props.port || 8080}`) 36 | ); 37 | return app; 38 | } 39 | 40 | if (type === CTYPES.router) { 41 | const router = express.Router({ 42 | caseSensitive: !!props.caseSensitive, 43 | mergeParams: !!props.mergeParams, 44 | strict: !!props.strict, 45 | }); 46 | 47 | return { routerInstance: router, path: props.path }; 48 | } 49 | 50 | if (type === CTYPES.route) { 51 | const paramsSeq = []; 52 | const middlewares = []; 53 | 54 | return { 55 | type, 56 | props: { ...props, paramsSeq, middlewares }, 57 | }; 58 | } 59 | 60 | if (type === CTYPES.middleware) { 61 | return { 62 | type, 63 | props, 64 | }; 65 | } 66 | 67 | if (type === CTYPES.param) { 68 | return { 69 | type, 70 | props, 71 | }; 72 | } 73 | 74 | if (type === CTYPES.static) { 75 | return { 76 | path: props.path, 77 | static: express.static(props.publicPath, props.options), 78 | }; 79 | } 80 | 81 | // eslint-disable-next-line react/destructuring-assignment 82 | if (type === CTYPES.logger) { 83 | return { 84 | type, 85 | props, 86 | }; 87 | } 88 | 89 | return null; 90 | }, 91 | 92 | appendInitialChild(parentInstance, child) { 93 | if (child.routerInstance) { 94 | if (parentInstance.routerInstance) { 95 | parentInstance.routerInstance.use(child.path || '/', child.routerInstance); 96 | } else { 97 | parentInstance.use(child.path || '/', child.routerInstance); 98 | } 99 | return; 100 | } 101 | 102 | if (child.type === CTYPES.route) { 103 | generateRoute(parentInstance.routerInstance, child.props, options); 104 | return; 105 | } 106 | 107 | if (child.type === CTYPES.middleware) { 108 | if (parentInstance.routerInstance) parentInstance.routerInstance.use(child.props.handler); 109 | if (parentInstance) { 110 | parentInstance.props.middlewares.push(child.props.handler); 111 | } 112 | return; 113 | } 114 | 115 | if (child.type === CTYPES.param) { 116 | parentInstance.props.paramsSeq.push(child.props); 117 | } 118 | 119 | if (child.static) { 120 | parentInstance.use(...(child.path ? [child.path, child.static] : [child.static])); 121 | return; 122 | } 123 | 124 | if (child.type === CTYPES.logger) { 125 | if (!child.props.disabled) { 126 | parentInstance.use(logger(child.props.mode)); 127 | } 128 | return; 129 | } 130 | }, 131 | 132 | finalizeInitialChildren(instance, type, props, rootContainerInstance, hostContext) {}, 133 | prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, hostContext) {}, 134 | shouldSetTextContent(type, props) {}, 135 | shouldDeprioritizeSubtree(type, props) {}, 136 | createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) { 137 | return text; 138 | }, 139 | 140 | now: null, 141 | 142 | isPrimaryRenderer: true, 143 | scheduleDeferredCallback: '', 144 | cancelDeferredCallback: '', 145 | 146 | supportsMutation: true, 147 | 148 | commitMount(instance, type, newProps, internalInstanceHandle) {}, 149 | commitUpdate(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {}, 150 | resetTextContent(instance) {}, 151 | commitTextUpdate(textInstance, oldText, newText) {}, 152 | appendChild(parentInstance, child) {}, 153 | appendChildToContainer(container, child) { 154 | if (child.type === CTYPES.app) { 155 | container = child; 156 | } 157 | }, 158 | insertBefore(parentInstance, child, beforeChild) {}, 159 | insertInContainerBefore(container, child, beforeChild) {}, 160 | removeChild(parentInstance, child) {}, 161 | removeChildFromContainer(container, child) {}, 162 | clearContainer(container, child) {}, 163 | }); 164 | 165 | export const registerApp = (App, custom = options) => { 166 | options = { 167 | ...options, 168 | ...custom, 169 | }; 170 | log('success', `starting...`); 171 | const container = reconciler.createContainer(null, false, false); 172 | reconciler.updateContainer(, container, null, null); 173 | }; 174 | -------------------------------------------------------------------------------- /src/renderer/renderHTML.js: -------------------------------------------------------------------------------- 1 | export const renderHTML = ({ head, styles, root }) => 2 | ` 3 | 4 | 5 | 6 | 7 | ${head} 8 | ${styles} 9 | 10 | 11 |
12 | ${root} 13 |
14 | 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /src/renderer/renderPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | import { Head } from '../components'; 5 | import { Error } from '../components/Error'; 6 | 7 | import { ReqResContext } from '../context'; 8 | import { log } from './helpers'; 9 | 10 | export async function renderPage(Component, ctx, options) { 11 | try { 12 | const { appHOC, renderHTML } = options; 13 | const PrenderedComponent = await Component({ ctx }); 14 | 15 | const sheet = new ServerStyleSheet(); 16 | const root = renderToString( 17 | sheet.collectStyles( 18 | 19 | {appHOC(() => PrenderedComponent)} 20 | 21 | ) 22 | ); 23 | 24 | const styles = sheet.getStyleTags(); 25 | const helmet = Head.renderStatic(); 26 | const head = [helmet.title, helmet.meta, helmet.link].map((h) => h.toString()).join('\n'); 27 | 28 | return renderHTML({ head, styles, root }); 29 | } catch (error) { 30 | const msg = `Error while rendering React DOM Component at "${ctx.req.originalUrl}" path`; 31 | log('error', msg); 32 | 33 | ctx.res.end(await renderPage(() => , ctx, options)); 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | export function get(object, keys, defaultVal) { 2 | keys = Array.isArray(keys) ? keys : keys.split('.'); 3 | object = object[keys[0]]; 4 | if (object && keys.length > 1) { 5 | return get(object, keys.slice(1)); 6 | } 7 | return object === undefined ? defaultVal : object; 8 | } 9 | 10 | // eslint-disable-next-line consistent-return 11 | export function set(object, keys, val) { 12 | keys = Array.isArray(keys) ? keys : keys.split('.'); 13 | if (keys.length > 1) { 14 | object[keys[0]] = object[keys[0]] || {}; 15 | return set(object[keys[0]], keys.slice(1), val); 16 | } 17 | object[keys[0]] = val; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/fakerUtil.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { get } from './common'; 3 | 4 | function getFakerMethod(methodPath) { 5 | try { 6 | const method = get(faker, methodPath); 7 | if (typeof method === 'function') { 8 | return method(); 9 | } 10 | return methodPath; 11 | } catch (error) { 12 | return methodPath; 13 | } 14 | } 15 | 16 | /** 17 | * @param {Object} objMap 18 | * @param {'az' | 'cz' | 'de' | 'de_AT' | 'de_CH' | 'en' | 'en_AU' | 'en_AU_ocker' | 'en_BORK' | 'en_CA' | 'en_GB' | 'en_IE' | 'en_IND' | 'en_US' | 'en_ZA' | 'es' | 'es_MX' | 'fa' | 'fi' | 'fr' | 'fr_CA' | 'fr_CH' | 'ge' | 'hy' | 'hr' | 'id_ID' | 'it' | 'ja' | 'ko' | 'nb_NO' | 'ne' | 'nl' | 'nl_BE' | 'pl' | 'pt_BR' | 'pt_PT' | 'ro' | 'ru' | 'sk' | 'sv' | 'tr' | 'uk' | 'vi' | 'zh_CN' | 'zh_TW'} locale 19 | */ 20 | export function transformFakerMap(objMap, locale = 'en') { 21 | const jsonOutput = JSON.parse(JSON.stringify(objMap)); 22 | faker.setLocale(locale); 23 | 24 | function assign(obj) { 25 | if (Array.isArray(obj) && obj.every((n) => typeof n !== 'string')) 26 | obj.forEach((o) => assign(o)); 27 | else if (Array.isArray(obj) && obj.every((n) => typeof n === 'string')) { 28 | for (let i = 0; i < obj.length; i++) { 29 | obj[i] = getFakerMethod(obj[i]); 30 | } 31 | } else { 32 | for (const prop in obj) { 33 | if (typeof obj[prop] === 'string' && Number.isNaN(+prop)) { 34 | obj[prop] = getFakerMethod(obj[prop]); 35 | } else if (typeof obj[prop] === 'string' && !Number.isNaN(+prop)) { 36 | obj[+prop] = getFakerMethod(obj[+prop]); 37 | } else if (Array.isArray(obj[prop]) && obj[prop].every((n) => typeof n !== 'string')) { 38 | obj[prop].forEach((o) => assign(o)); 39 | } else if (prop in obj) { 40 | assign(obj[prop]); 41 | } 42 | } 43 | } 44 | } 45 | 46 | assign(jsonOutput); 47 | return jsonOutput; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/propsUtil.js: -------------------------------------------------------------------------------- 1 | const PREFIXES = { 2 | param: '$param.', 3 | query: '$query.', 4 | body: '$body.', 5 | }; 6 | 7 | export function getFromParams(variable, params) { 8 | if (typeof variable === 'string' && variable.indexOf(PREFIXES.param) === 0) { 9 | const [, parsedVariable] = variable.split(PREFIXES.param); 10 | if (parsedVariable in params) return params[parsedVariable]; 11 | } 12 | return null; 13 | } 14 | 15 | export function getFromQuery(variable, query) { 16 | if (typeof variable === 'string' && variable.indexOf(PREFIXES.query) === 0) { 17 | const [, parsedVariable] = variable.split(PREFIXES.query); 18 | if (parsedVariable in query) return query[parsedVariable]; 19 | } 20 | 21 | return null; 22 | } 23 | 24 | export function getFromBody(variable, body) { 25 | if (typeof variable === 'string' && variable.indexOf(PREFIXES.body) === 0) { 26 | const [, parsedVariable] = variable.split(PREFIXES.body); 27 | if (parsedVariable in body) return body[parsedVariable]; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | export function passValues(req, input) { 34 | const obj = { ...input }; 35 | 36 | for (const prop in obj) { 37 | const paramResult = getFromParams(obj[prop], req.params); 38 | const queryResult = getFromQuery(obj[prop], req.query); 39 | 40 | if (paramResult) obj[prop] = paramResult; 41 | else if (queryResult) obj[prop] = queryResult; 42 | else obj[prop] = undefined; 43 | } 44 | 45 | return obj; 46 | } 47 | 48 | export function replaceValues(req, text) { 49 | return text.replace(/(? { 50 | const paramResult = getFromParams(value, req.params); 51 | const queryResult = getFromQuery(value, req.query); 52 | 53 | if (paramResult) return paramResult; 54 | if (queryResult) return queryResult; 55 | return value; 56 | }); 57 | } 58 | --------------------------------------------------------------------------------