├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .huskyrc.json ├── .nvmrc ├── .prettierrc.json ├── .yarnrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── babel.server.config.js ├── bin └── voltran.js ├── browserslist ├── changelog-template.hbs ├── config ├── string.js └── styles.js ├── fileMock.js ├── jest.config.js ├── jsconfig.json ├── lib ├── cli.js ├── config.js ├── os.js └── tools │ └── prom.js ├── package.json ├── postcss.config.js ├── setupTests.js ├── src ├── api │ └── controllers │ │ └── index.js ├── assets │ ├── .gitkeep │ ├── hepsiburada.png │ ├── hepsitech.png │ └── voltran-logo.png ├── client │ └── client.js ├── index.js ├── main.js ├── metrics.js ├── public │ └── .gitkeep ├── render.js ├── renderMultiple.js ├── server.js ├── tools │ ├── bundle.js │ ├── clean.js │ ├── lib │ │ └── fs.js │ ├── run.js │ ├── start.js │ └── task.js └── universal │ ├── common │ └── network │ │ └── apiUtils.js │ ├── components │ ├── App.js │ ├── ClientApp.js │ ├── Html.js │ ├── Preview.js │ ├── PureHtml.js │ └── route │ │ └── HbRoute.js │ ├── core │ ├── api │ │ ├── ApiManager.js │ │ ├── ClientApiManager.js │ │ ├── ClientApiManagerCache.js │ │ ├── ServerApiManager.js │ │ └── ServerApiManagerCache.js │ ├── cache │ │ ├── cacheManager.js │ │ └── cacheUtils.js │ ├── react │ │ └── ReactRenderContext.js │ └── route │ │ ├── routeConstants.js │ │ ├── routeUtils.js │ │ └── routesWithComponents.js │ ├── model │ ├── Component.js │ ├── Renderer.js │ └── Request.js │ ├── partials │ ├── Welcome │ │ ├── PartialList.js │ │ ├── Welcome.js │ │ ├── index.js │ │ ├── partials.js │ │ └── styled.js │ └── withBaseComponent.js │ ├── requests │ └── .gitkeep │ ├── routes │ └── routes.js │ ├── service │ └── RenderService.js │ ├── tools │ └── newrelic │ │ └── newrelic.js │ └── utils │ ├── baseRenderHtml.js │ ├── constants.js │ ├── helper.js │ ├── logger.js │ └── struct.js ├── webpack.client.config.js ├── webpack.common.config.js └── webpack.server.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | # editorconfig-tools is unable to ignore longs strings or urls 20 | max_line_length = null 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/public 3 | src/tools 4 | webpack.client.config.js 5 | webpack.common.config.js 6 | webpack.server.config.js 7 | babel.config.js 8 | babel.server.config.js 9 | postcss.config.js 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: ['airbnb', 'prettier'], 4 | globals: { 5 | hwindow: true, 6 | document: true, 7 | window: true, 8 | hepsiBus: true, 9 | global: true, 10 | jest: true, 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module' 18 | }, 19 | plugins: ['react', 'prettier'], 20 | rules: { 21 | 'prettier/prettier': ['error'], 22 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 23 | 'react/jsx-props-no-spreading': 'off', 24 | 'react/no-array-index-key': 'off', 25 | 'jsx-a11y/control-has-associated-label': 'off', 26 | 'jsx-a11y/anchor-has-content': 'off', 27 | 'jsx-a11y/anchor-is-valid': 'off', 28 | 'jsx-a11y/label-has-associated-control': 'off', 29 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 30 | 'class-methods-use-this': 'warn', 31 | 'import/no-extraneous-dependencies': ['off'], 32 | 'import/no-unresolved': ['off'], 33 | 'import/extensions': ['off'], 34 | 'import/order': ['off'], 35 | 'jsx-a11y/click-events-have-key-events': 'off', 36 | 'react/no-children-prop': 'off', 37 | 'react/no-unescaped-entities': 'off', 38 | 'react/require-default-props': 'off', 39 | 'react/forbid-prop-types': 'off', 40 | 'react/prop-types': 'off', 41 | 'react/jsx-one-expression-per-line': 'off', 42 | 'jsx-a11y/no-static-element-interactions': 'off', 43 | 'no-shadow': 'off', 44 | 'no-use-before-define': 'off', 45 | 'no-unused-expressions': 'off', 46 | 'no-nested-ternary': 'off', 47 | 'no-underscore-dangle': 'off', 48 | 'consistent-return': 'off', 49 | 'array-callback-return': 'off' 50 | }, 51 | env: { 52 | jest: true, 53 | browser: true, 54 | node: true, 55 | es6: true 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.html text eol=lf 11 | *.css text eol=lf 12 | *.less text eol=lf 13 | *.styl text eol=lf 14 | *.scss text eol=lf 15 | *.sass text eol=lf 16 | *.sss text eol=lf 17 | *.js text eol=lf 18 | *.jsx text eol=lf 19 | *.json text eol=lf 20 | *.md text eol=lf 21 | *.mjs text eol=lf 22 | *.sh text eol=lf 23 | *.svg text eol=lf 24 | *.txt text eol=lf 25 | *.xml text eol=lf 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | # Compiled output 4 | build 5 | 6 | # Runtime data 7 | database.sqlite 8 | 9 | # Test coverage 10 | coverage 11 | 12 | # Logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editors and IDEs 18 | .idea 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # Misc 26 | .DS_Store 27 | 28 | src/universal/assets.json 29 | src/universal/appConfig.js 30 | 31 | package-lock.json 32 | yarn.lock 33 | demo 34 | voltran.config.js -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-push": "yarn run" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.7.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.com/" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.10](https://github.com/hepsiburada/VoltranJS/compare/1.1.6...1.1.10) 4 | 5 | ### Commits 6 | 7 | - feat: cors policy added [`b00e710`](https://github.com/hepsiburada/VoltranJS/commit/b00e710a4dea603ccf017da0cf9c18910c6a9e9d) 8 | - feat: preview mode env control [`0999ad8`](https://github.com/hepsiburada/VoltranJS/commit/0999ad8d279f697a3ccf556ad5fe255ec89fadb7) 9 | - feat: update version [`d631a88`](https://github.com/hepsiburada/VoltranJS/commit/d631a885957fb5e0b481b53e42e3a878316be717) 10 | - feat: change cors policy [`216a339`](https://github.com/hepsiburada/VoltranJS/commit/216a339308b05ab25df70f62e6f12c1295c457ca) 11 | 12 | ## [1.1.6](https://github.com/hepsiburada/VoltranJS/compare/1.1.5...1.1.6) - 2023-10-17 13 | 14 | ### Merged 15 | 16 | - feat: update node-sass version [`#53`](https://github.com/hepsiburada/VoltranJS/pull/53) 17 | 18 | ### Commits 19 | 20 | - chore: update changelog [`3211ddf`](https://github.com/hepsiburada/VoltranJS/commit/3211ddfd561c0830e33667ea1f5ab6fff7aa0a7d) 21 | 22 | ## [1.1.5](https://github.com/hepsiburada/VoltranJS/compare/v1.1.5...1.1.5) - 2022-10-13 23 | 24 | ### Commits 25 | 26 | - updated changelog [`b65ad82`](https://github.com/hepsiburada/VoltranJS/commit/b65ad827c15e8e88bc9a05f1c16b5f6cc86bc9ee) 27 | 28 | ## [v1.1.5](https://github.com/hepsiburada/VoltranJS/compare/1.1.4...v1.1.5) - 2022-10-13 29 | 30 | ### Merged 31 | 32 | - feat: some package version updates for vulnerabilities [`#51`](https://github.com/hepsiburada/VoltranJS/pull/51) 33 | 34 | ## [1.1.4](https://github.com/hepsiburada/VoltranJS/compare/v1.1.4...1.1.4) - 2022-10-06 35 | 36 | ### Commits 37 | 38 | - chore: update changelog [`ab5a7b0`](https://github.com/hepsiburada/VoltranJS/commit/ab5a7b0094231bb6a562f9a45e9bd71494d18581) 39 | 40 | ## [v1.1.4](https://github.com/hepsiburada/VoltranJS/compare/1.1.3...v1.1.4) - 2022-10-06 41 | 42 | ### Merged 43 | 44 | - feat(newrelic): version upgrade [`#50`](https://github.com/hepsiburada/VoltranJS/pull/50) 45 | 46 | ## [1.1.3](https://github.com/hepsiburada/VoltranJS/compare/1.1.2...1.1.3) - 2022-07-07 47 | 48 | ### Merged 49 | 50 | - fixed bundle analyze automatically open report in default browser [`#48`](https://github.com/hepsiburada/VoltranJS/pull/48) 51 | 52 | ### Commits 53 | 54 | - chore: update changelog [`41856aa`](https://github.com/hepsiburada/VoltranJS/commit/41856aabc95630a2929482170e3a5e2e599c25fc) 55 | 56 | ## [1.1.2](https://github.com/hepsiburada/VoltranJS/compare/1.1.1...1.1.2) - 2022-06-29 57 | 58 | ### Merged 59 | 60 | - webpack budnle analyze static file added [`#47`](https://github.com/hepsiburada/VoltranJS/pull/47) 61 | 62 | ### Commits 63 | 64 | - chore: update changelog [`41300a5`](https://github.com/hepsiburada/VoltranJS/commit/41300a543d216914e9debe2148b205c5bebbd47f) 65 | 66 | ## [1.1.1](https://github.com/hepsiburada/VoltranJS/compare/1.0.29...1.1.1) - 2022-06-28 67 | 68 | ### Merged 69 | 70 | - Feature/upgrade to webpack5 [`#46`](https://github.com/hepsiburada/VoltranJS/pull/46) 71 | 72 | ### Commits 73 | 74 | - string-replace-loader upgraded to last version [`2840d9e`](https://github.com/hepsiburada/VoltranJS/commit/2840d9ec27b4925d39eb2dfe23d6fda0d163578f) 75 | - Upgrade webpack 5, [`5f4ca9d`](https://github.com/hepsiburada/VoltranJS/commit/5f4ca9dd08e6e116d0c3bd5fdea554edb6acd7aa) 76 | 77 | ## [1.0.29](https://github.com/hepsiburada/VoltranJS/compare/1.0.27...1.0.29) - 2022-04-21 78 | 79 | ### Merged 80 | 81 | - Newrelic transaction for all routes [`#45`](https://github.com/hepsiburada/VoltranJS/pull/45) 82 | 83 | ## [1.0.27](https://github.com/hepsiburada/VoltranJS/compare/1.0.26...1.0.27) - 2022-04-13 84 | 85 | ### Commits 86 | 87 | - Add description of new relic integration on README. Fix importing function problem about newrelic on server.js [`ab7c472`](https://github.com/hepsiburada/VoltranJS/commit/ab7c4720a8c20439afeb7180c1bc10fd73daeab1) 88 | - Update version to 1.0.28 [`38ec211`](https://github.com/hepsiburada/VoltranJS/commit/38ec21196d824e8462a83a38481fd9534ce99aee) 89 | - Fix changelog issue. [`ec94706`](https://github.com/hepsiburada/VoltranJS/commit/ec947068aee79476a7265c405c811651c3ae54f8) 90 | 91 | ## [1.0.26](https://github.com/hepsiburada/VoltranJS/compare/1.0.25...1.0.26) - 2022-04-12 92 | 93 | ### Merged 94 | 95 | - Add error message attribute on newrelic errors. [`#44`](https://github.com/hepsiburada/VoltranJS/pull/44) 96 | - feat: remove headers message on rendered html [`#43`](https://github.com/hepsiburada/VoltranJS/pull/43) 97 | - Reduced the size of the 'Client.js' file [`#39`](https://github.com/hepsiburada/VoltranJS/pull/39) 98 | 99 | ### Commits 100 | 101 | - Add json option on error messages for newrelic [`54416a9`](https://github.com/hepsiburada/VoltranJS/commit/54416a955a498aea74574ce03fd435ea63b6c7fc) 102 | - Update changelog for 1.0.27 version [`969958e`](https://github.com/hepsiburada/VoltranJS/commit/969958e531ab99fa4d9722eee48773aa89054dd4) 103 | 104 | ## [1.0.25](https://github.com/hepsiburada/VoltranJS/compare/1.0.24...1.0.25) - 2022-02-11 105 | 106 | ### Merged 107 | 108 | - feat: added criticalCss query params feature [`#38`](https://github.com/hepsiburada/VoltranJS/pull/38) 109 | 110 | ## [1.0.24](https://github.com/hepsiburada/VoltranJS/compare/1.0.23...1.0.24) - 2022-02-07 111 | 112 | ### Merged 113 | 114 | - Fix: Lodash dep. removed and replaced with native functions [`#36`](https://github.com/hepsiburada/VoltranJS/pull/36) 115 | 116 | ### Commits 117 | 118 | - - Lodash changed with native functions. [`3f12087`](https://github.com/hepsiburada/VoltranJS/commit/3f12087204c109b5da74661ad1d62bda74b312b7) 119 | - chore: update changelog [`15d0b1e`](https://github.com/hepsiburada/VoltranJS/commit/15d0b1ec8a6132d61cbdbac276c6a753ac489123) 120 | - version upgrade [`2049fd5`](https://github.com/hepsiburada/VoltranJS/commit/2049fd58b9a71c6fdf87c728db5fd950d689ece2) 121 | 122 | ## [1.0.23](https://github.com/hepsiburada/VoltranJS/compare/1.0.22...1.0.23) - 2022-01-04 123 | 124 | ### Merged 125 | 126 | - Css files with array for react lazy and suspense usage [`#34`](https://github.com/hepsiburada/VoltranJS/pull/34) 127 | 128 | ### Commits 129 | 130 | - Code refactor [`416b52b`](https://github.com/hepsiburada/VoltranJS/commit/416b52bf320737d4de854d694e658a4db3c253cb) 131 | - Css files with array [`1c60f25`](https://github.com/hepsiburada/VoltranJS/commit/1c60f250f025b6cea774d83077819f5255c960aa) 132 | - Version upgrade [`6699d87`](https://github.com/hepsiburada/VoltranJS/commit/6699d8745305493df5eb543deb1d122c4b4f3169) 133 | 134 | ## [1.0.22](https://github.com/hepsiburada/VoltranJS/compare/1.0.21...1.0.22) - 2021-11-16 135 | 136 | ### Merged 137 | 138 | - fix: welcome page reverted [`#31`](https://github.com/hepsiburada/VoltranJS/pull/31) 139 | 140 | ## [1.0.21](https://github.com/hepsiburada/VoltranJS/compare/1.0.20...1.0.21) - 2021-11-11 141 | 142 | ### Merged 143 | 144 | - feat: removed user agent on initial state [`#30`](https://github.com/hepsiburada/VoltranJS/pull/30) 145 | 146 | ### Commits 147 | 148 | - feat: updated voltran version [`a44c5e3`](https://github.com/hepsiburada/VoltranJS/commit/a44c5e31d129913b40a545e513c0b060fc25275c) 149 | 150 | ## [1.0.20](https://github.com/hepsiburada/VoltranJS/compare/1.0.19...1.0.20) - 2021-11-11 151 | 152 | ### Merged 153 | 154 | - feat: removed user agent on initial state [`#30`](https://github.com/hepsiburada/VoltranJS/pull/30) 155 | 156 | ## [1.0.19](https://github.com/hepsiburada/VoltranJS/compare/1.0.18...1.0.19) - 2021-10-27 157 | 158 | ### Merged 159 | 160 | - feat:welcome page is hidden in production environment [`#28`](https://github.com/hepsiburada/VoltranJS/pull/28) 161 | 162 | ### Commits 163 | 164 | - chore: version updated [`70cf0ed`](https://github.com/hepsiburada/VoltranJS/commit/70cf0ed1f87bd6cdfefad7de3d52aa2df397b72b) 165 | 166 | ## [1.0.18](https://github.com/hepsiburada/VoltranJS/compare/1.0.17...1.0.18) - 2021-09-08 167 | 168 | ### Merged 169 | 170 | - fixed regenerator-runtime [`#26`](https://github.com/hepsiburada/VoltranJS/pull/26) 171 | 172 | ### Commits 173 | 174 | - chore: update changelog [`4b1cd93`](https://github.com/hepsiburada/VoltranJS/commit/4b1cd93bf29c0c1ec831b435599f3bb5bb15c8ff) 175 | 176 | ## [1.0.17](https://github.com/hepsiburada/VoltranJS/compare/1.0.16...1.0.17) - 2021-09-06 177 | 178 | ### Merged 179 | 180 | - pass headers in fragments [`#27`](https://github.com/hepsiburada/VoltranJS/pull/27) 181 | 182 | ### Commits 183 | 184 | - chore: update changelog [`3a6f319`](https://github.com/hepsiburada/VoltranJS/commit/3a6f3196100385e25b8d08085b4f5e034564db56) 185 | 186 | ## [1.0.16](https://github.com/hepsiburada/VoltranJS/compare/v1.0.16...1.0.16) - 2021-08-23 187 | 188 | ### Commits 189 | 190 | - Update changelog [`717c909`](https://github.com/hepsiburada/VoltranJS/commit/717c9090983e84d29346a43ccdaf4d3fdc6c858d) 191 | 192 | ## [v1.0.16](https://github.com/hepsiburada/VoltranJS/compare/1.0.13...v1.0.16) - 2021-08-23 193 | 194 | ### Merged 195 | 196 | - set-cookie header imported to cookies [`#25`](https://github.com/hepsiburada/VoltranJS/pull/25) 197 | - Undefined SassResources bug [`#23`](https://github.com/hepsiburada/VoltranJS/pull/23) 198 | 199 | ### Commits 200 | 201 | - voltranConfig.sassResources undefined bug fixed. [`acb50ef`](https://github.com/hepsiburada/VoltranJS/commit/acb50efb72227a759663eef15be48afc4b26e132) 202 | - chore: update changelog [`f3501ad`](https://github.com/hepsiburada/VoltranJS/commit/f3501ad944fe8f186acdce498e82ec131e09ff51) 203 | - Version upgrade [`b4aad6f`](https://github.com/hepsiburada/VoltranJS/commit/b4aad6f1cbc30c22c547529e9f86bd267f0559b1) 204 | 205 | ## [1.0.13](https://github.com/hepsiburada/VoltranJS/compare/v1.0.13...1.0.13) - 2021-08-03 206 | 207 | ### Commits 208 | 209 | - chore: update changelog [`94a3f7a`](https://github.com/hepsiburada/VoltranJS/commit/94a3f7a0e76363fd6fc7fb22bd80dd5f34b228d9) 210 | 211 | ## [v1.0.13](https://github.com/hepsiburada/VoltranJS/compare/1.0.12...v1.0.13) - 2021-08-03 212 | 213 | ### Merged 214 | 215 | - sass-resources-loader implementation. [`#22`](https://github.com/hepsiburada/VoltranJS/pull/22) 216 | 217 | ### Commits 218 | 219 | - sass-resource-loader implementation is done. Updated read me according to new config. [`cc97ef5`](https://github.com/hepsiburada/VoltranJS/commit/cc97ef506b03be4db82c0044b12301a22eb97085) 220 | 221 | ## [1.0.12](https://github.com/hepsiburada/VoltranJS/compare/1.0.11...1.0.12) - 2021-07-30 222 | 223 | ### Merged 224 | 225 | - VoltranJS serverConfig file fixed. [`#21`](https://github.com/hepsiburada/VoltranJS/pull/21) 226 | 227 | ### Commits 228 | 229 | - chore: update changelog [`19134e7`](https://github.com/hepsiburada/VoltranJS/commit/19134e71989fe79328c8157b7ceb46bbc9afb7ef) 230 | 231 | ## [1.0.11](https://github.com/hepsiburada/VoltranJS/compare/1.0.10...1.0.11) - 2021-06-08 232 | 233 | ### Merged 234 | 235 | - feature: added without state parameter [`#20`](https://github.com/hepsiburada/VoltranJS/pull/20) 236 | - feat: jest library setup [`#19`](https://github.com/hepsiburada/VoltranJS/pull/19) 237 | 238 | ### Commits 239 | 240 | - Create CODE_OF_CONDUCT.md [`6da53eb`](https://github.com/hepsiburada/VoltranJS/commit/6da53ebcf849a8460619e77173cda485b6267c89) 241 | - Update issue templates [`6d1d122`](https://github.com/hepsiburada/VoltranJS/commit/6d1d122c790a1a1fe5f66f6c1f2c337ced793e16) 242 | - chore: update changelog [`05955c3`](https://github.com/hepsiburada/VoltranJS/commit/05955c343471d27e797c472c09811410d56fdb7d) 243 | 244 | ## [1.0.10](https://github.com/hepsiburada/VoltranJS/compare/1.0.9...1.0.10) - 2021-05-27 245 | 246 | ### Merged 247 | 248 | - feat: add changelog generator service [`#18`](https://github.com/hepsiburada/VoltranJS/pull/18) 249 | - use @babel/polyfill/noConflict instead @babel/polyfill [`#16`](https://github.com/hepsiburada/VoltranJS/pull/16) 250 | 251 | ### Commits 252 | 253 | - chore: add changelog [`b8662b0`](https://github.com/hepsiburada/VoltranJS/commit/b8662b05673537883ba45576cf1e2d357bb0e5e0) 254 | 255 | ## [1.0.9](https://github.com/hepsiburada/VoltranJS/compare/1.0.8...1.0.9) - 2021-04-29 256 | 257 | ## [1.0.8](https://github.com/hepsiburada/VoltranJS/compare/1.0.7...1.0.8) - 2021-04-29 258 | 259 | ### Merged 260 | 261 | - Promise catch chain fixed [`#15`](https://github.com/hepsiburada/VoltranJS/pull/15) 262 | 263 | ## [1.0.7](https://github.com/hepsiburada/VoltranJS/compare/1.0.6...1.0.7) - 2021-04-28 264 | 265 | ### Merged 266 | 267 | - Partial content server response feature [`#14`](https://github.com/hepsiburada/VoltranJS/pull/14) 268 | 269 | ## [1.0.6](https://github.com/hepsiburada/VoltranJS/compare/1.0.5...1.0.6) - 2021-04-26 270 | 271 | ### Merged 272 | 273 | - Internal cache remove endpoint created, [`#13`](https://github.com/hepsiburada/VoltranJS/pull/13) 274 | 275 | ### Commits 276 | 277 | - Version upgrade 1.0.6 [`6642d19`](https://github.com/hepsiburada/VoltranJS/commit/6642d19e99a8d772d8dff1325e8078c653fc7d29) 278 | 279 | ## [1.0.5](https://github.com/hepsiburada/VoltranJS/compare/1.0.4...1.0.5) - 2021-04-26 280 | 281 | ### Merged 282 | 283 | - Update README.md [`#12`](https://github.com/hepsiburada/VoltranJS/pull/12) 284 | - feat: esbuild loader integration [`#10`](https://github.com/hepsiburada/VoltranJS/pull/10) 285 | 286 | ### Commits 287 | 288 | - fix: conflict resolved [`b7d3893`](https://github.com/hepsiburada/VoltranJS/commit/b7d3893f4cdf5281ed443f49a61ab5eb378fa8ee) 289 | - fix: unintelligible code deleted [`d7bdb80`](https://github.com/hepsiburada/VoltranJS/commit/d7bdb800f378d1aa24d91262ca8add0305b0217d) 290 | - added new project logo [`ff89d80`](https://github.com/hepsiburada/VoltranJS/commit/ff89d80c17b6a159f22f4565c26716b043ac7c80) 291 | 292 | ## [1.0.4](https://github.com/hepsiburada/VoltranJS/compare/1.0.3...1.0.4) - 2021-04-15 293 | 294 | ### Merged 295 | 296 | - fix: client.css passing value problem [`#11`](https://github.com/hepsiburada/VoltranJS/pull/11) 297 | - feat: add close html preview condition to production mode [`#8`](https://github.com/hepsiburada/VoltranJS/pull/8) 298 | - Configured to pass webpack config, from main project [`#9`](https://github.com/hepsiburada/VoltranJS/pull/9) 299 | 300 | ### Commits 301 | 302 | - feat: add comp based isPreview xodndition [`dad29c3`](https://github.com/hepsiburada/VoltranJS/commit/dad29c3b1c6edb6d07af7e697c983be34f3c025e) 303 | - Added parameter for Preview mode. And Fixed some css bugs [`04dec7c`](https://github.com/hepsiburada/VoltranJS/commit/04dec7c4529130400612c4fd1f17292381f48fc1) 304 | 305 | ## [1.0.3](https://github.com/hepsiburada/VoltranJS/compare/1.0.2...1.0.3) - 2021-04-05 306 | 307 | ### Commits 308 | 309 | - Configured to pass webpack config, from main project [`8f6ce41`](https://github.com/hepsiburada/VoltranJS/commit/8f6ce414035fcb1d850a3417f10bacf46735b691) 310 | 311 | ## [1.0.2](https://github.com/hepsiburada/VoltranJS/compare/1.0.1...1.0.2) - 2021-01-20 312 | 313 | ### Merged 314 | 315 | - Eslint fixes [`#6`](https://github.com/hepsiburada/VoltranJS/pull/6) 316 | - Update README [`#1`](https://github.com/hepsiburada/VoltranJS/pull/1) 317 | 318 | ### Commits 319 | 320 | - Add type-hint [`f8838e4`](https://github.com/hepsiburada/VoltranJS/commit/f8838e422e3c08ac563bafe7064937c089c0cb4c) 321 | - Minor typos [`1976fce`](https://github.com/hepsiburada/VoltranJS/commit/1976fce2766cb7c2055c6af56dbd3d4a4dafecd1) 322 | - update version 1.0.2 [`81bee9b`](https://github.com/hepsiburada/VoltranJS/commit/81bee9b1865fbf2c8f1926bd8617c180d0995d14) 323 | 324 | ## [1.0.1](https://github.com/hepsiburada/VoltranJS/compare/1.0.0...1.0.1) - 2021-01-04 325 | 326 | ### Merged 327 | 328 | - Create LICENSE [`#4`](https://github.com/hepsiburada/VoltranJS/pull/4) 329 | 330 | ### Commits 331 | 332 | - Voltran JS 1.0.0 version [`c156f25`](https://github.com/hepsiburada/VoltranJS/commit/c156f254b92cb218a44fad9f0b00cc55213f8096) 333 | - update package json [`afb8d18`](https://github.com/hepsiburada/VoltranJS/commit/afb8d18d540fa9de751bc2ecdcbce5fc7681e7f3) 334 | 335 | ## 1.0.0 - 2020-12-29 336 | 337 | ### Commits 338 | 339 | - Voltran JS 1.0.0 version [`fedab7a`](https://github.com/hepsiburada/VoltranJS/commit/fedab7a0f70772f91278c2cb752e7c91ee09a7e9) 340 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hepsiburada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | voltran.js 4 |
5 | Micro Frontends Framework 6 |
7 |

8 | 9 |

Voltran is a micro frontends framework which is developed by Hepsiburada Technology Team. Micro frontends help cross functional teams to make end-to-end and independent developments and deployments.

10 | 11 |
12 | 13 |

14 | npm version 15 | npm 16 | npm 17 | Twitter Follow 18 |

19 | 20 |

21 | Key Features • 22 | Installation • 23 | Usage • 24 | Configs • 25 | Technology • 26 | Contributing 27 |

28 | 29 | ### Key Features 30 | 31 | You can use Voltran if you need a micro frontend framework that provides following features: 32 | 33 | - Lightweight and fast API 34 | - Serves single and multiple components 35 | - Preview (to visualize components) 36 | - SEO friendly (if needed) 37 | - CSS & SCSS support 38 | - Supports only React (for now) 39 | 40 | ## Installation 41 | 42 | Voltran requires [Node.js](https://nodejs.org/) v10.15.0+ to run. 43 | 44 | Install the Voltran. 45 | 46 | #### Yarn 47 | 48 | ```sh 49 | $ yarn add voltranjs 50 | ``` 51 | 52 | #### Npm 53 | 54 | ```sh 55 | $ npm install voltranjs 56 | ``` 57 | 58 | ## Usage 59 | 60 | This is an example component. 61 | 62 | First of all, you should import `@voltran/core`. 63 | 64 | After that we can write the component's code. 65 | 66 | **HelloWorld.js** 67 | 68 | ```jsx 69 | const voltran = require('@voltran/core'); 70 | 71 | import React from 'react'; 72 | 73 | const ROUTE_PATHS = { 74 | HELLOWORLDPAGE: '/HelloWorld' 75 | }; 76 | 77 | const HelloWorld = ({ initialState }) => { 78 | return <>Hello World!; 79 | }; 80 | 81 | const component = voltran.default.withBaseComponent(HelloWorld, ROUTE_PATHS.HELLOWORLDPAGE); 82 | 83 | export default component; 84 | ``` 85 | 86 | If you want to fetch data from server side, you should add `getInitialState`. 87 | 88 | **./conf/local.config.js** 89 | 90 | ```js 91 | const port = 3578; 92 | 93 | module.exports = { 94 | port: port, 95 | baseUrl: `http://localhost:${port}`, 96 | mediaUrl: '', 97 | services: { 98 | voltranapi: { 99 | clientUrl: 'http://voltran-api.qa.hepsiburada.com', 100 | serverUrl: 'http://voltran-api.qa.hepsiburada.com' 101 | } 102 | }, 103 | timeouts: { 104 | clientApiManager: 20 * 1000, 105 | serverApiManager: 20 * 1000 106 | } 107 | }; 108 | ``` 109 | 110 | **HelloWorld.js** 111 | 112 | ```jsx 113 | 114 | const voltran = require('@voltran/core'); 115 | 116 | import React from 'react'; 117 | import appConfig from '../appConfig'; 118 | 119 | const ROUTE_PATHS = { 120 | HELLOWORLDPAGE: '/HelloWorld', 121 | }; 122 | 123 | const HelloWorld = ({initialState}) => { 124 | HelloWorld.services = [appConfig.services.voltranApi]; 125 | 126 | HelloWorld.getInitialState = (voltranApiClientManager, context) => { 127 | const config = { headers: context.headers }; 128 | const params = {...}; 129 | 130 | return getName({ params }, voltranApiClientManager, config); 131 | }; 132 | 133 | return ( 134 | <> 135 | Hello World. My name is {initialState.name}! 136 | 137 | ); 138 | }; 139 | 140 | const component = voltran.default.withBaseComponent(HelloWorld, ROUTE_PATHS.HELLOWORLDPAGE); 141 | 142 | export default component; 143 | 144 | ``` 145 | 146 | **Output For Preview** 147 | 148 | ``` 149 | Hello World. My Name is Volkan! 150 | ``` 151 | 152 | **Output For Api** 153 | 154 | ``` 155 | { 156 | html: ..., 157 | scripts: [...], 158 | style: [...], 159 | activeComponent: { 160 | resultPath: "/HelloWorld", 161 | componentName: "HelloWorld", 162 | url: "/HelloWorld" 163 | }, 164 | } 165 | ``` 166 | 167 | ## Configs 168 | 169 | Voltran requires following configurations: 170 | 171 | | **Config** | **Type** | 172 | | --------------------------------------------- | -------------------- | 173 | | [appConfigFile](#appConfigFile) | Object | 174 | | [dev](#dev) | Boolean | 175 | | [distFolder](#distFolder) | String | 176 | | [publicDistFolder](#publicDistFolder) | String | 177 | | [inputFolder](#inputFolder) | String \* `required` | 178 | | [monitoring](#monitoring) | Object | 179 | | [port](#port) | Number - String | 180 | | [prefix](#prefix) | String \* `required` | 181 | | [ssr](#ssr) | String | 182 | | [styles](#styles) | Array | 183 | | [output](#output) | Object | 184 | | [staticProps](#staticProps) | Array | 185 | | [routing](#routing) | Object | 186 | | [webpackConfiguration](#webpackConfiguration) | Object | 187 | | [sassResources](#sassResources) | Array | 188 | | [criticalCssDisabled](#criticalCssDisabled) | Boolean | 189 | 190 | #### appConfigFile 191 | 192 | It should contain environment specific configurations (test, production ...). 193 | 194 | ``` 195 | appConfigFile: { 196 | entry: path.resolve(__dirname, './yourConfigFolder/'), 197 | output: { 198 | path: path.resolve(__dirname, './yourOutputFolder/'), 199 | name: 'yourFileName', 200 | } 201 | } 202 | ``` 203 | 204 | #### dev 205 | 206 | Development mode. Set to `true` if you need to debug. 207 | 208 | `Default`: `false` 209 | 210 | #### distFolder 211 | 212 | The path to the folder where bundled scripts will be placed after the build. 213 | 214 | `Default`: `./dist` 215 | 216 | #### publicDistFolder 217 | 218 | The path to the folder where asset files will be placed after the build. 219 | 220 | `Default`: `./dist/assets` 221 | 222 | #### inputFolder 223 | 224 | The path to the folder that contains script files. It's required. 225 | 226 | Passes this config to Babel Loader where it reads all js files under this folder. 227 | 228 | 'Voltran' converts your files to the appropriate format and optimizes them. 229 | 230 | #### monitoring 231 | 232 | For now, only prometheus is supported. 233 | 234 | ``` 235 | monitoring: { 236 | prometheus: false 237 | } 238 | ``` 239 | 240 | > or you can set your custom js file. 241 | 242 | ``` 243 | monitoring: { 244 | prometheus: path.resolve(__dirname, './src/tools/prometheus.js') 245 | } 246 | ``` 247 | 248 | #### port 249 | 250 | `Default`: `3578` 251 | 252 | > If you want to change the port 253 | > you may need to change the port in appConfigFiles 254 | 255 | #### prefix 256 | 257 | `It is required.` 258 | 259 | There may be different components owned by different teams using voltrans on the same page. Voltran needs to use a prefix in order to avoid conflicts issues. 260 | This prefix is prepended to initial states and CSS class names. 261 | 262 | > We recommend that each team use their own acronyms/prefixes. 263 | 264 | #### ssr 265 | 266 | `Default`: `true` 267 | Voltran supports server side rendering. 268 | Applications that need 'SEO' features needs to set this parameter to `true`. 269 | 270 | #### styles 271 | 272 | This field's value should be an array of strings. Array values should be the paths to the global CSS files. 273 | 274 | ``` 275 | styles: [ 276 | path.resolve(__dirname, './some-css-file.scss'), 277 | path.resolve(__dirname, './node_modules/carousel/carousel.css') 278 | ] 279 | ``` 280 | 281 | ### output 282 | 283 | ``` 284 | output: { 285 | client: { 286 | path: path.resolve(__dirname, './build/public/project/assets'), 287 | publicPath: path.resolve(__dirname, './src/assets'), 288 | filename: '[name]-[contenthash].js', 289 | chunkFilename: '[name]-[chunkhash].js' 290 | }, 291 | server: { 292 | path: path.resolve(__dirname, './build/server'), 293 | filename: '[name].js' 294 | }, 295 | }, 296 | ``` 297 | 298 | #### staticProps 299 | 300 | You can pass static props to all components at the same time. 301 | 302 | ``` 303 | staticProps: [ 304 | {'key': value} 305 | ] 306 | ``` 307 | 308 | #### routing 309 | 310 | Voltran need two files to set routing. 311 | 312 | ``` 313 | routing: { 314 | components: path.resolve(__dirname, './src/appRoute/components.js'), 315 | dictionary: path.resolve(__dirname, './src/appRoute/dictionary.js') 316 | } 317 | ``` 318 | 319 | #### criticalCssDisabled 320 | 321 | Set to `false` if don't need to critical styles. 322 | 323 | `Default`: `true` 324 | 325 | ### Example files can be found here: 326 | 327 | - [components.js](https://github.com/hepsiburada/VoltranJS-Starter-Kit/blob/master/src/appRoute/components.js) 328 | - [dictionary.js](https://github.com/hepsiburada/VoltranJS-Starter-Kit/blob/master/src/appRoute/dictionary.js) 329 | 330 | #### webpackConfiguration 331 | 332 | You can add your webpack configuration. They will be merged with the voltran configs. 333 | 334 | You can access the starter kit we created from the [link](https://github.com/hepsiburada/VoltranJS-Starter-Kit). 335 | 336 | #### sassResources 337 | 338 | You can add sass resources to this field as string array. sass-resource-loader gonna inject those files in every sass files so you won't need to import them. 339 | 340 | You can check [sass-resource-loader](https://github.com/shakacode/sass-resources-loader) for usage. 341 | 342 | ## New Relic Integration 343 | 344 | Add `newrelicEnabled: true` on your config. 345 | 346 | If you throw an error like `throw new Error({message: "Service error", code: 500})` from your fragments, Voltran detects the fields and sends each field to New Relic as a custom attribute. These fields appear with `_a` prefix to place in the first of rows on your new relic. 347 | 348 | ## Tech 349 | 350 | Voltran uses a number of open source projects to work properly: 351 | 352 | - [ReactJS] - A JavaScript library for building user interfaces! 353 | - [Webpack] - Module bundler 354 | - [babel] - The compiler for next generation JavaScript. 355 | - [node.js] - evented I/O for the backend 356 | - [hiddie] - fast node.js network app framework (friendly fork of [middie](https://github.com/fastify/middie)) 357 | - [Yarn] - the streaming build system 358 | 359 | ## Contributing 360 | 361 | 1. Fork it! 362 | 2. Create your feature branch: `git checkout -b my-new-feature` 363 | 3. Commit your changes: `git commit -m 'Add some feature'` 364 | 4. Push to the branch: `git push origin my-new-feature` 365 | 5. Submit a pull request 366 | 367 |

368 | hepsitech 369 |

370 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | const env = api.env(); 3 | 4 | const basePlugins = [ 5 | 'babel-plugin-styled-components', 6 | '@babel/syntax-dynamic-import', 7 | '@babel/plugin-syntax-jsx', 8 | '@babel/plugin-proposal-class-properties', 9 | '@babel/plugin-transform-runtime', 10 | '@babel/plugin-proposal-optional-chaining', 11 | '@babel/plugin-proposal-numeric-separator', 12 | '@babel/plugin-proposal-throw-expressions' 13 | ]; 14 | 15 | const basePresets = []; 16 | const presets = [...basePresets]; 17 | const plugins = [...basePlugins]; 18 | 19 | if (env === 'test') { 20 | presets.push([ 21 | '@babel/preset-env', 22 | { 23 | useBuiltIns: 'entry', 24 | corejs: '3.20.2' 25 | } 26 | ]); 27 | } else { 28 | if (env === 'production') { 29 | plugins.push([ 30 | 'transform-react-remove-prop-types', 31 | { 32 | mode: 'remove', 33 | removeImport: true, 34 | additionalLibraries: ['react-immutable-proptypes'] 35 | } 36 | ]); 37 | } else { 38 | plugins.push('react-hot-loader/babel'); 39 | } 40 | 41 | presets.push([ 42 | '@babel/preset-env', 43 | { 44 | useBuiltIns: 'entry', 45 | corejs: '3.20.2', 46 | targets: { 47 | esmodules: true, 48 | ie: '11', 49 | node: 'current' 50 | } 51 | } 52 | ]); 53 | } 54 | 55 | presets.push('@babel/preset-react'); 56 | 57 | return { 58 | presets, 59 | plugins 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /babel.server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | const basePlugins = [ 3 | 'babel-plugin-styled-components', 4 | '@babel/plugin-syntax-jsx', 5 | '@babel/plugin-proposal-class-properties', 6 | '@babel/plugin-transform-runtime', 7 | '@babel/plugin-proposal-numeric-separator', 8 | '@babel/plugin-proposal-throw-expressions', 9 | '@babel/plugin-proposal-optional-chaining' 10 | ]; 11 | 12 | const basePresets = ['@babel/preset-react']; 13 | 14 | const presets = [...basePresets]; 15 | const plugins = [...basePlugins]; 16 | 17 | return { 18 | presets, 19 | plugins 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /bin/voltran.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require = require('esm')(module /*, options*/); 4 | require('../lib/cli').cli(process.argv); -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | >1% 2 | last 4 versions 3 | Firefox ESR 4 | not ie < 9 5 | -------------------------------------------------------------------------------- /changelog-template.hbs: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | {{#each releases}} 4 | {{#if href}} 5 | ## [{{title}}]({{href}}){{#if tag}} - {{isoDate}}{{/if}} 6 | {{else}} 7 | ## {{title}}{{#if tag}} - {{isoDate}}{{/if}} 8 | {{/if}} 9 | 10 | {{#if summary}} 11 | {{summary}} 12 | {{/if}} 13 | 14 | {{#if merges}} 15 | ### Merged 16 | 17 | {{#each merges}} 18 | - {{#if commit.breaking}}**Breaking change:** {{/if}}{{message}} {{#if href}}[`#{{id}}`]({{href}}){{/if}} 19 | {{/each}} 20 | {{/if}} 21 | 22 | {{#if fixes}} 23 | ### Fixed 24 | 25 | {{#each fixes}} 26 | - {{#if commit.breaking}}**Breaking change:** {{/if}}{{commit.subject}}{{#each fixes}} {{#if href}}[`#{{id}}`]({{href}}){{/if}}{{/each}} 27 | {{/each}} 28 | {{/if}} 29 | 30 | {{#commit-list commits heading='### Commits'}} 31 | - {{#if breaking}}**Breaking change:** {{/if}}{{subject}} {{#if href}}[`{{shorthash}}`]({{href}}){{/if}} 32 | {{/commit-list}} 33 | 34 | {{/each}} 35 | -------------------------------------------------------------------------------- /config/string.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const normalizeUrl = require('../lib/os.js'); 4 | const getStyles = require('./styles.js'); 5 | 6 | const voltranConfig = require('../voltran.config'); 7 | 8 | const prometheusFile = voltranConfig.monitoring.prometheus; 9 | 10 | function replaceString() { 11 | const data = [ 12 | { 13 | search: '__V_COMPONENTS__', 14 | replace: normalizeUrl(voltranConfig.routing.components), 15 | flags: 'g' 16 | }, 17 | { 18 | search: '__APP_CONFIG__', 19 | replace: normalizeUrl(`${voltranConfig.appConfigFile.output.path}/${voltranConfig.appConfigFile.output.name}.js`), 20 | flags: 'g' 21 | }, 22 | { 23 | search: '__ASSETS_FILE_PATH__', 24 | replace: normalizeUrl(`${voltranConfig.inputFolder}/assets.json`), 25 | flags: 'g' 26 | }, 27 | { 28 | search: '__V_DICTIONARY__', 29 | replace: normalizeUrl(voltranConfig.routing.dictionary), 30 | flags: 'g' 31 | }, 32 | { 33 | search: '@voltran/core', 34 | replace: normalizeUrl(path.resolve(__dirname, '../src/index')), 35 | flags: 'g' 36 | }, 37 | { 38 | search: '"__V_styles__"', 39 | replace: getStyles() 40 | } 41 | ]; 42 | 43 | data.push({ 44 | search: '__V_PROMETHEUS__', 45 | replace: normalizeUrl(prometheusFile || '../lib/tools/prom.js'), 46 | flags: 'g' 47 | }); 48 | 49 | return data; 50 | } 51 | 52 | module.exports = replaceString; 53 | -------------------------------------------------------------------------------- /config/styles.js: -------------------------------------------------------------------------------- 1 | const normalizeUrl = require('../lib/os.js'); 2 | const voltranConfig = require('../voltran.config'); 3 | 4 | function getStyles () { 5 | let styles = ''; 6 | 7 | for(var i = 0; i < voltranConfig.styles.length; i++) { 8 | const style = normalizeUrl(voltranConfig.styles[i]); 9 | 10 | styles += `require('${style}');`; 11 | } 12 | 13 | return styles; 14 | } 15 | 16 | module.exports = getStyles; -------------------------------------------------------------------------------- /fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // Jest configuration 2 | // https://facebook.github.io/jest/docs/en/configuration.html 3 | module.exports = { 4 | verbose: true, 5 | automock: false, 6 | bail: true, 7 | collectCoverageFrom: [ 8 | 'src/**/*.{js,jsx}', 9 | '!**/node_modules/**', 10 | '!**/vendor/**', 11 | '!src/public/**', 12 | '!src/tools/**' 13 | ], 14 | coverageDirectory: '/coverage', 15 | globals: { 16 | window: true, 17 | __DEV__: true 18 | }, 19 | moduleFileExtensions: ['js', 'json', 'jsx', 'node'], 20 | setupFiles: ['/setupTests.js'], 21 | testPathIgnorePatterns: ['/node_modules/'], 22 | moduleNameMapper: { 23 | '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 24 | '/fileMock.js', 25 | '^.+\\.(css|less|scss)$': 'babel-jest' 26 | }, 27 | snapshotSerializers: ['enzyme-to-json/serializer'] 28 | }; 29 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "allowSyntheticDefaultImports": false, 5 | "experimentalDecorators": true, 6 | "baseUrl": "./" 7 | }, 8 | "exclude": ["node_modules", "dist"], 9 | "eslint.options": { 10 | "configFile": ".eslintrc.js" 11 | }, 12 | "tslint.enable": false 13 | } 14 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | import arg from 'arg'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import clc from "cli-color"; 5 | import { spawn } from 'child_process'; 6 | 7 | import normalizeUrl from './os'; 8 | 9 | import defaultConfigs from './config'; 10 | 11 | function parseArgumentsIntoOptions(rawArgs) { 12 | const args = arg( 13 | { 14 | '--config': String, 15 | '--dev': Boolean, 16 | '--bundle': Boolean, 17 | '--release': Boolean, 18 | '--for-cdn': Boolean, 19 | '--no-bundle': Boolean, 20 | '--analyze': Boolean, 21 | '--port': Number, 22 | '--ssr': Boolean, 23 | }, 24 | { 25 | argv: rawArgs.slice(2), 26 | } 27 | ); 28 | const argsList = removeUnneccesaryValueInObject({ 29 | port: args['--port'], 30 | dev: args['--dev'], 31 | bundle: args['--bundle'], 32 | noBundle: args['--no-bundle'], 33 | analyze: args['--analyze'], 34 | configFile: args['--config'], 35 | ssr: args['--ssr'], 36 | }); 37 | 38 | return argsList; 39 | } 40 | 41 | function getVoltranConfigs(configFile) { 42 | const normalizePath = normalizeUrl(path.resolve(process.cwd())); 43 | const voltranConfigs = require(path.resolve(normalizePath, configFile)); 44 | 45 | return voltranConfigs; 46 | } 47 | 48 | function removeUnneccesaryValueInObject(argsList) { 49 | for (const property in argsList) { 50 | if (argsList[property] === undefined) { 51 | delete argsList[property]; 52 | } 53 | } 54 | 55 | return argsList; 56 | } 57 | 58 | function runDevelopmentMode() { 59 | const run = require('../src/tools/run'); 60 | const start = require('../src/tools/start'); 61 | 62 | run(start); 63 | } 64 | 65 | function runProductionMode(voltranConfigs, onlyBundle) { 66 | const bundle = require('../src/tools/bundle'); 67 | 68 | bundle() 69 | .then((res) => { 70 | console.log(clc.green('Bundle is completed.\n',`File: ${voltranConfigs.distFolder}/server/server.js`)); 71 | 72 | if (!onlyBundle) { 73 | serve(voltranConfigs); 74 | } 75 | }); 76 | } 77 | 78 | function serve(voltranConfigs) { 79 | console.log(clc.green('Project Serve is starting...')); 80 | 81 | const out = spawn('node', [ 82 | '-r', 83 | 'source-map-support/register', 84 | '--max-http-header-size=20480', 85 | `${voltranConfigs.distFolder}/server/server.js` 86 | ], {env: {'NODE_ENV': 'production', ...process.env}}); 87 | 88 | out.stdout.on('data', (data) => { 89 | console.log(data.toString()); 90 | }); 91 | 92 | out.stderr.on('data', (data) => { 93 | console.error(data.toString()); 94 | }); 95 | 96 | out.on('close', (code) => { 97 | console.log(`child process exited with code ${code}`); 98 | }); 99 | } 100 | 101 | function checkRequiredVariables(mergeConfigs) { 102 | if (!mergeConfigs.prefix) { 103 | console.log(clc.red("***ERROR*** - 'prefix' is required")); 104 | console.log(clc.red("Please add 'prefix' value to your config file")); 105 | 106 | return false; 107 | } 108 | 109 | return true; 110 | } 111 | 112 | export function cli(args) { 113 | const argumentList = parseArgumentsIntoOptions(args); 114 | console.log(clc.blue(JSON.stringify(argumentList))); 115 | const voltranConfigs = argumentList.configFile ? getVoltranConfigs(argumentList.configFile) : {}; 116 | const assignedArgsAndVoltranConfigs = Object.assign(voltranConfigs, argumentList); 117 | const mergeAllConfigs = Object.assign(defaultConfigs, assignedArgsAndVoltranConfigs); 118 | const isValid = checkRequiredVariables(mergeAllConfigs); 119 | 120 | if (isValid) { 121 | const createdConfig = `module.exports = ${JSON.stringify(mergeAllConfigs)}`; 122 | 123 | fs.writeFile(path.resolve(__dirname, '../voltran.config.js'), createdConfig, function (err) { 124 | if (err) throw err; 125 | 126 | console.log('File is created successfully.', mergeAllConfigs.dev); 127 | 128 | if (mergeAllConfigs.dev) { 129 | runDevelopmentMode(); 130 | } else { 131 | argumentList.noBundle ? 132 | serve(voltranConfigs) : 133 | runProductionMode(mergeAllConfigs, argumentList.bundle); 134 | } 135 | }); 136 | } else { 137 | return false; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const dirName = path.resolve(__dirname).split('/node_modules')[0]; 3 | 4 | module.exports = { 5 | appConfigFile: { 6 | entry: path.resolve(dirName, '../demo/conf/'), 7 | output: { 8 | path: path.resolve(dirName, '../demo/'), 9 | name: 'appConfig', 10 | } 11 | }, 12 | svgFolder: '', 13 | configFile: path.resolve(dirName, '../demo/voltran-app.config.js'), 14 | dev: false, 15 | distFolder: path.resolve(dirName, '../dist'), 16 | publicDistFolder: path.resolve(dirName, '../dist/assets'), 17 | inputFolder: path.resolve(dirName, '../demo'), 18 | monitoring: { 19 | prometheus: false, 20 | }, 21 | criticalCssDisabled: false, 22 | port: 3578, 23 | prefix: 'sf', 24 | ssr: true, 25 | styles: [], 26 | output: { 27 | client: { 28 | path: path.resolve(dirName, '../dist/assets/project/assets'), 29 | publicPath: path.resolve(dirName, '../demo/assets'), 30 | filename: '[name]-[contenthash].js', 31 | chunkFilename: '[name]-[contenthash].js' 32 | }, 33 | server: { 34 | path: path.resolve(dirName, '../dist/server'), 35 | filename: '[name].js' 36 | }, 37 | }, 38 | staticProps: [], 39 | routing: { 40 | components: path.resolve(dirName, '../demo/appRoute/components.js'), 41 | dictionary: path.resolve(dirName, '../demo/appRoute/dictionary.js'), 42 | }, 43 | svgFolder: '', 44 | webpackConfiguration: { 45 | client: {}, 46 | common: {}, 47 | server: {}, 48 | }, 49 | internalCache: { 50 | defaultInternalCacheMilliseconds: 1000 * 60 * 15, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /lib/os.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function normalizeUrl(url) { 4 | const urlArray = url.split(path.sep); 5 | 6 | return urlArray.join('/'); 7 | } 8 | 9 | module.exports = normalizeUrl; -------------------------------------------------------------------------------- /lib/tools/prom.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voltranjs", 3 | "version": "1.1.11", 4 | "main": "src/index.js", 5 | "author": "Hepsiburada Technology Team", 6 | "bin": { 7 | "voltran": "./bin/voltran.js" 8 | }, 9 | "engines": { 10 | "node": ">= 10.15.0", 11 | "npm": ">=6.4" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/hepsiburada/VoltranJS" 19 | }, 20 | "dependencies": { 21 | "@babel/core": "7.16.10", 22 | "@babel/eslint-parser": "7.16.5", 23 | "@babel/plugin-proposal-class-properties": "7.16.7", 24 | "@babel/plugin-proposal-numeric-separator": "7.16.7", 25 | "@babel/plugin-proposal-optional-chaining": "7.16.7", 26 | "@babel/plugin-proposal-throw-expressions": "7.16.7", 27 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 28 | "@babel/plugin-syntax-jsx": "^7.12.13", 29 | "@babel/plugin-transform-runtime": "7.16.10", 30 | "@babel/preset-env": "7.16.10", 31 | "@babel/preset-react": "7.16.7", 32 | "@babel/runtime": "7.16.7", 33 | "@researchgate/react-intersection-observer": "1.0.3", 34 | "arg": "^4.1.3", 35 | "assets-webpack-plugin": "7.1.1", 36 | "async": "^3.2.0", 37 | "autoprefixer": "9.3.1", 38 | "axios": "0.21.2", 39 | "babel-core": "7.0.0-bridge.0", 40 | "babel-eslint": "10.1.0", 41 | "classnames": "2.2.6", 42 | "clean-webpack-plugin": "4.0.0", 43 | "cli-color": "2.0.2", 44 | "compose-middleware": "5.0.1", 45 | "compression": "^1.7.4", 46 | "cookie-parser": "1.4.3", 47 | "copy-webpack-plugin": "4.5.2", 48 | "core-js": "3.20.3", 49 | "css-loader": "6.5.1", 50 | "css-minimizer-webpack-plugin": "3.4.1", 51 | "eev": "0.1.5", 52 | "esbuild-loader": "2.19.0", 53 | "eslint": "6.1.0", 54 | "eslint-config-airbnb": "18.0.1", 55 | "eslint-config-prettier": "6.3.0", 56 | "eslint-plugin-import": "^2.19.1", 57 | "eslint-plugin-jsx-a11y": "6.2.3", 58 | "eslint-plugin-prettier": "3.1.1", 59 | "eslint-plugin-react": "7.14.3", 60 | "esm": "^3.2.25", 61 | "helmet": "3.21.3", 62 | "hiddie": "^1.0.0", 63 | "husky": "^3.1.0", 64 | "identity-obj-proxy": "3.0.0", 65 | "intersection-observer": "0.7.0", 66 | "js-cookie": "^2.2.1", 67 | "mini-css-extract-plugin": "2.5.2", 68 | "newrelic": "^9.1.0", 69 | "pixrem": "4.0.1", 70 | "pleeease-filters": "4.0.0", 71 | "postcss": "7.0.5", 72 | "postcss-inline-svg": "3.1.1", 73 | "postcss-loader": "3.0.0", 74 | "prettier": "1.18.2", 75 | "prom-client": "12.0.0", 76 | "prop-types": "15.6.2", 77 | "query-string": "6.10.1", 78 | "react": "16.14.0", 79 | "react-dom": "16.14.0", 80 | "react-hot-loader": "4.13.0", 81 | "react-router": "5.1.2", 82 | "react-router-dom": "5.1.2", 83 | "regenerator-runtime": "^0.13.9", 84 | "rimraf": "3.0.2", 85 | "serve-static": "1.14.1", 86 | "source-map-support": "0.5.21", 87 | "string-replace-loader": "3.1.0", 88 | "style-loader": "3.3.4", 89 | "styled-components": "5.1.0", 90 | "svg-url-loader": "7.1.1", 91 | "terser-webpack-plugin": "5.3.1", 92 | "webpack": "5.65.0", 93 | "webpack-bundle-analyzer": "4.5.0", 94 | "webpack-cli": "4.9.1", 95 | "webpack-dev-middleware": "3.7.3", 96 | "webpack-hot-middleware": "2.25.1", 97 | "webpack-hot-server-middleware": "0.6.1", 98 | "webpack-merge": "5.8.0", 99 | "webpack-node-externals": "3.0.0", 100 | "whatwg-fetch": "2.0.4", 101 | "sass": "^1.82.0", 102 | "sass-loader": "^16.0.4", 103 | "xss": "^1.0.8" 104 | }, 105 | "devDependencies": { 106 | "auto-changelog": "^2.2.1", 107 | "babel-jest": "^23.6.0", 108 | "enzyme": "^3.11.0", 109 | "enzyme-adapter-react-16": "^1.15.5", 110 | "enzyme-to-json": "^3.6.1", 111 | "jest": "^26.6.3", 112 | 113 | "sass-resources-loader": "^2.2.5" 114 | }, 115 | "scripts": { 116 | "lint": "npm run eslint && npm run prettier", 117 | "eslint": "eslint 'src/**/*.js' --fix", 118 | "prettier": "prettier --write 'src/**/*.{js,scss}' --config .prettierrc.json", 119 | "start": "voltran --config ./demo/voltran-app.config.js --dev", 120 | "serve": "voltran --config ./demo/voltran-app.config.js", 121 | "test": "jest", 122 | "test:watchAll": "jest --watchAll", 123 | "test:coverage": "jest --coverage", 124 | "test:updateSnapshot": "jest --updateSnapshot", 125 | "version": "auto-changelog" 126 | }, 127 | "auto-changelog": { 128 | "commitLimit": false, 129 | "template": "changelog-template.hbs", 130 | "package": true 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | const postCssFilters = require('pleeease-filters'); 3 | const postCssPixrem = require('pixrem'); 4 | const postCssInlineSvg = require('postcss-inline-svg'); 5 | 6 | const voltranConfig = require('./voltran.config'); 7 | 8 | module.exports = { 9 | plugins() { 10 | return [ 11 | postCssInlineSvg({path: voltranConfig.svgFolder}), 12 | postCssPixrem(), 13 | postCssFilters(), 14 | autoprefixer 15 | ]; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/api/controllers/index.js: -------------------------------------------------------------------------------- 1 | function registerControllers(hiddie) { 2 | hiddie.use('/api/status', (req, res) => { 3 | res.json({ status: true }); 4 | }); 5 | } 6 | 7 | export default registerControllers; 8 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/hepsiburada.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/assets/hepsiburada.png -------------------------------------------------------------------------------- /src/assets/hepsitech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/assets/hepsitech.png -------------------------------------------------------------------------------- /src/assets/voltran-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/assets/voltran-logo.png -------------------------------------------------------------------------------- /src/client/client.js: -------------------------------------------------------------------------------- 1 | import Eev from 'eev'; 2 | 3 | /* eslint-disable-next-line */ 4 | "__V_styles__" 5 | 6 | if (!window.HbEventBus) { 7 | window.HbEventBus = new Eev(); 8 | window.voltran_project_version = process.env.APP_BUILD_VERSION || '1.0.0'; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import withBaseComponent from './universal/partials/withBaseComponent'; 2 | import { SERVICES } from './universal/utils/constants'; 3 | 4 | export default { 5 | withBaseComponent, 6 | SERVICES 7 | }; 8 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import cluster from 'cluster'; 3 | 4 | import logger from './universal/utils/logger'; 5 | import Hiddie from 'hiddie'; 6 | import http from 'http'; 7 | import voltranConfig from '../voltran.config'; 8 | import prom from 'prom-client'; 9 | import {HTTP_STATUS_CODES} from './universal/utils/constants'; 10 | 11 | const enablePrometheus = voltranConfig.monitoring.prometheus; 12 | 13 | function triggerMessageListener(worker) { 14 | worker.on('message', function (message) { 15 | if (message?.options?.forwardAllWorkers) { 16 | sendMessageToAllWorkers(message); 17 | } 18 | }); 19 | } 20 | 21 | function sendMessageToAllWorkers(message) { 22 | Object.keys(cluster.workers).forEach(function (key) { 23 | const worker = cluster.workers[key]; 24 | worker.send({ 25 | msg: message.msg, 26 | }); 27 | }, this); 28 | } 29 | 30 | cluster.on('fork', (worker) => { 31 | triggerMessageListener(worker); 32 | }); 33 | 34 | if (cluster.isMaster) { 35 | for (let i = 0; i < os.cpus().length; i += 1) { 36 | cluster.fork(); 37 | } 38 | 39 | cluster.on('exit', worker => { 40 | logger.error(`Worker ${worker.id} died`); 41 | cluster.fork(); 42 | }); 43 | 44 | if (enablePrometheus) { 45 | const aggregatorRegistry = new prom.AggregatorRegistry(); 46 | const metricsPort = voltranConfig.port + 1; 47 | 48 | // eslint-disable-next-line consistent-return 49 | const hiddie = Hiddie(async (err, req, res) => { 50 | if (req.url === '/metrics' && req.method === 'GET') { 51 | res.setHeader('Content-Type', aggregatorRegistry.contentType); 52 | return res.end(await aggregatorRegistry.clusterMetrics()); 53 | } 54 | res.statusCode = HTTP_STATUS_CODES.NOT_FOUND; 55 | res.end(JSON.stringify({message: 'not found'})); 56 | }); 57 | 58 | http.createServer(hiddie.run).listen(metricsPort, () => { 59 | logger.info( 60 | `Voltran ready on ${voltranConfig.port} with ${ 61 | os.cpus().length 62 | } core, also /metrics ready on ${metricsPort}` 63 | ); 64 | }); 65 | } 66 | } else { 67 | // eslint-disable-next-line global-require 68 | require('./server'); 69 | } 70 | -------------------------------------------------------------------------------- /src/metrics.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import prom from 'prom-client'; 3 | 4 | const suffix = process.env.NODE_ENV === 'production' ? '' : `_${Date.now()}`; 5 | 6 | export default { 7 | fragmentRequestDurationMicroseconds: new prom.Histogram({ 8 | name: `fragment_request_duration_ms${suffix}`, 9 | help: 'Duration of fragment requests in ms', 10 | labelNames: ['name', 'without_html'], 11 | buckets: [5, 25, 50, 75, 100] 12 | }) 13 | }; 14 | -------------------------------------------------------------------------------- /src/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/public/.gitkeep -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import xss from 'xss'; 2 | 3 | import { matchUrlInRouteConfigs } from './universal/core/route/routeUtils'; 4 | import Preview from './universal/components/Preview'; 5 | import { HTTP_STATUS_CODES } from './universal/utils/constants'; 6 | import metrics from './metrics'; 7 | import { 8 | renderComponent, 9 | renderLinksAndScripts, 10 | isPreview, 11 | isWithoutHTML, 12 | isWithoutState 13 | } from './universal/service/RenderService'; 14 | import Component from './universal/model/Component'; 15 | import logger from './universal/utils/logger'; 16 | 17 | const appConfig = require('__APP_CONFIG__'); 18 | 19 | // eslint-disable-next-line consistent-return 20 | export default async (req, res) => { 21 | const isWithoutStateValue = isWithoutState(req.query); 22 | const pathParts = xss(req.path) 23 | .split('/') 24 | .filter(part => part); 25 | const componentPath = `/${pathParts.join('/')}`; 26 | 27 | const routeInfo = matchUrlInRouteConfigs(componentPath); 28 | 29 | if (routeInfo) { 30 | const path = `/${pathParts.slice(1, pathParts.length).join('/')}`; 31 | 32 | const context = { 33 | path: xss(path), 34 | query: JSON.parse(xss(JSON.stringify(req.query))), 35 | cookies: xss(JSON.stringify(req.cookies)), 36 | url: xss(req.url) 37 | .replace(componentPath, '/') 38 | .replace('//', '/'), 39 | userAgent: Buffer.from(req.headers['user-agent'], 'utf-8').toString('base64'), 40 | isWithoutState: isWithoutStateValue 41 | }; 42 | 43 | const component = new Component(routeInfo.path); 44 | 45 | let renderResponse = null; 46 | 47 | try { 48 | renderResponse = await renderComponent(component, context); 49 | } catch (exception) { 50 | logger.exception(exception); 51 | return res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({ 52 | message: exception.message 53 | }); 54 | } 55 | 56 | const { 57 | output, 58 | fullHtml, 59 | links, 60 | scripts, 61 | activeComponent, 62 | componentName, 63 | seoState, 64 | isPreviewQuery, 65 | responseOptions 66 | } = renderResponse; 67 | 68 | const statusCode = responseOptions?.isPartialContent 69 | ? HTTP_STATUS_CODES.PARTIAL_CONTENT 70 | : HTTP_STATUS_CODES.OK; 71 | 72 | if (!isPreview(context.query)) { 73 | const html = renderLinksAndScripts(output, '', ''); 74 | 75 | res.status(statusCode).json({ html, scripts, style: links, activeComponent, seoState }); 76 | 77 | metrics.fragmentRequestDurationMicroseconds 78 | .labels(componentName, isWithoutHTML(context.query) ? '1' : '0') 79 | .observe(Date.now() - res.locals.startEpoch); 80 | } else { 81 | const voltranEnv = appConfig.voltranEnv || 'local'; 82 | const previewEnvControl = voltranEnv !== 'prod' && voltranEnv !== 'production'; 83 | 84 | if (previewEnvControl && isPreviewQuery) { 85 | res.status(statusCode).html(Preview([fullHtml].join('\n'))); 86 | } else { 87 | res 88 | .status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR) 89 | .html('

Aradığınız sayfa bulunamadı...

'); 90 | } 91 | } 92 | } else { 93 | res.status(HTTP_STATUS_CODES.NOT_FOUND).json({ 94 | message: 'not found' 95 | }); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/renderMultiple.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { matchUrlInRouteConfigs } from './universal/core/route/routeUtils'; 3 | import Component from './universal/model/Component'; 4 | import Renderer from './universal/model/Renderer'; 5 | import async from 'async'; 6 | import Preview from './universal/components/Preview'; 7 | import { isPreview, isWithoutHTML } from './universal/service/RenderService'; 8 | import metrics from './metrics'; 9 | import { HTTP_STATUS_CODES } from './universal/utils/constants'; 10 | import logger from './universal/utils/logger'; 11 | 12 | function getRenderer(name, query, cookies, url, path, userAgent) { 13 | const componentPath = Component.getComponentPath(name); 14 | const routeInfo = matchUrlInRouteConfigs(componentPath); 15 | 16 | if (routeInfo) { 17 | const urlWithPath = url.replace('/', path); 18 | 19 | const context = { 20 | path, 21 | query, 22 | cookies, 23 | url: urlWithPath, 24 | userAgent, 25 | }; 26 | 27 | if (Component.isExist(componentPath)) { 28 | return new Renderer(new Component(componentPath), context); 29 | } 30 | 31 | return null; 32 | } 33 | 34 | return null; 35 | } 36 | 37 | function iterateServicesMap(servicesMap, callback) { 38 | Object.getOwnPropertySymbols(servicesMap).forEach(serviceName => { 39 | const endPoints = servicesMap[serviceName]; 40 | 41 | Object.keys(endPoints).forEach(endPointName => { 42 | callback(serviceName, endPointName); 43 | }); 44 | }); 45 | } 46 | 47 | function reduceServicesMap(servicesMap, callback, initialValue) { 48 | return Object.getOwnPropertySymbols(servicesMap).map(serviceName => { 49 | const endPoints = servicesMap[serviceName]; 50 | 51 | return Object.keys(endPoints).reduce((obj, endPointName) => { 52 | return callback(serviceName, endPointName, obj); 53 | }, initialValue); 54 | }); 55 | } 56 | 57 | function getHashes(renderers) { 58 | return renderers 59 | .filter(renderer => renderer.servicesMap) 60 | .reduce((hashes, renderer) => { 61 | iterateServicesMap(renderer.servicesMap, (serviceName, endPointName) => { 62 | const requests = renderer.servicesMap[serviceName][endPointName]; 63 | 64 | requests.forEach(request => { 65 | if (hashes[request.hash]) { 66 | hashes[request.hash].occurrence += 1; 67 | } else { 68 | hashes[request.hash] = { occurrence: 1, score: 0, request }; 69 | } 70 | }); 71 | }); 72 | 73 | return hashes; 74 | }, {}); 75 | } 76 | 77 | function getWinner(requests, hashes) { 78 | return requests.sort((requestA, requestB) => { 79 | const a = requestA.hash; 80 | const b = requestB.hash; 81 | 82 | if (hashes[a].score < hashes[b].score) return 1; 83 | if (hashes[a].score > hashes[b].score) return -1; 84 | 85 | if (hashes[a].occurrence < hashes[b].occurrence) return 1; 86 | if (hashes[a].occurrence > hashes[b].occurrence) return -1; 87 | 88 | return 1; 89 | })[0]; 90 | } 91 | 92 | function incWinnerScore(winner, hashes) { 93 | hashes[winner.hash].score += 1; 94 | } 95 | 96 | function putWinnerMap(serviceName, endPointName, winnerMap, winner) { 97 | if (winnerMap[serviceName]) { 98 | winnerMap[serviceName][endPointName] = winner; 99 | } else { 100 | winnerMap[serviceName] = { [endPointName]: winner }; 101 | } 102 | } 103 | 104 | async function setInitialStates(renderers) { 105 | const hashes = getHashes(renderers); 106 | 107 | const promises = renderers 108 | .filter(renderer => renderer.servicesMap) 109 | .reduce((promises, renderer) => { 110 | iterateServicesMap(renderer.servicesMap, (serviceName, endPointName) => { 111 | const requests = renderer.servicesMap[serviceName][endPointName]; 112 | 113 | const winner = getWinner(requests, hashes); 114 | incWinnerScore(winner, hashes); 115 | putWinnerMap(serviceName, endPointName, renderer.winnerMap, winner); 116 | 117 | if (!promises[winner.hash]) { 118 | promises[winner.hash] = callback => { 119 | winner 120 | .execute() 121 | .then(response => callback(null, response)) 122 | .catch(exception => 123 | callback(new Error(`${winner.uri} : ${exception.message}`), null) 124 | ); 125 | }; 126 | } 127 | }); 128 | 129 | return promises; 130 | }, {}); 131 | 132 | const results = await new Promise((resolve, reject) => { 133 | async.parallel(promises, (err, results) => { 134 | if (err) { 135 | return reject(err); 136 | } 137 | 138 | resolve(results); 139 | }); 140 | }); 141 | 142 | renderers.forEach(renderer => { 143 | if (renderer.winnerMap) { 144 | renderer.setInitialState( 145 | reduceServicesMap( 146 | renderer.winnerMap, 147 | (serviceName, endPointName, obj) => { 148 | const request = renderer.winnerMap[serviceName][endPointName]; 149 | obj[endPointName] = results[request.hash]; 150 | return obj; 151 | }, 152 | {} 153 | ) 154 | ); 155 | } 156 | }); 157 | 158 | return Object.keys(results).length; 159 | } 160 | 161 | async function getResponses(renderers) { 162 | return (await Promise.all(renderers.map(renderer => renderer.render()))) 163 | .filter(result => result.value != null) 164 | .reduce((obj, item) => { 165 | const el = obj; 166 | const name = `${item.key}_${item.id}`; 167 | 168 | if (!el[name]) el[name] = item.value; 169 | 170 | return el; 171 | }, {}); 172 | } 173 | 174 | async function getPreview(responses, requestCount) { 175 | return Preview( 176 | [...Object.keys(responses).map(name => responses[name].fullHtml)].join('\n'), 177 | `${requestCount} request!` 178 | ); 179 | } 180 | 181 | // eslint-disable-next-line consistent-return 182 | export default async (req, res) => { 183 | const renderers = req.params.components 184 | .split(',') 185 | .filter((value, index, self) => self.indexOf(value) === index) 186 | .map(name => 187 | getRenderer( 188 | name, 189 | req.query, 190 | req.cookies, 191 | req.url, 192 | `/${req.params.path || ''}`, 193 | req.headers['user-agent'] 194 | ) 195 | ) 196 | .filter(renderer => renderer != null); 197 | 198 | if (!renderers.length) { 199 | return res.status(HTTP_STATUS_CODES.NOT_FOUND).json({ 200 | message: 'not found' 201 | }); 202 | } 203 | 204 | const componentNames = renderers.map(renderer => renderer.component.name); 205 | 206 | let requestCount = null; 207 | 208 | try { 209 | requestCount = await setInitialStates(renderers); 210 | } catch (exception) { 211 | logger.exception(exception); 212 | return res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({ 213 | message: exception.message 214 | }); 215 | } 216 | 217 | const responses = await getResponses(renderers); 218 | 219 | if (isPreview(req.query)) { 220 | const preview = await getPreview(responses, requestCount); 221 | res.html(preview); 222 | } else { 223 | res.json(responses); 224 | 225 | metrics.fragmentRequestDurationMicroseconds 226 | .labels(componentNames.sort().join(','), isWithoutHTML(req.query) ? '1' : '0') 227 | .observe(Date.now() - res.locals.startEpoch); 228 | } 229 | }; 230 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import newrelic, { addCustomAttrsToNewrelic } from './universal/tools/newrelic/newrelic'; 3 | 4 | import cookieParser from 'cookie-parser'; 5 | import { compose } from 'compose-middleware'; 6 | import compression from 'compression'; 7 | import path from 'path'; 8 | import Hiddie from 'hiddie'; 9 | import http from 'http'; 10 | import serveStatic from 'serve-static'; 11 | import prom from 'prom-client'; 12 | import helmet from 'helmet'; 13 | import url from 'url'; 14 | import xss from 'xss'; 15 | 16 | import Welcome from './universal/partials/Welcome'; 17 | import render from './render'; 18 | import registerControllers from './api/controllers'; 19 | import renderMultiple from './renderMultiple'; 20 | 21 | import { createCacheManagerInstance } from './universal/core/cache/cacheUtils'; 22 | 23 | import { HTTP_STATUS_CODES } from './universal/utils/constants'; 24 | 25 | import voltranConfig from '../voltran.config'; 26 | 27 | const {bundleAnalyzerStaticEnabled} = require('__APP_CONFIG__'); 28 | 29 | const enablePrometheus = voltranConfig.monitoring.prometheus; 30 | let Prometheus; 31 | 32 | if (enablePrometheus) { 33 | // eslint-disable-next-line global-require 34 | Prometheus = require('__V_PROMETHEUS__'); 35 | } 36 | 37 | const fragmentManifest = require('__V_DICTIONARY__'); 38 | 39 | process.on('unhandledRejection', (reason, p) => { 40 | console.error('Unhandled Rejection at:', p, 'reason:', reason); 41 | process.exit(1); 42 | }); 43 | 44 | process.on('message', message => { 45 | handleProcessMessage(message); 46 | }); 47 | 48 | const fragments = []; 49 | 50 | Object.keys(fragmentManifest).forEach(index => { 51 | const fragmentUrl = fragmentManifest[index].path; 52 | const arr = fragmentUrl.split(path.sep); 53 | const name = arr[arr.length - 1]; 54 | fragments.push(name); 55 | }); 56 | 57 | const handleProcessMessage = message => { 58 | if (message?.msg?.action === 'deleteallcache') { 59 | createCacheManagerInstance().removeAll(); 60 | } else if (message?.msg?.action === 'deletecache') { 61 | createCacheManagerInstance().remove(message?.msg?.key); 62 | } 63 | }; 64 | 65 | const handleUrls = async (req, res, next) => { 66 | newrelic?.setTransactionName?.(req.path); 67 | 68 | if (req.url === '/' && req.method === 'GET') { 69 | res.html(Welcome()); 70 | } else if (req.url === '/metrics' && req.method === 'GET' && !enablePrometheus) { 71 | res.setHeader('Content-Type', prom.register.contentType); 72 | res.end(prom.register.metrics()); 73 | } else if (req.url === '/status' && req.method === 'GET') { 74 | res.json({ success: true, version: process.env.GO_PIPELINE_LABEL || '1.0.0', fragments }); 75 | } else if ((req.url === '/statusCheck' || req.url === '/statuscheck') && req.method === 'GET') { 76 | res.json({ success: true, version: process.env.GO_PIPELINE_LABEL || '1.0.0', fragments }); 77 | } else if (req.url === '/deleteallcache' && req.method === 'GET') { 78 | process.send({ 79 | msg: { 80 | action: 'deleteallcache' 81 | }, 82 | options: { 83 | forwardAllWorkers: true 84 | } 85 | }); 86 | res.json({ success: true }); 87 | } else if (req.path === '/deletecache' && req.method === 'GET') { 88 | if (req?.query?.key) { 89 | process.send({ 90 | msg: { 91 | action: 'deletecache', 92 | key: req?.query?.key 93 | }, 94 | options: { 95 | forwardAllWorkers: true 96 | } 97 | }); 98 | res.json({ success: true }); 99 | } else { 100 | res.json({ success: false }); 101 | } 102 | } else { 103 | next(); 104 | } 105 | }; 106 | 107 | const cors = async (req, res, next) => { 108 | const { corsWhiteListDomains } = voltranConfig; 109 | const { origin } = req.headers; 110 | if (origin && corsWhiteListDomains?.map(domain => domain?.includes(origin))) { 111 | res.setHeader('Access-Control-Allow-Origin', origin); 112 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 113 | } else { 114 | res.setHeader('Access-Control-Allow-Origin', '*'); 115 | } 116 | res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, HEAD, OPTIONS'); 117 | 118 | if (req.method === 'OPTIONS') { 119 | res.writeHead(HTTP_STATUS_CODES.OK); 120 | res.end(); 121 | 122 | return; 123 | } 124 | 125 | next(); 126 | }; 127 | 128 | const utils = async (req, res, next) => { 129 | res.json = json => { 130 | addCustomAttrsToNewrelic(json.message); 131 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 132 | res.end(JSON.stringify(json)); 133 | }; 134 | 135 | res.html = html => { 136 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 137 | res.end(html); 138 | }; 139 | 140 | res.status = code => { 141 | res.statusCode = code; 142 | return res; 143 | }; 144 | 145 | next(); 146 | }; 147 | 148 | const locals = async (req, res, next) => { 149 | const parsedUrl = url.parse(req.url, true); 150 | 151 | req.query = JSON.parse(xss(JSON.stringify(parsedUrl.query))); 152 | req.path = xss(parsedUrl.pathname); 153 | req.url = xss(req.url); 154 | 155 | if (req.headers['set-cookie']) { 156 | req.headers.cookie = req.headers.cookie || req.headers['set-cookie']?.join(); 157 | delete req.headers['set-cookie']; 158 | } 159 | 160 | res.locals = {}; 161 | res.locals.startEpoch = new Date(); 162 | 163 | next(); 164 | }; 165 | 166 | if (process.env.NODE_ENV === 'production') { 167 | const hiddie = Hiddie(async (err, req, res) => { 168 | res.end(); 169 | }); 170 | hiddie.use(compression()); 171 | hiddie.use(locals); 172 | hiddie.use(helmet()); 173 | hiddie.use(cors); 174 | hiddie.use('/', serveStatic(`${voltranConfig.distFolder}/public`)); 175 | bundleAnalyzerStaticEnabled && 176 | hiddie.use( 177 | '/bundleAnalyze', 178 | serveStatic(`${voltranConfig.distFolder}/public/project/assets/report.html`) 179 | ); 180 | hiddie.use(cookieParser()); 181 | hiddie.use(utils); 182 | hiddie.use(handleUrls); 183 | 184 | if (enablePrometheus) { 185 | Prometheus.injectMetricsRoute(hiddie); 186 | Prometheus.startCollection(); 187 | } 188 | 189 | registerControllers(hiddie); 190 | hiddie.use('/components/:components/:path*', renderMultiple); 191 | hiddie.use('/components/:components', renderMultiple); 192 | hiddie.use(render); 193 | http.createServer(hiddie.run).listen(voltranConfig.port); 194 | } 195 | 196 | export default () => { 197 | return compose([ 198 | compression(), 199 | locals, 200 | helmet(), 201 | serveStatic(`${voltranConfig.distFolder}/public`), 202 | cookieParser(), 203 | utils, 204 | handleUrls, 205 | render 206 | ]); 207 | }; 208 | -------------------------------------------------------------------------------- /src/tools/bundle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const webpack = require('webpack'); 3 | 4 | const webpackClientConfig = require('../../webpack.client.config'); 5 | const webpackServerConfig = require('../../webpack.server.config'); 6 | 7 | function compilationCallback(err, stats, config, resolve, reject) { 8 | if (err) { 9 | console.log('Compilation execution error occurred'); 10 | console.log(err); 11 | 12 | return reject(err); 13 | } 14 | 15 | if (stats.hasErrors()) { 16 | console.log('Compilation bundle error occurred'); 17 | console.log(stats.toJson().errors); 18 | 19 | return reject(new Error('Webpack compilation errors')); 20 | } 21 | 22 | console.log(`Compilation completed for ${config.name}`); 23 | 24 | resolve(); 25 | } 26 | 27 | async function bundle() { 28 | const clientCompilationPromise = new Promise((resolve, reject) => { 29 | webpack(webpackClientConfig, (err, stats) => 30 | compilationCallback(err, stats, webpackClientConfig, resolve, reject) 31 | ); 32 | }); 33 | 34 | await clientCompilationPromise; 35 | 36 | const serverCompilationPromise = new Promise((resolve, reject) => { 37 | webpack(webpackServerConfig, (err, stats) => 38 | compilationCallback(err, stats, webpackServerConfig, resolve, reject) 39 | ); 40 | }); 41 | 42 | await serverCompilationPromise; 43 | } 44 | 45 | module.exports = bundle; 46 | -------------------------------------------------------------------------------- /src/tools/clean.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { cleanDir } = require('./lib/fs'); 3 | const voltranConfig = require('../../voltran.config'); 4 | 5 | function clean() { 6 | return Promise.all([cleanDir(`${voltranConfig.distFolder}/*`, { nosort: true, dot: true })]); 7 | } 8 | 9 | module.exports = clean; 10 | -------------------------------------------------------------------------------- /src/tools/lib/fs.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const rimraf = require('rimraf'); 3 | 4 | const cleanDir = (pattern, options) => 5 | new Promise((resolve, reject) => 6 | rimraf(pattern, { glob: options }, (err, result) => (err ? reject(err) : resolve(result))) 7 | ); 8 | 9 | module.exports = { 10 | cleanDir 11 | }; 12 | -------------------------------------------------------------------------------- /src/tools/run.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | function run(fn, options) { 4 | const task = typeof fn.default === 'undefined' ? fn : fn.default; 5 | const start = new Date(); 6 | 7 | console.log( 8 | 'src/tools/run.js', 9 | 'run', 10 | `Starting '${task.name}${options ? ` (${options})` : ''}'...` 11 | ); 12 | 13 | return task(options).then(resolution => { 14 | const end = new Date(); 15 | const time = end.getTime() - start.getTime(); 16 | console.log( 17 | 'src/tools/run.js', 18 | 'run', 19 | `Finished '${task.name}${options ? ` (${options})` : ''}' after ${time} ms` 20 | ); 21 | return resolution; 22 | }); 23 | } 24 | 25 | if (require.main === module && process.argv.length > 2) { 26 | delete require.cache[__filename]; 27 | 28 | const module = require(`./${process.argv[2]}.js`); 29 | 30 | run(module).catch(err => { 31 | console.error('src/tools/run.js', 'run', err.message); 32 | process.exit(1); 33 | }); 34 | } 35 | 36 | module.exports = run; 37 | -------------------------------------------------------------------------------- /src/tools/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const http = require('http'); 3 | const Hiddie = require('hiddie'); 4 | const webpack = require('webpack'); 5 | const webpackDevMiddleware = require('webpack-dev-middleware'); 6 | const webpackHotMiddleware = require('webpack-hot-middleware'); 7 | const webpackHotServerMiddleware = require('webpack-hot-server-middleware'); 8 | const webpackClientConfig = require('../../webpack.client.config'); 9 | const webpackServerConfig = require('../../webpack.server.config'); 10 | const voltranConfig = require('../../voltran.config'); 11 | const run = require('./run'); 12 | const clean = require('./clean'); 13 | 14 | const PORT_REPORT_TIMEOUT = 1000; 15 | 16 | async function start() { 17 | const hiddie = Hiddie((err, req, res) => { 18 | res.end(); 19 | }); 20 | 21 | await run(clean); 22 | 23 | const compiler = webpack([webpackClientConfig, webpackServerConfig]); 24 | 25 | const clientCompiler = compiler.compilers.find(compiler => compiler.name === 'client'); 26 | 27 | hiddie.use( 28 | webpackDevMiddleware(compiler, { 29 | quiet: true, 30 | noInfo: true, 31 | lazy: false, 32 | writeToDisk: true, 33 | serverSideRender: false, 34 | publicPath: webpackClientConfig.output.publicPath 35 | }) 36 | ); 37 | 38 | hiddie.use(webpackHotMiddleware(clientCompiler)); 39 | hiddie.use( 40 | webpackHotServerMiddleware(compiler, { 41 | chunkName: 'server', 42 | serverRendererOptions: { hiddie } 43 | }) 44 | ); 45 | 46 | http.createServer(hiddie.run).listen(voltranConfig.port); 47 | 48 | compiler.hooks.done.tap('start.js__port-reporting', () => { 49 | setTimeout(() => { 50 | console.log(`Voltran ready on ${voltranConfig.port}`); 51 | }, PORT_REPORT_TIMEOUT); 52 | }); 53 | 54 | return hiddie; 55 | } 56 | 57 | module.exports = start; 58 | -------------------------------------------------------------------------------- /src/tools/task.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | function run(task, action, ...args) { 3 | const command = process.argv[2]; 4 | const taskName = command && !command.startsWith('-') ? `${task}:${command}` : task; 5 | const start = new Date(); 6 | 7 | console.log('tools/task.js', 'run', `Starting '${taskName}'...\n`); 8 | 9 | return Promise.resolve() 10 | .then(() => action(...args)) 11 | .then(() => { 12 | console.log( 13 | 'tools/task.js', 14 | 'run', 15 | `Finished '${taskName}' after ${new Date().getTime() - start.getTime()}ms\n` 16 | ); 17 | }) 18 | .catch(err => console.log('tools/task.js', 'run', `${err.stack}\n`)); 19 | } 20 | 21 | process.nextTick(() => require.main.exports()); 22 | 23 | module.exports = (task, action) => run.bind(undefined, task, action); 24 | -------------------------------------------------------------------------------- /src/universal/common/network/apiUtils.js: -------------------------------------------------------------------------------- 1 | import Request from '../../model/Request'; 2 | import { createCacheManagerInstance } from '../../core/cache/cacheUtils'; 3 | 4 | function createApiClient(apiManager) { 5 | const cacheManager = createCacheManagerInstance(); 6 | 7 | function getSortedParams(nonSortedParams) { 8 | if (!nonSortedParams) { 9 | return nonSortedParams; 10 | } 11 | 12 | return Object.keys(nonSortedParams) 13 | .sort() 14 | .reduce( 15 | (params, key) => ({ 16 | ...params, 17 | [key]: nonSortedParams[key] 18 | }), 19 | {} 20 | ); 21 | } 22 | 23 | function getPayload(url, method, params, configArgument) { 24 | let payload; 25 | if (configArgument) { 26 | payload = { url, method, params, ...configArgument }; 27 | } else { 28 | payload = { url, method, params }; 29 | } 30 | return payload; 31 | } 32 | 33 | function getRequest(method, url, paramsArgument, configArgument, response) { 34 | const params = getSortedParams(paramsArgument); 35 | const payload = getPayload(url, method, params, configArgument); 36 | const uri = apiManager.api.getUri(payload); 37 | 38 | return new Request(apiManager.api, payload, uri, response); 39 | } 40 | 41 | function makeRequest(method, url, paramsArgument, configArgument, cacheSettings) { 42 | let request; 43 | 44 | const isCacheEnabled = cacheSettings && cacheSettings.cacheStatus && cacheSettings.cacheKey; 45 | if (isCacheEnabled) { 46 | const cacheResponse = cacheManager.get(cacheSettings); 47 | 48 | if (cacheResponse && !cacheResponse.isExpired) { 49 | // console.log('Came from cache', cacheSettings.cacheKey); 50 | request = getRequest(method, url, paramsArgument, configArgument, { 51 | cacheResponse: cacheResponse.cacheValue 52 | }); 53 | } else { 54 | // console.log('Not exist cache, request sent', cacheSettings.cacheKey); 55 | request = getRequest(method, url, paramsArgument, configArgument, { 56 | onSuccess: response => { 57 | cacheManager.put(cacheSettings, response); 58 | return response; 59 | }, 60 | onError: error => { 61 | if (cacheResponse) { 62 | // console.log('Came from cache - IN ERROR BLOCK', cacheSettings.cacheKey); 63 | return Promise.resolve(cacheResponse.cacheValue); 64 | } 65 | return error; 66 | } 67 | }); 68 | } 69 | } else { 70 | // console.log('Doesnt have CacheSettings', url); 71 | request = getRequest(method, url, paramsArgument, configArgument); 72 | } 73 | 74 | return request; 75 | } 76 | 77 | return { 78 | get(url, params, config, cacheSettings) { 79 | return makeRequest('get', url, params, config, cacheSettings); 80 | }, 81 | 82 | post(url, params, config, cacheSettings) { 83 | return makeRequest('post', url, params, config, cacheSettings); 84 | }, 85 | 86 | put(url, params, config, cacheSettings) { 87 | return makeRequest('put', url, params, config, cacheSettings); 88 | }, 89 | 90 | delete(url, config, cacheSettings) { 91 | return makeRequest('delete', url, config, null, cacheSettings); 92 | }, 93 | 94 | head(url, config, cacheSettings) { 95 | return makeRequest('head', url, config, null, cacheSettings); 96 | }, 97 | 98 | options(url, config, cacheSettings) { 99 | return makeRequest('options', url, config, null, cacheSettings); 100 | } 101 | }; 102 | } 103 | 104 | // eslint-disable-next-line import/prefer-default-export 105 | export { createApiClient }; 106 | -------------------------------------------------------------------------------- /src/universal/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import { Switch } from 'react-router-dom'; 4 | import { hot } from 'react-hot-loader'; 5 | import PropTypes from 'prop-types'; 6 | 7 | import { renderRoutes } from '../routes/routes'; 8 | import { extractQueryParamsFromLocation } from '../core/route/routeUtils'; 9 | import ReactRenderContext from '../core/react/ReactRenderContext'; 10 | 11 | class App extends PureComponent { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | isHydratingCompleted: false 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this.setState({ 22 | isHydratingCompleted: true 23 | }); 24 | } 25 | 26 | componentWillUpdate(nextProps) { 27 | const { location } = this.props; 28 | 29 | const url = location.pathname + location.search; 30 | const nextUrl = nextProps.location.pathname + nextProps.location.search; 31 | 32 | if (url !== nextUrl && process.env.BROWSER) { 33 | window.ref = window.location.origin + url; 34 | } 35 | } 36 | 37 | generateRoutingProps() { 38 | const { initialState, location } = this.props; 39 | const query = extractQueryParamsFromLocation(location); 40 | return { 41 | query, 42 | initialState 43 | }; 44 | } 45 | 46 | render() { 47 | const { isHydratingCompleted } = this.state; 48 | return ( 49 | 50 |
51 | {renderRoutes(this.generateRoutingProps())} 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | App.propTypes = { 59 | initialState: PropTypes.shape(), 60 | location: PropTypes.shape() 61 | }; 62 | 63 | App.defaultProps = { 64 | initialState: null, 65 | location: null 66 | }; 67 | 68 | export { App }; 69 | export default hot(module)(withRouter(App)); 70 | -------------------------------------------------------------------------------- /src/universal/components/ClientApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function ClientApp({ children }) { 4 | return
{children}
; 5 | } 6 | 7 | export default ClientApp; 8 | -------------------------------------------------------------------------------- /src/universal/components/Html.js: -------------------------------------------------------------------------------- 1 | import voltranConfig from '../../../voltran.config'; 2 | 3 | function generateInitialState(initialState, componentName) { 4 | const prefix = voltranConfig.prefix.toUpperCase(); 5 | const include = `window.${prefix}.${componentName.toUpperCase().replace(/['"]+/g, '')}`; 6 | 7 | return ` 8 | window.${prefix} = window.${prefix} || {}; 9 | 10 | ${include} = Object.assign(${include} || {}, { 11 | '${initialState.id}': { 12 | 'STATE': ${JSON.stringify(initialState).replace(new RegExp('', 'g'), '<\\/script>')} 13 | } 14 | })`; 15 | } 16 | 17 | function cr(condition, ok, cancel) { 18 | return condition ? ok : cancel || ''; 19 | } 20 | 21 | function componentClassName(componentName, context) { 22 | return context.query && context.query.id ? `${componentName}_${context.query.id}` : componentName; 23 | } 24 | 25 | function Html({ 26 | componentName, 27 | children, 28 | styleTags, 29 | initialState, 30 | fullWidth, 31 | isMobileFragment, 32 | context 33 | }) { 34 | return ` 35 |
36 | ${styleTags} 37 | 38 |
44 | ${children} 45 |
46 |
REPLACE_WITH_LINKS
47 |
REPLACE_WITH_SCRIPTS
48 | 49 | ${cr( 50 | process.env.NODE_ENV !== 'production', 51 | `` 52 | )} 53 | ${cr( 54 | process.env.NODE_ENV !== 'production', 55 | `` 56 | )} 57 |
`; 58 | } 59 | 60 | export default Html; 61 | -------------------------------------------------------------------------------- /src/universal/components/Preview.js: -------------------------------------------------------------------------------- 1 | const appConfig = require('__APP_CONFIG__'); 2 | 3 | export default (body, title = null) => { 4 | const additionalTitle = title ? ` - ${title}` : ''; 5 | 6 | function cr(condition, ok, cancel) { 7 | return condition ? ok : cancel || ''; 8 | } 9 | 10 | return ` 11 | 12 | 13 | Preview${additionalTitle} 14 | 15 | 16 | 17 | ${cr( 18 | appConfig.showPreviewFrame, 19 | `` 64 | )} 65 | 66 | 67 | ${body} 68 | 69 | 70 | `; 71 | }; 72 | -------------------------------------------------------------------------------- /src/universal/components/PureHtml.js: -------------------------------------------------------------------------------- 1 | import voltranConfig from '../../../voltran.config'; 2 | 3 | function generateInitialState(initialState, componentName) { 4 | const prefix = voltranConfig.prefix.toUpperCase(); 5 | const include = `window.${prefix}.${componentName.toUpperCase().replace(/['"]+/g, '')}`; 6 | 7 | return ` 8 | window.${prefix} = window.${prefix} || {}; 9 | ${include} = { 10 | ...(${include} || {}), 11 | '${initialState.id}': { 12 | 'STATE': ${JSON.stringify(initialState).replace( 13 | new RegExp('', 'g'), 14 | '<\\/script>' 15 | )} 16 | } 17 | }`; 18 | } 19 | 20 | const generateScripts = scripts => { 21 | return scripts 22 | .map(script => { 23 | return ``; 24 | }) 25 | .join(''); 26 | }; 27 | 28 | const generateLinks = links => { 29 | return links.map(link => ``).join(''); 30 | }; 31 | 32 | export { generateScripts, generateLinks }; 33 | 34 | export default (resultPath, componentName, initialState) => { 35 | return ` 36 |
37 | 38 |
41 |
REPLACE_WITH_LINKS
42 |
REPLACE_WITH_SCRIPTS
43 |
44 | `; 45 | }; 46 | -------------------------------------------------------------------------------- /src/universal/components/route/HbRoute.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Route } from 'react-router-dom'; 5 | import { renderMergedProps } from '../../core/route/routeUtils'; 6 | 7 | class HbRoute extends PureComponent { 8 | hbRouteProps = routeProps => { 9 | const { component, routingProps } = this.props; 10 | return renderMergedProps(component, routeProps, routingProps); 11 | }; 12 | 13 | render() { 14 | const { component, routingProps, ...rest } = this.props; 15 | return ; 16 | } 17 | } 18 | 19 | HbRoute.propTypes = { 20 | component: PropTypes.func.isRequired, 21 | routeProps: PropTypes.shape(), 22 | routingProps: PropTypes.shape() 23 | }; 24 | 25 | HbRoute.defaultProps = { 26 | routeProps: null, 27 | routingProps: null 28 | }; 29 | 30 | export default HbRoute; 31 | -------------------------------------------------------------------------------- /src/universal/core/api/ApiManager.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | CONTENT_TYPE_HEADER, 4 | JSON_CONTENT_TYPE, 5 | REQUEST_TYPES_WITH_BODY 6 | } from '../../utils/constants'; 7 | 8 | function createBaseConfig() { 9 | return {}; 10 | } 11 | 12 | class ApiManager { 13 | constructor(customConfig) { 14 | const headers = { 15 | common: { 16 | ...createBaseConfig.headers, 17 | ...(customConfig ? customConfig.headers : null) 18 | } 19 | }; 20 | 21 | if (!process.env.BROWSER) { 22 | headers['Accept-Encoding'] = 'gzip, deflate'; 23 | } 24 | 25 | this.api = this.createInstance({ 26 | ...createBaseConfig(), 27 | ...customConfig, 28 | headers 29 | }); 30 | } 31 | 32 | createInstance(config) { 33 | const instance = axios.create(config); 34 | 35 | REQUEST_TYPES_WITH_BODY.forEach(requestType => { 36 | instance.defaults.headers[requestType][CONTENT_TYPE_HEADER] = JSON_CONTENT_TYPE; 37 | }); 38 | 39 | return instance; 40 | } 41 | } 42 | 43 | export default ApiManager; 44 | -------------------------------------------------------------------------------- /src/universal/core/api/ClientApiManager.js: -------------------------------------------------------------------------------- 1 | import ApiManager from './ApiManager'; 2 | import { createApiClient } from '../../common/network/apiUtils'; 3 | 4 | export default (config, timeout) => { 5 | const apiManager = new ApiManager({ 6 | baseURL: config.clientUrl, 7 | timeout 8 | }); 9 | 10 | return createApiClient(apiManager); 11 | }; 12 | -------------------------------------------------------------------------------- /src/universal/core/api/ClientApiManagerCache.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import ClientApiManager from './ClientApiManager'; 3 | import { SERVICES } from '../../utils/constants'; 4 | 5 | const { services, timeouts } = require('__APP_CONFIG__'); 6 | 7 | const cache = {}; 8 | 9 | Object.entries(services).forEach(entity => { 10 | cache[SERVICES[entity[0]]] = ClientApiManager(entity[1], timeouts.clientApiManager); 11 | }); 12 | 13 | export default cache; 14 | -------------------------------------------------------------------------------- /src/universal/core/api/ServerApiManager.js: -------------------------------------------------------------------------------- 1 | import ApiManager from './ApiManager'; 2 | import { createApiClient } from '../../common/network/apiUtils'; 3 | 4 | import http from 'http'; 5 | import https from 'https'; 6 | 7 | const BASE_HTTP_AGENT_CONFIG = { 8 | keepAlive: true, 9 | rejectUnauthorized: false 10 | }; 11 | 12 | export default (config, timeout) => { 13 | const apiManager = new ApiManager({ 14 | timeout, 15 | baseURL: config.serverUrl, 16 | httpAgent: new http.Agent(BASE_HTTP_AGENT_CONFIG), 17 | httpsAgent: new https.Agent(BASE_HTTP_AGENT_CONFIG) 18 | }); 19 | 20 | return createApiClient(apiManager); 21 | }; 22 | -------------------------------------------------------------------------------- /src/universal/core/api/ServerApiManagerCache.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import ServerApiManager from './ServerApiManager'; 3 | import { SERVICES } from '../../utils/constants'; 4 | 5 | const { services, timeouts } = require('__APP_CONFIG__'); 6 | 7 | const cache = {}; 8 | 9 | Object.entries(services).forEach(entity => { 10 | cache[SERVICES[entity[0]]] = ServerApiManager(entity[1], timeouts.serverApiManager); 11 | }); 12 | 13 | export default cache; 14 | -------------------------------------------------------------------------------- /src/universal/core/cache/cacheManager.js: -------------------------------------------------------------------------------- 1 | const appConfig = require('__APP_CONFIG__'); 2 | 3 | const DEFAULT_EXPIRE_TIME = appConfig?.internalCache?.defaultInternalCacheMilliseconds; 4 | 5 | class CacheManager { 6 | constructor() { 7 | this.store = new Map(); 8 | } 9 | 10 | get(options) { 11 | const { cacheKey, expireTime } = options; 12 | 13 | const cache = this.store.get(cacheKey); 14 | if (!cache) { 15 | return null; 16 | } 17 | 18 | const { createdDate, cacheValue } = cache; 19 | const cacheCreatedDate = cache && new Date(createdDate); 20 | const cacheExpireDate = new Date(new Date() - (expireTime || DEFAULT_EXPIRE_TIME)); 21 | 22 | return { 23 | createdDate, 24 | cacheValue, 25 | isExpired: cacheExpireDate > cacheCreatedDate 26 | }; 27 | } 28 | 29 | put(options, val) { 30 | const { cacheKey } = options; 31 | 32 | this.store.set(cacheKey, { 33 | createdDate: new Date(), 34 | cacheValue: val 35 | }); 36 | } 37 | 38 | remove(cacheKey) { 39 | this.store.delete(cacheKey); 40 | } 41 | 42 | removeAll() { 43 | this.store = new Map(); 44 | } 45 | 46 | delete() { 47 | // no op here because this is standalone, not a part of $cacheFactory 48 | } 49 | } 50 | 51 | export default CacheManager; 52 | -------------------------------------------------------------------------------- /src/universal/core/cache/cacheUtils.js: -------------------------------------------------------------------------------- 1 | import CacheManager from './cacheManager'; 2 | 3 | let cacheManagerInstance = null; 4 | 5 | function createCacheManagerInstance() { 6 | if (!cacheManagerInstance) { 7 | cacheManagerInstance = new CacheManager(); 8 | } 9 | return cacheManagerInstance; 10 | } 11 | 12 | // eslint-disable-next-line import/prefer-default-export 13 | export { createCacheManagerInstance }; 14 | -------------------------------------------------------------------------------- /src/universal/core/react/ReactRenderContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ReactRenderContext = React.createContext(false); 4 | 5 | export default ReactRenderContext; 6 | -------------------------------------------------------------------------------- /src/universal/core/route/routeConstants.js: -------------------------------------------------------------------------------- 1 | const components = require('__V_COMPONENTS__'); 2 | 3 | const ROUTE_PATHS = {}; 4 | const ROUTE_CONFIGS = {}; 5 | 6 | Object.keys(components.default).forEach(path => { 7 | const info = components.default[path]; 8 | ROUTE_PATHS[info.name] = path; 9 | ROUTE_CONFIGS[path] = { 10 | routeName: info.name, 11 | isPublic: true, 12 | exact: true 13 | }; 14 | }); 15 | 16 | const ROUTE_PATH_ARRAY = Object.values(ROUTE_PATHS); 17 | 18 | export { ROUTE_PATHS, ROUTE_PATH_ARRAY, ROUTE_CONFIGS }; 19 | -------------------------------------------------------------------------------- /src/universal/core/route/routeUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import queryString from 'query-string'; 3 | import { matchPath } from 'react-router'; 4 | 5 | import { removeQueryStringFromUrl } from '../../utils/helper'; 6 | import { ROUTE_PATH_ARRAY, ROUTE_CONFIGS } from './routeConstants'; 7 | 8 | const extractQueryParamsFromLocation = location => { 9 | return queryString.parse(location.search); 10 | }; 11 | 12 | const matchUrlForRoutePath = (urlPath, routePath) => { 13 | let result = null; 14 | const routeConfig = ROUTE_CONFIGS[routePath]; 15 | const rawMatchPathResult = matchPath(removeQueryStringFromUrl(urlPath), { 16 | path: routePath, 17 | routePath, 18 | ...routeConfig 19 | }); 20 | 21 | if (rawMatchPathResult) { 22 | result = { 23 | ...rawMatchPathResult, 24 | ...routeConfig 25 | }; 26 | } 27 | 28 | return result; 29 | }; 30 | 31 | const matchUrlInRouteArray = (path, routeArray) => { 32 | let result = null; 33 | 34 | for (let index = 0; index < routeArray.length; index += 1) { 35 | const routePath = routeArray[index]; 36 | const matchResult = matchUrlForRoutePath(path, routePath); 37 | 38 | if (matchResult) { 39 | result = matchResult; 40 | break; 41 | } 42 | } 43 | 44 | return result; 45 | }; 46 | 47 | const matchUrlInRouteConfigs = path => { 48 | return matchUrlInRouteArray(path, ROUTE_PATH_ARRAY); 49 | }; 50 | 51 | const renderMergedProps = (component, ownRouteProps, routingProps) => { 52 | const finalProps = { 53 | ...ownRouteProps, 54 | ...routingProps 55 | }; 56 | return React.createElement(component, finalProps); 57 | }; 58 | 59 | export { 60 | extractQueryParamsFromLocation, 61 | matchUrlInRouteConfigs, 62 | matchUrlInRouteArray, 63 | matchUrlForRoutePath, 64 | renderMergedProps 65 | }; 66 | -------------------------------------------------------------------------------- /src/universal/core/route/routesWithComponents.js: -------------------------------------------------------------------------------- 1 | const components = require('__V_COMPONENTS__'); 2 | 3 | const routesWithComponents = {}; 4 | 5 | Object.keys(components.default).forEach(path => { 6 | const info = components.default[path]; 7 | routesWithComponents[path] = info.fragment; 8 | }); 9 | 10 | export default routesWithComponents; 11 | -------------------------------------------------------------------------------- /src/universal/model/Component.js: -------------------------------------------------------------------------------- 1 | import routesWithComponents from '../core/route/routesWithComponents'; 2 | import { createComponentName } from '../utils/helper'; 3 | 4 | const COMPONENTS = require('__V_COMPONENTS__').default; 5 | 6 | export default class Component { 7 | static getComponentName = path => { 8 | return createComponentName(path); 9 | }; 10 | 11 | static getComponentPath = name => `/${name}`; 12 | 13 | static getComponentIsMobileFragment = path => { 14 | return COMPONENTS[path].isMobileFragment ? COMPONENTS[path].isMobileFragment : false; 15 | }; 16 | 17 | static getComponentIsFullWidth = path => { 18 | return COMPONENTS[path].fullWidth ? COMPONENTS[path].fullWidth : false; 19 | }; 20 | 21 | static getComponentIsPreviewQuery = path => { 22 | return COMPONENTS[path].isPreviewQuery || true; 23 | }; 24 | 25 | static getComponentObjectWithPath = path => routesWithComponents[path]; 26 | 27 | static getComponentWithName = name => new Component(Component.getComponentPath(name)); 28 | 29 | static getComponentWithPath = path => new Component(path); 30 | 31 | static isExist = path => Object.prototype.hasOwnProperty.call(routesWithComponents, path); 32 | 33 | constructor(path) { 34 | this.name = Component.getComponentName(path); 35 | this.path = path; 36 | this.isMobileFragment = Component.getComponentIsMobileFragment(path); 37 | this.fullWidth = Component.getComponentIsFullWidth(path); 38 | this.object = Component.getComponentObjectWithPath(path); 39 | this.isPreviewQuery = Component.getComponentIsPreviewQuery(path); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/universal/model/Renderer.js: -------------------------------------------------------------------------------- 1 | import ServerApiManagerCache from '../core/api/ServerApiManagerCache'; 2 | import { renderComponent, renderLinksAndScripts } from '../service/RenderService'; 3 | 4 | export default class Renderer { 5 | constructor(component, context) { 6 | this.component = component; 7 | this.context = context; 8 | this.servicesMap = null; 9 | this.initialState = null; 10 | this.winnerMap = null; 11 | 12 | if (this.isPredefinedInitialStateSupported() && 13 | (process.env.BROWSER || (!process.env.BROWSER && !this.context.isWithoutState))) { 14 | this.servicesMap = this.getServicesMap(); 15 | this.winnerMap = {}; 16 | } 17 | } 18 | 19 | setInitialState(prepareInitialStateArgs) { 20 | this.initialState = { 21 | data: this.component.object.prepareInitialState(...prepareInitialStateArgs) 22 | }; 23 | } 24 | 25 | isPredefinedInitialStateSupported() { 26 | return this.component.object.getServicesMap && this.component.object.prepareInitialState; 27 | } 28 | 29 | getServicesMap() { 30 | const services = this.component.object.services.map( 31 | serviceName => ServerApiManagerCache[serviceName] 32 | ); 33 | 34 | const params = [...services, this.context]; 35 | return this.component.object.getServicesMap(...params); 36 | } 37 | 38 | render() { 39 | return new Promise(resolve => { 40 | renderComponent(this.component, this.context, this.initialState).then(response => { 41 | const { output, links, scripts, activeComponent, seoState } = response; 42 | const html = renderLinksAndScripts(output, '', ''); 43 | 44 | resolve({ 45 | key: this.component.name, 46 | value: { html, scripts, style: links, activeComponent, seoState }, 47 | id: this.component.id 48 | }); 49 | }); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/universal/model/Request.js: -------------------------------------------------------------------------------- 1 | export default class Request { 2 | constructor(client, payload, uri, response) { 3 | this.uri = uri; 4 | this.client = client; 5 | this.payload = payload; 6 | this.hash = [payload.baseURL || client.defaults.baseURL, uri, payload.method].join(','); 7 | this.response = response; 8 | } 9 | 10 | execute() { 11 | let promise; 12 | if (this.response?.cacheResponse) { 13 | promise = Promise.resolve(this.response.cacheResponse); 14 | } else { 15 | promise = this.client 16 | .request(this.payload) 17 | .then(response => { 18 | if (this.response?.onSuccess) { 19 | return this.response.onSuccess(response); 20 | } 21 | return response; 22 | }) 23 | .catch(error => { 24 | if (this.response?.onError) { 25 | return this.response.onError(error); 26 | } 27 | throw error; 28 | }); 29 | } 30 | return promise; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/universal/partials/Welcome/PartialList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import structUtils from '../../utils/struct'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import { ServerStyleSheet } from 'styled-components'; 5 | 6 | import { List, ListItem, Name, Url, Footer, Label, Dot, Link, HeaderName } from './styled'; 7 | import partials from './partials'; 8 | 9 | const sheet = new ServerStyleSheet(); 10 | 11 | const Welcome = () => { 12 | const { live = [], dev = [], page = [] } = structUtils.groupBy(partials, item => item.status); 13 | 14 | const renderItem = item => ( 15 | 16 | 17 | {item.name} 18 | {item.url} 19 |
20 | 23 |
24 | 25 |
26 | ); 27 | return ( 28 | 29 | Live 30 | {live.map(item => renderItem(item))} 31 | Pages 32 | {page.map(item => renderItem(item))} 33 | Development 34 | {dev.map(item => renderItem(item))} 35 | 36 | ); 37 | }; 38 | export default ReactDOMServer.renderToString(sheet.collectStyles()); 39 | const styleTags = sheet.getStyleTags(); 40 | 41 | export { styleTags }; 42 | -------------------------------------------------------------------------------- /src/universal/partials/Welcome/Welcome.js: -------------------------------------------------------------------------------- 1 | import PartialList, { styleTags } from './PartialList'; 2 | 3 | export default () => { 4 | return ` 5 | 6 | 7 | Welcome 8 | 9 | 23 | ${styleTags} 24 | 25 | ${PartialList} 26 | 27 | `; 28 | }; 29 | -------------------------------------------------------------------------------- /src/universal/partials/Welcome/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Welcome'; 2 | -------------------------------------------------------------------------------- /src/universal/partials/Welcome/partials.js: -------------------------------------------------------------------------------- 1 | const components = require('__V_COMPONENTS__'); 2 | 3 | const partials = []; 4 | 5 | Object.keys(components.default).forEach(path => { 6 | const info = components.default[path]; 7 | partials.push({ 8 | name: info.fragmentName, 9 | url: path, 10 | status: info.status 11 | }); 12 | }); 13 | 14 | export default partials; 15 | -------------------------------------------------------------------------------- /src/universal/partials/Welcome/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const STATUS_COLOR = { 4 | live: '#8dc63f', 5 | dev: '#FF6000' 6 | }; 7 | export const List = styled.ul` 8 | list-style: none; 9 | margin: 0; 10 | padding: 0; 11 | `; 12 | 13 | export const HeaderName = styled.div` 14 | font-size: 36px; 15 | font-weight: bold; 16 | margin: 10px; 17 | `; 18 | 19 | export const ListItem = styled.li` 20 | padding: 20px; 21 | border-radius: 2px; 22 | background: white; 23 | box-shadow: 0 2px 1px rgba(170, 170, 170, 0.25); 24 | position: relative; 25 | display: inline-block; 26 | vertical-align: top; 27 | height: 120px; 28 | width: 320px; 29 | margin: 10px; 30 | cursor: pointer; 31 | 32 | &:hover { 33 | background: #efefef; 34 | } 35 | 36 | @media screen and (max-width: 600px) { 37 | display: block; 38 | width: auto; 39 | height: 150px; 40 | margin: 10px auto; 41 | } 42 | `; 43 | 44 | export const Link = styled.a` 45 | text-decoration: none; 46 | color: #49494a; 47 | 48 | &:before { 49 | position: absolute; 50 | z-index: 0; 51 | top: 0; 52 | right: 0; 53 | bottom: 0; 54 | left: 0; 55 | display: block; 56 | content: ''; 57 | } 58 | `; 59 | 60 | export const Name = styled.span` 61 | font-weight: 400; 62 | display: block; 63 | max-width: 80%; 64 | font-size: 16px; 65 | line-height: 18px; 66 | `; 67 | 68 | export const Url = styled.span` 69 | font-size: 11px; 70 | line-height: 16px; 71 | color: #a1a1a4; 72 | `; 73 | 74 | export const Footer = styled.span` 75 | display: block; 76 | position: absolute; 77 | bottom: 0; 78 | left: 0; 79 | right: 0; 80 | width: 100%; 81 | padding: 20px; 82 | border-top: 1px solid #eee; 83 | font-size: 13px; 84 | `; 85 | 86 | export const Label = styled.span` 87 | font-size: 13px; 88 | align-items: center; 89 | font-weight: bold; 90 | display: flex; 91 | position: absolute; 92 | right: 20px; 93 | top: 0; 94 | line-height: 40px; 95 | margin: 0 10px; 96 | 97 | @media screen and (max-width: 200px) { 98 | right: auto; 99 | left: 10px; 100 | } 101 | 102 | color: ${({ status }) => (status && STATUS_COLOR[status]) || '#8dc63f'}; 103 | `; 104 | 105 | export const Dot = styled.span` 106 | display: inline-block; 107 | vertical-align: middle; 108 | width: 16px; 109 | height: 16px; 110 | overflow: hidden; 111 | border-radius: 50%; 112 | padding: 0; 113 | text-indent: -9999px; 114 | color: transparent; 115 | line-height: 16px; 116 | margin-left: 10px; 117 | background: ${({ status }) => (status && STATUS_COLOR[status]) || '#8dc63f'}; 118 | `; 119 | -------------------------------------------------------------------------------- /src/universal/partials/withBaseComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import ClientApp from '../components/ClientApp'; 5 | import { WINDOW_GLOBAL_PARAMS } from '../utils/constants'; 6 | import { createComponentName } from '../utils/helper'; 7 | import voltranConfig from '../../../voltran.config'; 8 | 9 | const getStaticProps = () => { 10 | const staticProps = {}; 11 | 12 | if (voltranConfig.staticProps) { 13 | voltranConfig.staticProps.map(property => { 14 | staticProps[property.name] = property.value; 15 | }); 16 | } 17 | 18 | return staticProps; 19 | }; 20 | 21 | const withBaseComponent = (PageComponent, pathName) => { 22 | const componentName = createComponentName(pathName); 23 | const prefix = voltranConfig.prefix.toUpperCase(); 24 | 25 | if (process.env.BROWSER && window[prefix] && window[prefix][componentName.toUpperCase()]) { 26 | const fragments = window[prefix][componentName.toUpperCase()]; 27 | const history = window[WINDOW_GLOBAL_PARAMS.HISTORY]; 28 | const staticProps = getStaticProps(); 29 | 30 | Object.keys(fragments).forEach(id => { 31 | const componentEl = document.getElementById(`${componentName}_${id}`); 32 | const isHydrated = componentEl && !!componentEl.getAttribute('voltran-hydrated'); 33 | 34 | if (isHydrated || !componentEl) return; 35 | 36 | const initialState = fragments[id].STATE; 37 | 38 | ReactDOM.hydrate( 39 | 40 | 41 | , 42 | componentEl, 43 | () => { 44 | componentEl.style.pointerEvents = 'auto'; 45 | componentEl.setAttribute('voltran-hydrated', 'true'); 46 | } 47 | ); 48 | }); 49 | } 50 | 51 | return props => { 52 | return ; 53 | }; 54 | }; 55 | 56 | export default withBaseComponent; 57 | -------------------------------------------------------------------------------- /src/universal/requests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hepsiburada/VoltranJS/a7aa59b8f7e4871a19db6cc234df5d43d1f3189b/src/universal/requests/.gitkeep -------------------------------------------------------------------------------- /src/universal/routes/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ROUTE_CONFIGS } from '../core/route/routeConstants'; 4 | import routesWithComponents from '../core/route/routesWithComponents'; 5 | 6 | import HbRoute from '../components/route/HbRoute'; 7 | 8 | export const renderRoutes = routingProps => 9 | Object.entries(routesWithComponents).map(entity => ( 10 | 17 | )); 18 | 19 | export default { renderRoutes }; 20 | -------------------------------------------------------------------------------- /src/universal/service/RenderService.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | import { StaticRouter } from 'react-router'; 5 | import ConnectedApp from '../components/App'; 6 | import Html from '../components/Html'; 7 | import PureHtml, { generateLinks, generateScripts } from '../components/PureHtml'; 8 | import ServerApiManagerCache from '../core/api/ServerApiManagerCache'; 9 | import createBaseRenderHtmlProps from '../utils/baseRenderHtml'; 10 | import { guid } from '../utils/helper'; 11 | 12 | const getStates = async (component, context, predefinedInitialState) => { 13 | const initialState = predefinedInitialState || { data: {} }; 14 | let subComponentFiles = []; 15 | let seoState = {}; 16 | let responseOptions = {}; 17 | 18 | if (context.isWithoutState) { 19 | return { initialState, seoState, subComponentFiles, responseOptions }; 20 | } 21 | 22 | if (!predefinedInitialState && component.getInitialState) { 23 | const services = component.services.map(serviceName => ServerApiManagerCache[serviceName]); 24 | initialState.data = await component.getInitialState(...[...services, context]); 25 | } 26 | 27 | if (component.getSeoState) { 28 | seoState = component.getSeoState(initialState.data); 29 | } 30 | 31 | if (initialState.data.subComponentFiles) { 32 | subComponentFiles = initialState.data.subComponentFiles; 33 | } 34 | 35 | if (initialState.data.responseOptions) { 36 | responseOptions = initialState.data.responseOptions; 37 | } 38 | 39 | return { initialState, seoState, subComponentFiles, responseOptions }; 40 | }; 41 | 42 | const renderLinksAndScripts = (html, links, scripts) => { 43 | return html 44 | .replace('
REPLACE_WITH_LINKS
', links) 45 | .replace('
REPLACE_WITH_SCRIPTS
', scripts); 46 | }; 47 | 48 | const renderHtml = (component, initialState, context) => { 49 | // eslint-disable-next-line no-param-reassign 50 | component.id = guid(); 51 | const initialStateWithLocation = { ...initialState, location: context, id: component.id }; 52 | const sheet = new ServerStyleSheet(); 53 | 54 | if (isWithoutHTML(context.query)) { 55 | return PureHtml(component.path, component.name, initialStateWithLocation); 56 | } 57 | 58 | const children = ReactDOMServer.renderToString( 59 | sheet.collectStyles( 60 | 61 | 62 | 63 | ) 64 | ); 65 | 66 | const styleTags = sheet.getStyleTags(); 67 | const resultPath = `'${component.path}'`; 68 | 69 | return Html({ 70 | resultPath, 71 | componentName: component.name, 72 | children, 73 | styleTags, 74 | initialState: initialStateWithLocation, 75 | fullWidth: component.fullWidth, 76 | isMobileFragment: component.isMobileFragment, 77 | context 78 | }); 79 | }; 80 | 81 | const isWithoutHTML = query => { 82 | return query.withoutHTML === ''; 83 | }; 84 | 85 | const isPreview = query => { 86 | return query.preview === ''; 87 | }; 88 | 89 | const isWithoutState = query => { 90 | return query.withoutState === ''; 91 | }; 92 | 93 | const renderComponent = async (component, context, predefinedInitialState = null) => { 94 | const { initialState, seoState, subComponentFiles, responseOptions } = await getStates( 95 | component.object, 96 | context, 97 | predefinedInitialState 98 | ); 99 | 100 | const { links, scripts, activeComponent } = await createBaseRenderHtmlProps( 101 | component.name, 102 | subComponentFiles, 103 | context 104 | ); 105 | 106 | const output = renderHtml(component, initialState, context); 107 | const fullHtml = renderLinksAndScripts(output, generateLinks(links), generateScripts(scripts)); 108 | 109 | return { 110 | output, 111 | fullHtml, 112 | links, 113 | scripts, 114 | activeComponent, 115 | componentName: component.name, 116 | seoState, 117 | fullWidth: component.fullWidth, 118 | isMobileComponent: component.isMobileComponent, 119 | isPreviewQuery: component.isPreviewQuery, 120 | responseOptions 121 | }; 122 | }; 123 | 124 | export { 125 | renderHtml, 126 | renderLinksAndScripts, 127 | getStates, 128 | isWithoutHTML, 129 | isPreview, 130 | isWithoutState, 131 | renderComponent 132 | }; 133 | -------------------------------------------------------------------------------- /src/universal/tools/newrelic/newrelic.js: -------------------------------------------------------------------------------- 1 | const { newrelicEnabled } = require('__APP_CONFIG__'); 2 | 3 | // eslint-disable-next-line import/no-mutable-exports 4 | let newrelic = null; 5 | 6 | if (newrelicEnabled) { 7 | // eslint-disable-next-line global-require 8 | newrelic = require('newrelic'); 9 | } 10 | 11 | const isJsonValid = str => { 12 | try { 13 | JSON.parse(str); 14 | } catch (e) { 15 | return false; 16 | } 17 | return true; 18 | }; 19 | 20 | export const addCustomAttrsToNewrelic = message => { 21 | if (!newrelic) return; 22 | const isValidJson = isJsonValid(message); 23 | if (!isValidJson) return; 24 | const parsedMessage = JSON.parse(message); 25 | // eslint-disable-next-line no-restricted-syntax 26 | for (const key in parsedMessage) { 27 | if (Object.hasOwnProperty.call(parsedMessage, key)) { 28 | newrelic?.addCustomAttribute(`a_${key}`, parsedMessage[key]); 29 | } 30 | } 31 | newrelic?.addCustomAttribute('a_voltran.error.message', message); 32 | }; 33 | 34 | export default newrelic; 35 | -------------------------------------------------------------------------------- /src/universal/utils/baseRenderHtml.js: -------------------------------------------------------------------------------- 1 | const appConfig = require('__APP_CONFIG__'); 2 | const assets = require('__ASSETS_FILE_PATH__'); 3 | const voltranConfig = require('../../../voltran.config'); 4 | import { QUERY_PARAMS } from './constants'; 5 | 6 | const assetsBaseUrl = !appConfig.mediaUrl ? appConfig.baseUrl : ''; 7 | const assetsPrefix = appConfig.mediaUrl.length ? appConfig.mediaUrl : appConfig.baseUrl; 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | const cssContentCache = {}; 13 | 14 | const cleanAssetsPrefixFromAssetURI = assetURI => { 15 | return assetURI.replace(assetsPrefix, ''); 16 | }; 17 | 18 | const readAsset = name => { 19 | return fs.readFileSync( 20 | path.resolve( 21 | process.cwd(), 22 | `${voltranConfig.publicDistFolder}/${cleanAssetsPrefixFromAssetURI(name)}` 23 | ), 24 | 'utf8' 25 | ); 26 | }; 27 | 28 | if (process.env.NODE_ENV === 'production') { 29 | Object.keys(assets).forEach(name => { 30 | if (!assets[name].css) { 31 | return; 32 | } 33 | 34 | if (Array.isArray(assets[name].css)) { 35 | assets[name].css.map(cssItem => { 36 | cssContentCache[cssItem] = readAsset(cssItem); 37 | }); 38 | } else { 39 | cssContentCache[name] = readAsset(assets[name].css); 40 | } 41 | }); 42 | } 43 | 44 | const getScripts = (name, subComponentFiles) => { 45 | const subComponentFilesScripts = subComponentFiles.scripts; 46 | const scripts = [ 47 | { 48 | src: `${assetsBaseUrl}${assets.client.js}`, 49 | isAsync: false 50 | }, 51 | { 52 | src: `${assetsBaseUrl}${assets[name].js}`, 53 | isAsync: false 54 | } 55 | ]; 56 | const mergedScripts = 57 | subComponentFilesScripts && subComponentFilesScripts.length > 0 58 | ? scripts.concat(subComponentFiles.scripts) 59 | : scripts; 60 | 61 | return mergedScripts; 62 | }; 63 | 64 | const getStyles = async (name, subComponentFiles, predefinedInitialState) => { 65 | const links = []; 66 | const withCriticalCss = predefinedInitialState.query[QUERY_PARAMS.WITH_CRITICAL_STYLES]; 67 | const subComponentFilesStyles = subComponentFiles.styles; 68 | 69 | if (assets[name].css) { 70 | links.push({ 71 | rel: 'stylesheet', 72 | href: `${assetsBaseUrl}${assets[name].css}`, 73 | criticalStyleComponent: 74 | process.env.NODE_ENV === 'production' && 75 | !voltranConfig.criticalCssDisabled && 76 | withCriticalCss 77 | ? cssContentCache[name] 78 | : null 79 | }); 80 | } 81 | 82 | if (assets.client.css) { 83 | links.push({ 84 | rel: 'stylesheet', 85 | href: `${assetsBaseUrl}${assets.client.css}`, 86 | criticalStyleComponent: 87 | process.env.NODE_ENV === 'production' && 88 | !voltranConfig.criticalCssDisabled && 89 | withCriticalCss 90 | ? cssContentCache.client 91 | : null 92 | }); 93 | } 94 | 95 | const mergedLinks = 96 | subComponentFilesStyles && subComponentFilesStyles.length > 0 97 | ? links.concat(subComponentFiles.styles) 98 | : links; 99 | 100 | return mergedLinks; 101 | }; 102 | 103 | const getActiveComponent = name => { 104 | const path = `/${name}`; 105 | 106 | return { 107 | resultPath: path, 108 | componentName: name, 109 | url: path 110 | }; 111 | }; 112 | 113 | const createBaseRenderHtmlProps = async (name, subComponentFiles, predefinedInitialState) => { 114 | return { 115 | scripts: getScripts(name, subComponentFiles), 116 | links: await getStyles(name, subComponentFiles, predefinedInitialState), 117 | activeComponent: getActiveComponent(name) 118 | }; 119 | }; 120 | 121 | export default createBaseRenderHtmlProps; 122 | -------------------------------------------------------------------------------- /src/universal/utils/constants.js: -------------------------------------------------------------------------------- 1 | const appConfig = require('__APP_CONFIG__'); 2 | 3 | const WINDOW_GLOBAL_PARAMS = { 4 | HISTORY: 'storefront.pwa.mobile.global.history' 5 | }; 6 | 7 | const HTTP_STATUS_CODES = { 8 | OK: 200, 9 | CREATED: 201, 10 | PARTIAL_CONTENT: 206, 11 | MOVED_PERMANENTLY: 301, 12 | FOUND: 302, 13 | NOT_FOUND: 404, 14 | INTERNAL_SERVER_ERROR: 500 15 | }; 16 | 17 | const JSON_CONTENT_TYPE = 'application/json'; 18 | const CONTENT_TYPE_HEADER = 'Content-Type'; 19 | const REQUEST_TYPES_WITH_BODY = ['post', 'put', 'patch']; 20 | const SERVICES = Object.freeze( 21 | Object.keys(appConfig.services).reduce((obj, val) => { 22 | // eslint-disable-next-line no-param-reassign 23 | obj[val] = Symbol(val); 24 | return obj; 25 | }, {}) 26 | ); 27 | 28 | const QUERY_PARAMS = { 29 | WITH_CRITICAL_STYLES: 'withCriticalCss' 30 | }; 31 | 32 | export { 33 | HTTP_STATUS_CODES, 34 | WINDOW_GLOBAL_PARAMS, 35 | JSON_CONTENT_TYPE, 36 | CONTENT_TYPE_HEADER, 37 | REQUEST_TYPES_WITH_BODY, 38 | SERVICES, 39 | QUERY_PARAMS 40 | }; 41 | -------------------------------------------------------------------------------- /src/universal/utils/helper.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const removeQueryStringFromUrl = url => { 3 | const partials = url.split('?'); 4 | 5 | return partials[0]; 6 | }; 7 | 8 | export const createComponentName = routePath => { 9 | return routePath.split('/').join(''); 10 | }; 11 | 12 | export function guid() { 13 | return `${s4()}${s4()}-${s4()}-4${s4().substr(0, 3)}-${s4()}-${s4()}${s4()}${s4()}`.toLowerCase(); 14 | } 15 | 16 | export function s4() { 17 | return Math.floor((1 + Math.random()) * 0x10000) 18 | .toString(16) 19 | .substring(1); 20 | } 21 | -------------------------------------------------------------------------------- /src/universal/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const application = 'voltran'; 3 | const currentThread = 'event-loop'; 4 | const sourceContext = 'app'; 5 | 6 | const logger = { 7 | formatter(level, message) { 8 | return `{"SourceContext":"${sourceContext}","@i":"${currentThread}","Application":"${application}","@l":"${level}", "@m":"${message}","@t":"${new Date().getTime()}"}`; 9 | }, 10 | 11 | info(message) { 12 | if (process.env.BROWSER && process.env.VOLTRAN_ENV === 'prod') { 13 | return; 14 | } 15 | 16 | console.log(this.formatter('INFO', message)); 17 | }, 18 | 19 | error(message) { 20 | if (process.env.BROWSER && process.env.VOLTRAN_ENV === 'prod') { 21 | return; 22 | } 23 | 24 | console.error(this.formatter('ERROR', message)); 25 | }, 26 | 27 | exception(exception, stack = true, requestPath = null) { 28 | if (process.env.BROWSER && process.env.VOLTRAN_ENV === 'prod') { 29 | return; 30 | } 31 | 32 | let message = ''; 33 | message += `Message: ${exception.message}`; 34 | if (stack) { 35 | message += `\\n Stack: ${exception.stack.replace(/\n/g, '\\n')}`; 36 | } 37 | 38 | console.error(this.formatter('ERROR', message, requestPath)); 39 | } 40 | }; 41 | 42 | export default logger; 43 | -------------------------------------------------------------------------------- /src/universal/utils/struct.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | const structUtils = { 3 | groupBy(list, getFn) { 4 | const map = {}; 5 | list.map(item => { 6 | const key = getFn(item); 7 | const collection = map[key]; 8 | if (!collection) { 9 | map[key] = [item]; 10 | } else { 11 | collection.push(item); 12 | } 13 | }); 14 | return map; 15 | } 16 | }; 17 | 18 | export default structUtils; 19 | -------------------------------------------------------------------------------- /webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const webpack = require("webpack"); 5 | const { merge } = require("webpack-merge"); 6 | const AssetsPlugin = require("assets-webpack-plugin"); 7 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 8 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 9 | const TerserWebpackPlugin = require("terser-webpack-plugin"); 10 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 11 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 12 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 13 | const { ESBuildMinifyPlugin } = require("esbuild-loader"); 14 | 15 | require("intersection-observer"); 16 | 17 | const { createComponentName } = require("./src/universal/utils/helper.js"); 18 | const packageJson = require(path.resolve(process.cwd(), "package.json")); 19 | 20 | const isBuildingForCDN = process.argv.includes("--for-cdn"); 21 | const env = process.env.VOLTRAN_ENV || "local"; 22 | 23 | const voltranConfig = require("./voltran.config"); 24 | const appConfigFilePath = `${voltranConfig.appConfigFile.entry}/${env}.conf.js`; 25 | const appConfig = require(appConfigFilePath); 26 | const commonConfig = require("./webpack.common.config"); 27 | const postCssConfig = require("./postcss.config"); 28 | const babelConfig = require("./babel.server.config"); 29 | 30 | const voltranClientConfigPath = voltranConfig.webpackConfiguration.client; 31 | const voltranClientConfig = voltranClientConfigPath 32 | ? require(voltranConfig.webpackConfiguration.client) 33 | : ""; 34 | 35 | const normalizeUrl = require("./lib/os.js"); 36 | const replaceString = require("./config/string.js"); 37 | 38 | const fragmentManifest = require(voltranConfig.routing.dictionary); 39 | 40 | const isDebug = voltranConfig.dev; 41 | const reScript = /\.(js|jsx|mjs)$/; 42 | const distFolderPath = voltranConfig.distFolder; 43 | const prometheusFile = voltranConfig.monitoring.prometheus; 44 | 45 | const chunks = {}; 46 | 47 | chunks.client = [ 48 | path.resolve(__dirname, "src/client/client.js") 49 | ]; 50 | 51 | for (const index in fragmentManifest) { 52 | if (!fragmentManifest[index]) { 53 | continue; 54 | } 55 | 56 | const fragment = fragmentManifest[index]; 57 | const fragmentUrl = fragment.path; 58 | const name = createComponentName(fragment.routePath); 59 | 60 | chunks[name] = [fragmentUrl]; 61 | } 62 | 63 | const GO_PIPELINE_LABEL = process.env.GO_PIPELINE_LABEL || packageJson.version; 64 | const appConfigFileTarget = `${voltranConfig.appConfigFile.output.path}/${voltranConfig.appConfigFile.output.name}.js`; 65 | 66 | fs.copyFileSync(appConfigFilePath, appConfigFileTarget); 67 | 68 | if (isDebug) { 69 | const appConfigJSONContent = require(appConfigFileTarget); 70 | 71 | for (const service in appConfigJSONContent.services) { 72 | appConfigJSONContent.services[service].clientUrl = 73 | appConfigJSONContent.services[service].serverUrl; 74 | } 75 | 76 | const moduleExportsText = "module.exports"; 77 | const appConfigFileContent = fs.readFileSync(appConfigFileTarget).toString(); 78 | const moduleExportsIndex = appConfigFileContent.indexOf(moduleExportsText); 79 | 80 | let context = appConfigFileContent.substr(0, moduleExportsIndex + moduleExportsText.length); 81 | context += "="; 82 | context += JSON.stringify(appConfigJSONContent); 83 | context += ";"; 84 | 85 | fs.writeFileSync(appConfigFileTarget, context); 86 | 87 | chunks.client.unshift( 88 | "regenerator-runtime/runtime.js", 89 | "core-js/stable", 90 | "intersection-observer" 91 | ); 92 | chunks.client.push("webpack-hot-middleware/client"); 93 | } 94 | 95 | const outputPath = voltranConfig.output.client.path; 96 | 97 | const clientConfig = merge(commonConfig, voltranClientConfig, { 98 | name: "client", 99 | 100 | target: "web", 101 | 102 | mode: isDebug ? "development" : "production", 103 | 104 | entry: chunks, 105 | 106 | output: { 107 | path: outputPath, 108 | publicPath: `${appConfig.mediaUrl}/project/assets/`, 109 | filename: voltranConfig.output.client.filename, 110 | chunkFilename: voltranConfig.output.client.chunkFilename, 111 | chunkLoadingGlobal: `WP_${voltranConfig.prefix.toUpperCase()}_VLTRN` 112 | }, 113 | 114 | module: { 115 | rules: [ 116 | { 117 | test: reScript, 118 | loader: "esbuild-loader", 119 | include: [path.resolve(__dirname, "src"), voltranConfig.inputFolder], 120 | options: { 121 | loader: "jsx", 122 | target: "es2015" 123 | } 124 | }, 125 | { 126 | test: /\.js$/, 127 | loader: "string-replace-loader", 128 | options: { 129 | multiple: [...replaceString()] 130 | } 131 | }, 132 | { 133 | test: /\.css$/, 134 | use: [ 135 | isDebug 136 | ? { 137 | loader: "style-loader", 138 | options: { 139 | injectType: "singletonStyleTag" 140 | } 141 | } 142 | : MiniCssExtractPlugin.loader, 143 | { 144 | loader: "css-loader", 145 | options: { 146 | modules: false, 147 | importLoaders: 1, 148 | sourceMap: isDebug 149 | } 150 | }, 151 | { 152 | loader: "postcss-loader", 153 | options: postCssConfig 154 | } 155 | ] 156 | }, 157 | { 158 | test: /\.scss$/, 159 | use: [ 160 | isDebug 161 | ? { 162 | loader: "style-loader", 163 | options: { 164 | injectType: "singletonStyleTag" 165 | } 166 | } 167 | : MiniCssExtractPlugin.loader, 168 | { 169 | loader: "css-loader", 170 | options: { 171 | modules: { 172 | localIdentName: appConfig.isCssClassNameObfuscationEnabled 173 | ? `${voltranConfig.prefix}-[name]-[hash:base64]` 174 | : `${voltranConfig.prefix}-[path][name]__[local]`, 175 | localIdentHashSalt: packageJson.name 176 | }, 177 | importLoaders: 2, 178 | sourceMap: isDebug 179 | } 180 | }, 181 | { 182 | loader: "postcss-loader", 183 | options: postCssConfig 184 | }, 185 | { 186 | loader: "sass-loader", 187 | options: { 188 | implementation: require("sass"), 189 | sassOptions: { 190 | outputStyle: "compressed" 191 | } 192 | } 193 | }, 194 | ...(voltranConfig.sassResources 195 | ? [ 196 | { 197 | loader: "sass-resources-loader", 198 | options: { 199 | sourceMap: false, 200 | resources: voltranConfig.sassResources 201 | } 202 | } 203 | ] 204 | : []) 205 | ] 206 | } 207 | ] 208 | }, 209 | 210 | optimization: { 211 | // emitOnErrors: false, 212 | minimizer: [ 213 | new ESBuildMinifyPlugin({ 214 | target: "es2015", 215 | css: true 216 | }), 217 | new TerserWebpackPlugin({ 218 | terserOptions: { 219 | mangle: { 220 | safari10: true 221 | } 222 | } 223 | }), 224 | new CssMinimizerPlugin({}) 225 | ] 226 | }, 227 | 228 | resolve: { 229 | alias: { 230 | "react": path.resolve(process.cwd(), "node_modules/react"), 231 | "react-dom": path.resolve(process.cwd(), "node_modules/react-dom") 232 | } 233 | }, 234 | 235 | plugins: [ 236 | ...(isBuildingForCDN 237 | ? [] 238 | : [ 239 | new CleanWebpackPlugin({ 240 | verbose: false, 241 | dangerouslyAllowCleanPatternsOutsideProject: true 242 | }) 243 | ]), 244 | 245 | new webpack.DefinePlugin({ 246 | "process.env": {}, 247 | "process.env.BROWSER": true, 248 | __DEV__: isDebug, 249 | GO_PIPELINE_LABEL: JSON.stringify(GO_PIPELINE_LABEL) 250 | }), 251 | 252 | new CopyWebpackPlugin([ 253 | { 254 | from: voltranConfig.output.client.publicPath, 255 | to: voltranConfig.publicDistFolder 256 | } 257 | ]), 258 | 259 | ...(isDebug 260 | ? [new webpack.HotModuleReplacementPlugin()] 261 | : [ 262 | new MiniCssExtractPlugin({ 263 | filename: "[name].css", 264 | chunkFilename: "[id]-[contenthash].css" 265 | }) 266 | ]), 267 | 268 | new AssetsPlugin({ 269 | path: voltranConfig.inputFolder, 270 | filename: "assets.json", 271 | prettyPrint: true 272 | }), 273 | 274 | ...(appConfig?.bundleAnalyzerStaticEnabled ? [new BundleAnalyzerPlugin({analyzerMode: 'static', openAnalyzer: false})] : []) 275 | ] 276 | }); 277 | 278 | module.exports = clientConfig; 279 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const {merge} = require('webpack-merge'); 4 | const packageJson = require(path.resolve(process.cwd(), 'package.json')); 5 | const voltranConfig = require('./voltran.config'); 6 | 7 | const reStyle = /\.(css|less|styl|scss|sass|sss)$/; 8 | const reImage = /\.(bmp|gif|jpg|jpeg|png|svg)$/; 9 | const staticAssetName = '[name]-[contenthash].[ext]'; 10 | 11 | const isDebug = voltranConfig.dev; 12 | 13 | const voltranCommonConfigPath = voltranConfig.webpackConfiguration.common; 14 | const voltranCommonConfig = voltranCommonConfigPath 15 | ? require(voltranConfig.webpackConfiguration.common) 16 | : ''; 17 | 18 | const commonConfig = merge(voltranCommonConfig, { 19 | mode: isDebug ? 'development' : 'production', 20 | 21 | output: { 22 | assetModuleFilename: staticAssetName, 23 | }, 24 | 25 | module: { 26 | // Make missing exports an error instead of warning 27 | strictExportPresence: true, 28 | 29 | rules: [ 30 | // Rules for images 31 | { 32 | test: reImage, 33 | oneOf: [ 34 | // Inline lightweight images into CSS 35 | { 36 | issuer: reStyle, 37 | oneOf: [ 38 | // Inline lightweight SVGs as UTF-8 encoded DataUrl string 39 | { 40 | test: /\.svg$/, 41 | type: 'asset', 42 | use: 'svg-url-loader', 43 | parser: { 44 | dataUrlCondition: { 45 | maxSize: 4096, // 4kb 46 | }, 47 | }, 48 | }, 49 | 50 | // Inline lightweight images as Base64 encoded DataUrl string 51 | { 52 | type: 'asset', 53 | parser: { 54 | dataUrlCondition: { 55 | maxSize: 4096, // 4kb 56 | }, 57 | }, 58 | }, 59 | ], 60 | }, 61 | 62 | { 63 | type: 'asset/resource', 64 | }, 65 | ], 66 | }, 67 | 68 | { 69 | test: /\.(png|jpg|jpeg|gif|svg)$/, 70 | type: 'asset', 71 | parser: { 72 | dataUrlCondition: { 73 | maxSize: 10000, 74 | }, 75 | }, 76 | }, 77 | 78 | { 79 | test: /\.(ttf|eot|otf|woff|woff2)$/, 80 | type: 'asset/resource', 81 | } 82 | ] 83 | }, 84 | 85 | stats: 'errors-only', 86 | 87 | // Choose a developer tool to enhance debugging 88 | // https://webpack.js.org/configuration/devtool/#devtool 89 | devtool: isDebug ? 'inline-cheap-module-source-map' : 'source-map', 90 | 91 | plugins: [ 92 | new webpack.DefinePlugin({ 93 | VOLTRAN_API_VERSION: JSON.stringify(packageJson.version), 94 | 'process.env.GO_PIPELINE_LABEL': JSON.stringify(process.env.GO_PIPELINE_LABEL), 95 | }), 96 | ], 97 | resolve: { 98 | alias: { 99 | // 'styled-components': path.resolve(__dirname, 'node_modules', 'styled-components'), 100 | // 'postcss-loader': path.resolve(__dirname, 'node_modules', 'postcss-loader'), 101 | }, 102 | fallback: { 103 | url: false, 104 | }, 105 | }, 106 | }); 107 | 108 | module.exports = commonConfig; 109 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const {merge} = require('webpack-merge'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 6 | 7 | const env = process.env.VOLTRAN_ENV || 'local'; 8 | 9 | const voltranConfig = require('./voltran.config'); 10 | 11 | const appConfigFilePath = `${voltranConfig.appConfigFile.entry}/${env}.conf.js`; 12 | const appConfig = require(appConfigFilePath); // eslint-disable-line import/no-dynamic-require 13 | 14 | const commonConfig = require('./webpack.common.config'); 15 | const postCssConfig = require('./postcss.config'); 16 | const packageJson = require(path.resolve(process.cwd(), 'package.json')); 17 | const replaceString = require('./config/string.js'); 18 | 19 | const distFolderPath = voltranConfig.distFolder; 20 | const isDebug = voltranConfig.dev; 21 | 22 | let styles = ''; 23 | 24 | for (let i = 0; i < voltranConfig.styles.length; i++) { 25 | styles += `require('${voltranConfig.styles[i]}');`; 26 | } 27 | const voltranServerConfigPath = voltranConfig.webpackConfiguration.server; 28 | const voltranServerConfig = voltranServerConfigPath 29 | ? require(voltranConfig.webpackConfiguration.server) 30 | : ''; 31 | 32 | const serverConfig = merge(commonConfig, voltranServerConfig, { 33 | name: 'server', 34 | 35 | target: 'node', 36 | 37 | mode: isDebug ? 'development' : 'production', 38 | 39 | entry: { 40 | server: [path.resolve(__dirname, isDebug ? 'src/server.js' : 'src/main.js')], 41 | }, 42 | 43 | output: { 44 | path: voltranConfig.output.server.path, 45 | filename: voltranConfig.output.server.filename, 46 | libraryTarget: 'commonjs2', 47 | }, 48 | 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.(js|jsx|mjs)$/, 53 | loader: 'esbuild-loader', 54 | include: [path.resolve(__dirname, 'src'), voltranConfig.inputFolder], 55 | options: { 56 | loader: 'jsx', 57 | target: 'es2015', 58 | }, 59 | }, 60 | { 61 | test: /\.js$/, 62 | loader: 'string-replace-loader', 63 | options: { 64 | multiple: [...replaceString()], 65 | }, 66 | }, 67 | { 68 | test: /\.scss$/, 69 | use: [ 70 | { 71 | loader: 'css-loader', 72 | options: { 73 | modules: { 74 | localIdentName: appConfig.isCssClassNameObfuscationEnabled 75 | ? `${voltranConfig.prefix}-[name]-[hash:base64]` 76 | : `${voltranConfig.prefix}-[path][name]__[local]`, 77 | localIdentHashSalt: packageJson.name, 78 | exportOnlyLocals: true, 79 | }, 80 | importLoaders: 1, 81 | sourceMap: isDebug, 82 | } 83 | }, 84 | { 85 | loader: 'postcss-loader', 86 | options: postCssConfig 87 | }, 88 | { 89 | loader: 'sass-loader', 90 | }, 91 | ...(voltranConfig.sassResources 92 | ? [ 93 | { 94 | loader: 'sass-resources-loader', 95 | options: { 96 | sourceMap: false, 97 | resources: voltranConfig.sassResources, 98 | }, 99 | }, 100 | ] 101 | : []) 102 | ] 103 | } 104 | ] 105 | }, 106 | 107 | externalsPresets: {node: true}, 108 | externals: [ 109 | nodeExternals(), 110 | ], 111 | 112 | plugins: [ 113 | new CleanWebpackPlugin({ 114 | verbose: false, 115 | dangerouslyAllowCleanPatternsOutsideProject: true, 116 | }), 117 | 118 | new webpack.DefinePlugin({ 119 | 'process.env.BROWSER': false, 120 | __DEV__: isDebug, 121 | }), 122 | 123 | ...(isDebug ? [new webpack.HotModuleReplacementPlugin()] : []) 124 | ] 125 | }); 126 | 127 | module.exports = serverConfig; 128 | --------------------------------------------------------------------------------