├── .codeclimate.yml ├── .editorconfig ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── README.md ├── build └── config.gypi ├── lib ├── index.js ├── models │ └── layout.js ├── renderer │ ├── process-style.js │ ├── renderer-vueify.js │ └── renderer-webpack.js └── utils │ ├── cache.js │ ├── config.js │ ├── findPaths.js │ ├── head.js │ ├── index.js │ ├── layout-webpack.js │ ├── layout.js │ ├── promiseFS.js │ ├── stream.js │ └── vueVersion.js ├── nodemon.json ├── package-lock.json ├── package.json ├── tests ├── example │ ├── components │ │ ├── component.vue │ │ ├── copy │ │ │ └── component.vue │ │ ├── inner.vue │ │ ├── message-comp.vue │ │ ├── subcomponent.vue │ │ └── users.vue │ ├── express.js │ ├── expressvue.config.js │ ├── index-vueify.js │ ├── index-webpack.js │ ├── mixins │ │ └── exampleMixin.js │ └── views │ │ ├── error.vue │ │ └── index │ │ ├── index-webpack.vue │ │ ├── index-with-props.vue │ │ └── index.vue ├── expected │ ├── stream-full-object.html │ ├── stream-no-object.html │ ├── string-full-config.html │ ├── string-some-config.html │ ├── string-zero-config.html │ └── string-zero-object.html ├── renderer-vueify.js ├── renderer-webpack.js ├── renderer.json └── utils │ ├── cache.js │ ├── config.js │ ├── findPaths.js │ ├── head.js │ ├── layout.js │ ├── stream.js │ └── vueVersion.js └── tsconfig.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | channel: "eslint-5" 14 | fixme: 15 | enabled: true 16 | ratings: 17 | paths: 18 | - "**.inc" 19 | - "**.js" 20 | - "**.jsx" 21 | - "**.module" 22 | - "**.php" 23 | - "**.py" 24 | - "**.rb" 25 | exclude_paths: 26 | - tests/ 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | 8 | [*.yml] 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master, develop ] 9 | pull_request: 10 | branches: [ master, develop ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | \.nyc_output/ 30 | .expressvue 31 | *.clinic-doctor 32 | *.clinic-flame 33 | *.clinic-doctor.html 34 | *.clinic-flame.html 35 | 36 | .expressvue 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v10 4 | - v8 5 | before_install: 6 | - npm install -g npm 7 | - npm install -g codecov 8 | - npm install -g greenkeeper-lockfile@1 9 | before_script: greenkeeper-lockfile-update 10 | after_script: greenkeeper-lockfile-upload 11 | after_success: npm run coverage 12 | deploy: 13 | - provider: npm 14 | skip_cleanup: true 15 | email: daniel@cherubini.casa 16 | on: 17 | all_branches: true 18 | tags: true 19 | repo: express-vue/vue-pronto 20 | api_key: 21 | secure: Fw5+Z4k/meEj0s6uQkdQGu+w2TefFNBV37szGGFzhcCDmJocSZGoNB46YoMNayxtW6MKyxKJuXbbc+gcGQBdnWK/1FesOFPoKR2ok8774Gb83P5aO/AnQ1bSK5n4pqCSZdkADW15NyYs+ugDtHtVFB32SaHV4saSN6mJOGWTXH2y7PtVUcM05wYAGk/xZpqorApNa660bBdX0W123uTw29boG4fE/SNsxCNi6vuOi+JjtNJ4QsNSrEUPSgsghUUIZ1toiZhlBRjOrzs9RcAdRKQjuPfF6/WnShzBIVym5mPJvfROUleAAsBHw74CiNLv8iV6AtESklp9UBg6Tfn1vhMZir4dLVsAa9KE78gr7lDMGYyvUk/tr0chzkIro045dClE3dcD+ggFapxiOwEY6mTQ5SbS3hs/4FH3FsNugS0/abJSCZPhydJuhZ4T7V2sM4Y0TxnldXWib+kuMJwq0sHBfXf0akE6kpntjsuir6bAhLDA4U2XdGfbB9BjtBfkSpdicXmbmb+7Wh4E/j7e7M9dbfWzuDrrglBn3Pw5FWLUl9pMpwUJqO/YLhxVUaREiel0IkFzSa4Ejxqcaxfy6QZZZgqtnwo62v47fYnMUEXep77e3az1PXp+MeBTqa/hnkrjXcNEV3AxBlbLX9hg/4VwA5W2YkIbZmKUa/+rD1s= 22 | addons: 23 | code_climate: 24 | repo_token: fdc3db1ac144125b5f5a11900fdaa719a4b882b55b4d5ea307a21164e01cce8a 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach", 12 | "restart": true, 13 | "protocol": "inspector", 14 | "port": 9229 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at daniel@cherubini.casa. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Please: 4 | 1. Do all work in feature branches and open PR into develop. 5 | 2. Use git-flow. 6 | 3. Use flow-type. 7 | 4. Write tests in ava. 8 | 9 | Pull requests without flow typing will not be accepted without flow typing.. please read the flow docs if you need help 10 | https://flow.org/en/docs/ 11 | 12 | Pull requests require 1 review to merge, this will probably be changed later when we have more people. 13 | 14 | ## ALL PULL REQUESTS MUST GO TO DEVELOP! 15 | 16 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Description of Issue 5 | 6 | 7 | 8 | 9 | ### Stack Trace / Console Log 10 | 11 | 12 | 13 | 14 | ### Additional Comments 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "{}" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright {yyyy} {name of copyright owner} 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-pronto 2 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][cov-image]][cov-url] [![Greenkeeper badge](https://badges.greenkeeper.io/express-vue/vue-pronto.svg)](https://greenkeeper.io/) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/51e27f21101e492fabf93dc6d81b8f28)](https://www.codacy.com/app/intothemild/vue-pronto?utm_source=github.com&utm_medium=referral&utm_content=express-vue/vue-pronto&utm_campaign=badger) 3 | > Rendering Engine for turning Vue files into Javascript Objects 4 | 5 | ## Installation 6 | 7 | ```sh 8 | $ npm install --save vue-pronto 9 | ``` 10 | 11 | ## Usage 12 | 13 | Include the library at the top level like so 14 | 15 | ```js 16 | const Pronto = require('vue-pronto'); 17 | ``` 18 | 19 | Then init the renderer 20 | 21 | ```js 22 | const renderer = new Pronto({object}); 23 | ``` 24 | 25 | This returns 2 main functions. 26 | It takes 3 params, 2 required and one optional. 27 | ```js 28 | renderer.RenderToString(componentPath, data, [vueOptions]); 29 | renderer.RenderToStream(componentPath, data, [vueOptions]); 30 | ``` 31 | 32 | Both methods return a promise. Stream returns a stream, and String returns a string. 33 | 34 | ## RenderToStream 35 | 36 | 37 | ### renderer.RenderToStream(vuefile, data, vueOptions) ⇒ Promise 38 | renderToStream returns a stream from res.renderVue to the client 39 | 40 | **Kind**: instance method of [Renderer](#Renderer) 41 | **Returns**: Promise - - Promise returns a Stream 42 | 43 | | Param | Type | Description | 44 | | --- | --- | --- | 45 | | vuefile | string | full path to .vue component | 46 | | data | Object | data to be inserted when generating vue class | 47 | | vueOptions | Object | vue options to be used when generating head | 48 | 49 | ## RenderToString 50 | 51 | ### renderer.RenderToString(vuefile, data, vueOptions) ⇒ Promise 52 | renderToStream returns a string from res.renderVue to the client 53 | 54 | **Kind**: instance method of [Renderer](#Renderer) 55 | 56 | | Param | Type | 57 | | --- | --- | 58 | | vuefile | string | 59 | | data | object | 60 | | vueOptions | object | 61 | 62 | 63 | ## VueOptions 64 | 65 | ```js 66 | { 67 | pagesPath: path.join(__dirname, '/../tests'), 68 | vueVersion: "2.3.4", 69 | template: { 70 | body: { 71 | start: '
', 72 | end: '
' 73 | } 74 | }, 75 | webpack: { 76 | /** 77 | * Webpack Server and Client Configs go here 78 | * Takes webpack configs and uses webpack-merge to merge them 79 | * */ 80 | client: {}, 81 | server: {} 82 | }, 83 | vue: { 84 | /** 85 | * This is where you put the string versions of the 86 | * entry.js for server and client 87 | * app is for the app entry.js 88 | * */ 89 | app: 'string', 90 | client: 'string', 91 | server: 'string', 92 | }, 93 | head: { 94 | metas: [ 95 | { 96 | property: 'og:title', 97 | content: 'Page Title' 98 | }, 99 | { 100 | name: 'twitter:title', 101 | content: 'Page Title' 102 | }, 103 | { 104 | name: 'viewport', 105 | content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' 106 | } 107 | ], 108 | scripts: [ 109 | {src: 'https://unpkg.com/vue@2.3.4/dist/vue.js'} 110 | ], 111 | styles: [ 112 | 113 | ] 114 | } 115 | data: { 116 | thing: true 117 | } 118 | ``` 119 | 120 | 121 | ## License 122 | 123 | Apache-2.0 © [Daniel Cherubini](https://github.com/express-vue) 124 | 125 | 126 | [npm-image]: https://badge.fury.io/js/vue-pronto.svg 127 | [npm-url]: https://npmjs.org/package/vue-pronto 128 | [travis-image]: https://travis-ci.org/express-vue/vue-pronto.svg?branch=master 129 | [travis-url]: https://travis-ci.org/express-vue/vue-pronto 130 | [daviddm-image]: https://david-dm.org/express-vue/vue-pronto.svg?theme=shields.io 131 | [daviddm-url]: https://david-dm.org/express-vue/vue-pronto 132 | [cov-image]: https://codecov.io/gh/express-vue/vue-pronto/branch/master/graph/badge.svg 133 | [cov-url]: https://codecov.io/gh/express-vue/vue-pronto 134 | 135 | -------------------------------------------------------------------------------- /build/config.gypi: -------------------------------------------------------------------------------- 1 | # Do not edit. File was generated by node-gyp's "configure" step 2 | { 3 | "target_defaults": { 4 | "cflags": [], 5 | "default_configuration": "Release", 6 | "defines": [], 7 | "include_dirs": [], 8 | "libraries": [] 9 | }, 10 | "variables": { 11 | "asan": 0, 12 | "coverage": "false", 13 | "debug_devtools": "node", 14 | "debug_http2": "false", 15 | "debug_nghttp2": "false", 16 | "force_dynamic_crt": 0, 17 | "host_arch": "x64", 18 | "icu_data_file": "icudt59l.dat", 19 | "icu_data_in": "../../deps/icu-small/source/data/in/icudt59l.dat", 20 | "icu_endianness": "l", 21 | "icu_gyp_path": "tools/icu/icu-generic.gyp", 22 | "icu_locales": "en,root", 23 | "icu_path": "deps/icu-small", 24 | "icu_small": "true", 25 | "icu_ver_major": "59", 26 | "llvm_version": 0, 27 | "node_byteorder": "little", 28 | "node_enable_d8": "false", 29 | "node_enable_v8_vtunejit": "false", 30 | "node_install_npm": "true", 31 | "node_module_version": 57, 32 | "node_no_browser_globals": "false", 33 | "node_prefix": "/", 34 | "node_release_urlbase": "https://nodejs.org/download/release/", 35 | "node_shared": "false", 36 | "node_shared_cares": "false", 37 | "node_shared_http_parser": "false", 38 | "node_shared_libuv": "false", 39 | "node_shared_openssl": "false", 40 | "node_shared_zlib": "false", 41 | "node_tag": "", 42 | "node_use_bundled_v8": "true", 43 | "node_use_dtrace": "true", 44 | "node_use_etw": "false", 45 | "node_use_lttng": "false", 46 | "node_use_openssl": "true", 47 | "node_use_perfctr": "false", 48 | "node_use_v8_platform": "true", 49 | "node_without_node_options": "false", 50 | "openssl_fips": "", 51 | "openssl_no_asm": 0, 52 | "shlib_suffix": "57.dylib", 53 | "target_arch": "x64", 54 | "uv_parent_path": "/deps/uv/", 55 | "uv_use_dtrace": "true", 56 | "v8_enable_gdbjit": 0, 57 | "v8_enable_i18n_support": 1, 58 | "v8_enable_inspector": 1, 59 | "v8_no_strict_aliasing": 1, 60 | "v8_optimized_debug": 0, 61 | "v8_promise_internal_field_count": 1, 62 | "v8_random_seed": 0, 63 | "v8_trace_maps": 0, 64 | "v8_use_snapshot": "true", 65 | "want_separate_host_toolset": 0, 66 | "xcode_version": "7.0", 67 | "nodedir": "/Users/danielcherubini/.node-gyp/8.9.0", 68 | "standalone_static_library": 1 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const RendererVueify = require("./renderer/renderer-vueify"); 4 | const RendererWebpack = require("./renderer/renderer-webpack"); 5 | 6 | module.exports.ProntoVueify = RendererVueify; 7 | module.exports.ProntoWebpack = RendererWebpack; 8 | -------------------------------------------------------------------------------- /lib/models/layout.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | class HTML { 4 | /** 5 | * @param {object} html 6 | */ 7 | constructor(html = {}) { 8 | this.start = html.start ? html.start : ""; 9 | this.end = html.end ? html.end : ""; 10 | } 11 | } 12 | 13 | class Body { 14 | /** 15 | * @param {object} body 16 | */ 17 | constructor(body = {}) { 18 | this.start = body.start ? body.start : ""; 19 | this.end = body.end ? body.end : ""; 20 | } 21 | } 22 | 23 | class Template { 24 | /** 25 | * @param {object} template 26 | */ 27 | constructor(template = {}) { 28 | this.start = template.start ? template.start : "
"; 29 | this.end = template.end ? template.end : "
"; 30 | } 31 | } 32 | 33 | class Layout { 34 | /** 35 | * Creates a layout model 36 | * @constructor 37 | * @param {object} obj 38 | * @property {HTML} html 39 | * @property {Body} body 40 | * @property {Template} template 41 | */ 42 | constructor(obj = {}) { 43 | this.html = new HTML(obj.html); 44 | this.body = new Body(obj.body); 45 | this.template = new Template(obj.template); 46 | } 47 | } 48 | 49 | module.exports.Layout = Layout; 50 | module.exports.HTML = HTML; 51 | module.exports.Body = Body; 52 | module.exports.Template = Template; 53 | -------------------------------------------------------------------------------- /lib/renderer/process-style.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require("fs"); 3 | const CleanCss = require("clean-css"); 4 | const autoprefixer = require("autoprefixer"); 5 | const cssMinify = new CleanCss(); 6 | 7 | //@ts-ignore 8 | const compilers = require("vueify/lib/compilers"); 9 | // @ts-ignore 10 | const rewriteStyle = require("vueify/lib/style-rewriter"); 11 | const path = require("path"); 12 | //@ts-ignore 13 | const chalk = require("chalk"); 14 | 15 | /** 16 | * 17 | * @param {object} part 18 | * @param {string} filePath 19 | * @param {string} id 20 | * @returns {Promise} 21 | */ 22 | function processStyle(part, filePath, id) { 23 | var style = getContent(part, filePath); 24 | return new Promise((resolve, reject) => { 25 | compileAsPromise("style", style, part.lang, filePath) 26 | .then(compiledStyle => { 27 | // @ts-ignore 28 | return rewriteStyle(id, compiledStyle, part.scoped, { postcss: [autoprefixer()] }).then(function(res) { 29 | const minified = minifyStyles(res); 30 | resolve(minified); 31 | }); 32 | }) 33 | .catch(reject); 34 | }); 35 | } 36 | 37 | /** 38 | * 39 | * @param {string} style 40 | * @returns {string} 41 | */ 42 | function minifyStyles(style) { 43 | const minified = cssMinify.minify(style); 44 | return minified.styles; 45 | } 46 | 47 | /** 48 | * 49 | * @param {*} part 50 | * @param {*} filePath 51 | * @returns {*} 52 | */ 53 | function getContent(part, filePath) { 54 | return part.src 55 | ? loadSrc(part.src, filePath) 56 | : part.content; 57 | } 58 | 59 | /** 60 | * 61 | * @param {*} src 62 | * @param {*} filePath 63 | * @returns {string} 64 | */ 65 | function loadSrc(src, filePath) { 66 | var dir = path.dirname(filePath); 67 | var srcPath = path.resolve(dir, src); 68 | try { 69 | return fs.readFileSync(srcPath, "utf-8"); 70 | } catch (e) { 71 | //@ts-ignore 72 | console.error(chalk.red(`Failed to load src: ${src} from file: ${filePath}`)); 73 | throw e; 74 | } 75 | } 76 | 77 | /** 78 | * 79 | * @param {*} type 80 | * @param {*} source 81 | * @param {*} lang 82 | * @param {*} filePath 83 | * @returns {Promise} 84 | */ 85 | function compileAsPromise(type, source, lang, filePath) { 86 | var compile = compilers[lang]; 87 | if (compile) { 88 | compile.options = compile.options || {}; 89 | compile.emit = compile.emit || function() { 90 | return true; 91 | }; 92 | return new Promise(function(resolve, reject) { 93 | /** 94 | * @param {Object} err 95 | * @param {Object} res 96 | */ 97 | function compileFunction(err, res) { 98 | if (err) { 99 | // report babel error codeframe 100 | if (err.codeFrame) { 101 | process.nextTick(function() { 102 | console.error(err.codeFrame); 103 | }); 104 | } 105 | return reject(err); 106 | } 107 | 108 | resolve(res.trim()); 109 | } 110 | compile(source, compileFunction, compile, filePath); 111 | }); 112 | } else { 113 | return Promise.resolve(source); 114 | } 115 | } 116 | 117 | module.exports.processStyle = processStyle; 118 | -------------------------------------------------------------------------------- /lib/renderer/renderer-vueify.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const requireFromString = require("require-from-string"); 5 | // @ts-ignore 6 | const vueCompiler = require("vue-template-compiler"); 7 | // @ts-ignore 8 | const vueify = require("vueify"); 9 | vueify.compiler.loadConfig(); 10 | const vueServerRenderer = require("vue-server-renderer"); 11 | const Vue = require("vue"); 12 | const uglify = require("uglify-js"); 13 | const LRU = require("lru-cache"); 14 | const processStyle = require("./process-style").processStyle; 15 | const Utils = require("../utils"); 16 | // @ts-ignore 17 | const find = require("find"); 18 | // @ts-ignore 19 | const jsToString = require("js-to-string"); 20 | 21 | /** 22 | * @typedef CompiledObjectType 23 | * @prop {String} compiled 24 | * @prop {String} style 25 | * @prop {String} filePath 26 | */ 27 | 28 | /** 29 | * @typedef VueOptionsType 30 | * @prop {String} title 31 | * @prop {Object} head 32 | * @prop {Object[]} head.scripts 33 | * @prop {Object[]} head.metas 34 | * @prop {Object[]} head.styles 35 | * @prop {Layout} template 36 | */ 37 | 38 | /** 39 | * @typedef ConfigObjectType 40 | * @prop {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache 41 | * @prop {String} rootPath 42 | * @prop {String} vueVersion 43 | * @prop {(VueOptionsType|Object)} head 44 | * @prop {Object} data 45 | * @prop {Object} babel 46 | */ 47 | 48 | class Renderer { 49 | /** 50 | * @param {(ConfigObjectType|Object)} [options={}] - options for constructor 51 | * @property {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache 52 | * @property {LRU} lruCache - LRU Cache 53 | * @property {vueServerRenderer} renderer - instance of vue server renderer 54 | * @property {String} rootPath 55 | * @property {String} nodeModulesPath 56 | * @property {String} vueVersion 57 | * @property {Object} babel 58 | */ 59 | constructor(options = {}) { 60 | 61 | this.cacheOptions = options.cacheOptions || { 62 | max: 500, 63 | maxAge: 1000 * 60 * 60, 64 | }; 65 | this.renderer = vueServerRenderer.createRenderer(); 66 | this.lruCache = new LRU(this.cacheOptions); 67 | this.internalCache = new Utils.Cache(); 68 | this.head = options.head || {}; 69 | this.data = options.data || {}; 70 | this.propsData = options.propsData || {}; 71 | this.template = options.template || {}; 72 | const version = Utils.vueVersion(options.vueVersion); 73 | if (version.enabled) { 74 | if (this.head.scripts) { 75 | this.head.scripts.push(version.script); 76 | } else { 77 | this.head.scripts = [version.script]; 78 | } 79 | } 80 | this.rootPath = Utils.findRootPath(options.rootPath); 81 | this.babel = options.babel; 82 | if (options.babel) { 83 | vueify.compiler.applyConfig({ babel: options.babel}); 84 | } 85 | if (!process.env.VUE_DEV) { 86 | // find all the vue files and try to precompile them and store them in cache 87 | find.file(/\.vue$/, this.rootPath, 88 | /** @param {array} files */ 89 | files => { 90 | //Make Bundle for each component and store it in cache 91 | for (let filePath of files) { 92 | this.MakeVueClass(filePath, {}) 93 | .then(() => { 94 | const debug = `Precached -> ${filePath}`; 95 | if (!process.env.TEST) { 96 | console.info(debug); 97 | } 98 | }) 99 | .catch(error => { 100 | console.error(`Error Precaching \nfilePath -> ${filePath}\n-------\n${error}`); 101 | }); 102 | } 103 | }); 104 | } 105 | 106 | this.nodeModulesPath = Utils.findNodeModules(this.rootPath); 107 | this.jsToStringOptions = { 108 | functions: [ 109 | { 110 | name: "data", 111 | /** 112 | * @param {function} script 113 | * @return {string|void} 114 | */ 115 | toString: function(script) { 116 | const func = `module.exports = function data() { return ${jsToString(script())}; };`; 117 | const required = requireFromString(func); 118 | return String(required); 119 | }, 120 | }, 121 | ], 122 | }; 123 | 124 | } 125 | /** 126 | * @param {Object} oldData 127 | * @param {Object} newData 128 | * @returns {Function} 129 | */ 130 | FixData(oldData, newData) { 131 | const mergedData = Object.assign({}, oldData, this.data, newData); 132 | return function data() { 133 | return mergedData; 134 | }; 135 | } 136 | /** 137 | * @param {Object} oldPropsData 138 | * @param {Object} newPropsData 139 | * @returns {Function} 140 | */ 141 | FixPropsData(oldPropsData, newPropsData) { 142 | return Object.assign({}, oldPropsData, this.propsData, newPropsData); 143 | } 144 | /** 145 | * @param {string} componentFile 146 | * @param {string} filePath 147 | * @param {string} vueComponentMatch 148 | * @returns {Promise<{compiled: string, filePath: string, match: string, style: string}>} 149 | */ 150 | BuildComponent(componentFile, filePath, vueComponentMatch) { 151 | return new Promise((resolve, reject) => { 152 | const cachedComponent = this.lruCache.get(vueComponentMatch); 153 | if (cachedComponent) { 154 | resolve(cachedComponent); 155 | } else { 156 | const parsedDirectory = filePath.includes("node_modules") ? filePath : path.parse(filePath).dir; 157 | const relativePath = path.resolve(parsedDirectory, componentFile); 158 | this.Compile(relativePath) 159 | .then(compiled => { 160 | this.FindAndReplaceComponents(compiled, relativePath) 161 | .then((compiledObject) => { 162 | let reg; 163 | const isES6 = compiledObject.compiled.includes("use strict"); 164 | if (isES6) { 165 | reg = /(?:"use strict";)(.*)(?:module.exports={|exports.default={)(.*)(?:}}\(\);?)(?:.*)(?:__vue__options__.render=function\(\){)(.*)(?:},?;?__vue__options__.staticRenderFns=\[)(.*)(?:\])((?:,?;?__vue__options__._scopeId=")(.*)(?:"))?/gm; 166 | } else { 167 | reg = /(?!"use strict";)(.*)(?:module.exports={|exports.default={)(.*)(?:}\(?\)?;?)(?:.*)(?:__vue__options__.render=function\(\){)(.*)(?:},?;?__vue__options__.staticRenderFns=\[)(.*)(?:\])((?:,?;?__vue__options__._scopeId=")(.*)(?:"))?/gm; 168 | } 169 | 170 | let vueComponent = ""; 171 | let imports = ""; 172 | let moduleExports = ""; 173 | let renderFunctionContents = ""; 174 | let staticRenderFns = ""; 175 | let scopeId = ""; 176 | 177 | let { code } = uglify.minify(compiledObject.compiled, { mangle: false }); 178 | 179 | const matches = reg.exec(code); 180 | if (matches && matches.length > 0) { 181 | const importMatch = matches[1]; 182 | const exportMatch = matches[2]; 183 | const renderMatch = matches[3]; 184 | const staticMatch = matches[4]; 185 | const scopeMatch = matches[6]; 186 | 187 | if (importMatch && importMatch !== "") { 188 | imports = importMatch; 189 | } 190 | 191 | if (exportMatch && exportMatch !== "") { 192 | moduleExports = exportMatch; 193 | } 194 | 195 | if (renderMatch && renderMatch !== "") { 196 | renderFunctionContents = `,render: function render() {${renderMatch}}`; 197 | } 198 | 199 | if (staticMatch && staticMatch !== "") { 200 | staticRenderFns = `,staticRenderFns: [${staticMatch}]`; 201 | } 202 | 203 | if (scopeMatch && scopeMatch !== "") { 204 | scopeId = `,_scopeId: "${scopeMatch}"`; 205 | } 206 | 207 | } 208 | 209 | if (imports === "") { 210 | vueComponent = `{${moduleExports}${renderFunctionContents}${staticRenderFns}${scopeId}}`; 211 | } else { 212 | vueComponent = `function(){${imports}return {${moduleExports}${renderFunctionContents}${staticRenderFns}${scopeId}}}()`; 213 | } 214 | if (vueComponent.includes("Object.defineProperty(exports,\"__esModule\",{value:!0}),return")) { 215 | vueComponent = vueComponent.replace("Object.defineProperty(exports,\"__esModule\",{value:!0}),return", "Object.defineProperty(exports,\"__esModule\",{value:!0});return"); 216 | } 217 | 218 | const objectToReturn = { 219 | compiled: vueComponent, 220 | filePath: relativePath, 221 | match: vueComponentMatch, 222 | style: compiledObject.style, 223 | }; 224 | 225 | this.lruCache.set(cachedComponent, objectToReturn); 226 | resolve(objectToReturn); 227 | }) 228 | .catch(error => { 229 | reject(error); 230 | }); 231 | }) 232 | .catch(reject); 233 | } 234 | }); 235 | } 236 | /** 237 | * 238 | * @param {CompiledObjectType} compiledObject 239 | * @param {string} filePath 240 | * @returns {Promise} 241 | */ 242 | FindAndReplaceComponents(compiledObject, filePath) { 243 | return new Promise((resolve, reject) => { 244 | /** @type {Promise[]} */ 245 | let promiseArray = []; 246 | const vueFileRegex = /([\w/.\-@_\d]*\.vue)/igm; 247 | const requireModuleRegex = /(require\(['"])([^./][\w][\w:/.\-@_\d]*\.vue?[^\.js])(['"][\)])/igm; 248 | const requireRegex = /(require\(['"])([.?][\w:/.\-@_\d]*\.vue?[^\.js])(['"][\)])/igm; 249 | let vueComponentMatches = compiledObject.compiled.match(requireRegex); 250 | let vueComponentsModuleMatches = compiledObject.compiled.match(requireModuleRegex); 251 | if (vueComponentMatches && vueComponentMatches.length > 0) { 252 | for (let index = 0; index < vueComponentMatches.length; index++) { 253 | const vueComponentMatch = vueComponentMatches[index]; 254 | const vueComponentFileNameMatch = vueComponentMatch.match(vueFileRegex); 255 | if (vueComponentFileNameMatch && vueComponentFileNameMatch.length > 0) { 256 | promiseArray.push(this.BuildComponent(vueComponentFileNameMatch[0], filePath, vueComponentMatch)); 257 | } 258 | } 259 | } 260 | if (vueComponentsModuleMatches && vueComponentsModuleMatches.length > 0) { 261 | for (let index = 0; index < vueComponentsModuleMatches.length; index++) { 262 | let vueComponentModuleMatch = vueComponentsModuleMatches[index]; 263 | const vueComponentFileNameMatch = vueComponentModuleMatch.match(vueFileRegex); 264 | if (vueComponentFileNameMatch && vueComponentFileNameMatch.length > 0) { 265 | promiseArray.push(this.BuildComponent(vueComponentFileNameMatch[0], this.nodeModulesPath, vueComponentModuleMatch)); 266 | } 267 | } 268 | } 269 | /** 270 | * @param {Promise[]} promises 271 | * @param {(String[]|null)} componentArray 272 | * @param {(String[]|null)} modulesArray 273 | * @returns {Boolean} 274 | */ 275 | function isFinished(promises, componentArray, modulesArray) { 276 | let components = 0; 277 | let modules = 0; 278 | if (componentArray) { 279 | components = componentArray.length; 280 | } 281 | if (modulesArray) { 282 | modules = modulesArray.length; 283 | } 284 | if (promises.length > 0 && (promises.length === (modules + components))) { 285 | return true; 286 | } else { 287 | return false; 288 | } 289 | } 290 | const finished = isFinished(promiseArray, vueComponentMatches, vueComponentsModuleMatches); 291 | if (finished && promiseArray.length > 0) { 292 | Promise.all(promiseArray) 293 | .then(renderedItemArray => { 294 | for (var index = 0; index < renderedItemArray.length; index++) { 295 | var renderedItem = renderedItemArray[index]; 296 | compiledObject.compiled = compiledObject.compiled.replace(renderedItem.match, renderedItem.compiled); 297 | compiledObject.style += renderedItem.style; 298 | } 299 | //check if its the last element and then render 300 | const lastElement = compiledObject.compiled.match(requireRegex); 301 | if (lastElement === undefined || lastElement === null) { 302 | resolve(compiledObject); 303 | } 304 | }) 305 | .catch(reject); 306 | } else { 307 | resolve(compiledObject); 308 | } 309 | }); 310 | } 311 | /** 312 | * 313 | * @param {String} stringFile 314 | * @param {String} filePath 315 | * @returns {String} 316 | */ 317 | FindAndReplaceScripts(stringFile, filePath) { 318 | const requireRegex = /(require\(['"])([.?][\w:/.\-@_\d]*(?:\.js)?[^\.vue])(['"][\)])/igm; 319 | let scriptFileMatches = requireRegex.exec(stringFile); 320 | while (scriptFileMatches && scriptFileMatches.length > 0) { // this loop used to be an if 321 | const match = scriptFileMatches[2]; 322 | const resolvedPath = path.resolve(path.parse(filePath).dir, match).replace(/\\/g, "/"); 323 | if (resolvedPath) { 324 | stringFile = stringFile.replace(match, resolvedPath); 325 | } 326 | scriptFileMatches = requireRegex.exec(stringFile); // this is to update the matches 327 | } 328 | return stringFile; 329 | } 330 | /** 331 | * 332 | * @param {string} filePath 333 | * @returns {Promise} 334 | */ 335 | Compile(filePath) { 336 | return new Promise((resolve, reject) => { 337 | const vm = this; 338 | fs.readFile(filePath, function(err, fileContent) { 339 | if (err) { 340 | reject(err); 341 | } 342 | const content = String(fileContent); 343 | let compiled = { 344 | compiled: "", 345 | style: "", 346 | filePath: filePath, 347 | }; 348 | 349 | const stylesArray = vueCompiler.parseComponent(content, { pad: true }).styles; 350 | 351 | if (vm.babel) { 352 | vueify.compiler.applyConfig({ babel: vm.babel}); 353 | } 354 | const compiler = vueify.compiler; 355 | compiler.compile(content, filePath, 356 | /** 357 | * @param {Object} error 358 | * @param {string} stringFile 359 | */ 360 | function(error, stringFile) { 361 | if (error) { 362 | reject(error); 363 | } 364 | stringFile = vm.FindAndReplaceScripts(stringFile, filePath); 365 | let id = ""; 366 | stringFile.replace(/__vue__options__\._scopeId = "(.*?)"/gm, 367 | /** 368 | * @param {string} match 369 | * @param {string} p1 370 | * @return {string} 371 | */ 372 | function(match, p1) { 373 | id = p1; 374 | return ""; 375 | }); 376 | if (stylesArray.length > 0) { 377 | processStyle(stylesArray[0], filePath, id) 378 | .then(processedStyle => { 379 | compiled.compiled = stringFile; 380 | compiled.style += processedStyle; 381 | resolve(compiled); 382 | }) 383 | .catch(reject); 384 | } else { 385 | compiled.compiled = stringFile; 386 | resolve(compiled); 387 | } 388 | }); 389 | }); 390 | }); 391 | } 392 | /** 393 | * 394 | * @param {CompiledObjectType} compiledObject 395 | * @param {string} filePath 396 | * @returns {Promise<{data: object, propsData: object, props: object}>} 397 | */ 398 | MakeBundle(compiledObject, filePath) { 399 | return new Promise((resolve, reject) => { 400 | this.FindAndReplaceComponents(compiledObject, filePath) 401 | .then(compiled => { 402 | const bundle = requireFromString(compiled.compiled, filePath); 403 | resolve(bundle); 404 | }) 405 | .catch(reject); 406 | }); 407 | } 408 | /** 409 | * 410 | * @param {string} filePath 411 | * @param {Object} data 412 | * @param {{data: Object, propsData: Object} | Object} vueOptions 413 | * @returns {Promise<{vue: object, css: string, script: string}>} 414 | */ 415 | MakeVueClass(filePath, data, vueOptions = {}) { 416 | return new Promise((resolve, reject) => { 417 | let cachedBundle = this.internalCache.get(filePath); 418 | if (cachedBundle) { 419 | const cachedData = Object.assign({}, cachedBundle.data()); 420 | const newbundle = Object.assign({}, cachedBundle.bundle); 421 | 422 | if (cachedBundle.bundle.data && typeof cachedBundle.bundle.data === "function") { 423 | newbundle.data = this.FixData(cachedData, data); 424 | } 425 | if (vueOptions.propsData && (cachedBundle.bundle.propsData || cachedBundle.bundle.props)) { 426 | newbundle.propsData = this.FixPropsData(cachedBundle.bundle.propsData || {}, vueOptions.propsData); 427 | } 428 | 429 | // @ts-ignore 430 | const vue = new Vue(newbundle); 431 | // vue._data = newbundle.data(); 432 | const cleanBundle = this._deleteCtor(newbundle); 433 | const object = { 434 | vue: vue, 435 | css: cachedBundle.style, 436 | script: jsToString(cleanBundle, this.jsToStringOptions), 437 | }; 438 | resolve(object); 439 | } else { 440 | //Make Bundle 441 | this.Compile(filePath) 442 | .then(compiled => { 443 | this.MakeBundle(compiled, filePath) 444 | .then(bundle => { 445 | this.internalCache.set(filePath, { bundle: bundle, style: compiled.style, data: bundle.data }); 446 | //Insert Data 447 | if (bundle.data && typeof bundle.data === "function") { 448 | bundle.data = this.FixData(bundle.data(), data); 449 | } 450 | //Insert propsData 451 | if (vueOptions.propsData && (bundle.propsData || bundle.props)) { 452 | bundle.propsData = this.FixPropsData(bundle.propsData || {}, vueOptions.propsData); 453 | } 454 | 455 | //Create Vue Class 456 | // @ts-ignore 457 | const vue = new Vue(bundle); 458 | const cleanBundle = this._deleteCtor(bundle); 459 | const object = { 460 | vue: vue, 461 | css: compiled.style, 462 | script: jsToString(cleanBundle, this.jsToStringOptions), 463 | }; 464 | resolve(object); 465 | }) 466 | .catch(reject); 467 | }) 468 | .catch(reject); 469 | } 470 | }); 471 | } 472 | /** 473 | * 474 | * @param {Object} script 475 | * @returns {Object} 476 | */ 477 | _deleteCtor(script) { 478 | for (let component in script.components) { 479 | delete script.components[component]._Ctor; 480 | if (script.components[component].components) { 481 | script.components[component] = this._deleteCtor(script.components[component]); 482 | } 483 | } 484 | return script; 485 | } 486 | /** 487 | * 488 | * @param {String} vueFile 489 | * @param {String} [parentRoute=""] 490 | * @returns {Promise} 491 | */ 492 | FindFile(vueFile, parentRoute = "") { 493 | return new Promise((resolve, reject) => { 494 | const cacheKey = vueFile + parentRoute; 495 | const cached = this.lruCache.get(cacheKey); 496 | if (cached) { 497 | resolve(cached); 498 | } else { 499 | let pathToTest = ""; 500 | if (this.rootPath === undefined) { 501 | pathToTest = vueFile; 502 | } else { 503 | pathToTest = path.join(this.rootPath, vueFile); 504 | } 505 | fs.stat(pathToTest, (error) => { 506 | if (error) { 507 | reject(error); 508 | } else { 509 | this.lruCache.set(cacheKey, pathToTest); 510 | resolve(pathToTest); 511 | } 512 | }); 513 | } 514 | }); 515 | } 516 | /** 517 | * renderToString returns a string from res.renderVue to the client 518 | * @param {string} vueFile - full path to vue component 519 | * @param {Object} data - data to be inserted when generating vue class 520 | * @param {Object} vueOptions - vue options to be used when generating head 521 | * @returns {Promise} 522 | */ 523 | RenderToString(vueFile, data, vueOptions) { 524 | return new Promise((resolve, reject) => { 525 | this.FindFile(vueFile) 526 | .then(filePath => { 527 | this.MakeVueClass(filePath, data, vueOptions) 528 | .then(vueClass => { 529 | const mergedHeadObject = Utils.mergeHead(vueOptions.head, this.head); 530 | const template = Object.assign({}, this.template, vueOptions.template); 531 | //Init Renderer 532 | const context = { 533 | head: Utils.buildHead(mergedHeadObject), 534 | template: template, 535 | css: vueClass.css, 536 | script: vueClass.script, 537 | }; 538 | const layout = Utils.buildLayout(context); 539 | this.renderer.renderToString(vueClass.vue) 540 | .then(html => { 541 | const htmlString = `${layout.start}${html}${layout.end}`; 542 | resolve(htmlString); 543 | }) 544 | .catch(reject); 545 | }) 546 | .catch(reject); 547 | }) 548 | .catch(reject); 549 | }); 550 | } 551 | /** 552 | * renderToStream returns a stream from res.renderVue to the client 553 | * @param {string} vueFile - full path to .vue component 554 | * @param {Object} data - data to be inserted when generating vue class 555 | * @param {(VueOptionsType|object)} vueOptions - vue options to be used when generating head 556 | * @return {Promise} 557 | */ 558 | RenderToStream(vueFile, data, vueOptions) { 559 | return new Promise((resolve, reject) => { 560 | this.FindFile(vueFile) 561 | .then(filePath => { 562 | this.MakeVueClass(filePath, data, vueOptions) 563 | .then(vueClass => { 564 | const mergedHeadObject = Utils.mergeHead(vueOptions.head, this.head); 565 | const headString = Utils.buildHead(mergedHeadObject); 566 | const template = Object.assign({}, this.template, vueOptions.template); 567 | //Init Renderer 568 | const context = { 569 | head: headString, 570 | template: template, 571 | css: vueClass.css, 572 | script: vueClass.script, 573 | }; 574 | const vueStream = this.renderer.renderToStream(vueClass.vue); 575 | let htmlStream; 576 | const layout = Utils.buildLayout(context); 577 | 578 | htmlStream = new Utils.StreamUtils(layout.start, layout.end); 579 | htmlStream = vueStream.pipe(htmlStream); 580 | 581 | resolve(htmlStream); 582 | }) 583 | .catch(reject); 584 | }) 585 | .catch(reject); 586 | }); 587 | } 588 | } 589 | 590 | module.exports = Renderer; 591 | -------------------------------------------------------------------------------- /lib/renderer/renderer-webpack.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { createBundleRenderer } = require("vue-server-renderer"); 3 | const LRU = require("lru-cache"); 4 | const { 5 | StreamUtils, 6 | Cache, 7 | config, 8 | findRootPath, 9 | findNodeModules, 10 | buildLayoutWebpack, 11 | buildHead, 12 | mergeHead, 13 | promiseFS, 14 | } = require("../utils"); 15 | // @ts-ignore 16 | const find = require("find"); 17 | const mkdirp = require("mkdirp"); 18 | const webpack = require("webpack"); 19 | const MFS = require("memory-fs"); 20 | 21 | /** 22 | * @typedef CompiledObjectType 23 | * @prop {String} server 24 | * @prop {String} client 25 | * @prop {String} filePath 26 | */ 27 | 28 | /** 29 | * @typedef WebpackConfigType 30 | * @prop {string} server 31 | * @prop {string} client 32 | * @prop {Object} config 33 | */ 34 | 35 | /** 36 | * @typedef VueOptionsType 37 | * @prop {String} title 38 | * @prop {Object} head 39 | * @prop {Object[]} head.scripts 40 | * @prop {Object[]} head.metas 41 | * @prop {Object[]} head.styles 42 | * @prop {import("../models/layout").Layout} template 43 | */ 44 | 45 | /** 46 | * @typedef BundleFileSubtype 47 | * @prop {string} path 48 | * @prop {string} filename 49 | */ 50 | 51 | /** 52 | * @typedef MemoryBundleFileType 53 | * @prop {string} base 54 | * @prop {string} path 55 | * @prop {{client:string, server:string}} filename 56 | */ 57 | 58 | /** 59 | * @typedef BundleFileType 60 | * @prop {string} base 61 | * @prop {MemoryBundleFileType} memory 62 | * @prop {BundleFileSubtype} server 63 | * @prop {BundleFileSubtype} client 64 | */ 65 | 66 | /** 67 | * @typedef ConfigObjectType 68 | * @prop {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache 69 | * @prop {String} pagesPath 70 | * @prop {String} vueVersion 71 | * @prop {(VueOptionsType|Object)} head 72 | * @prop {Object} data 73 | * @prop {{server: webpack.Configuration, client: webpack.Configuration}} webpack 74 | * @prop {Object} vue 75 | * @prop {string} expressVueFolder 76 | */ 77 | 78 | class Renderer { 79 | /** 80 | * @param {(ConfigObjectType|Object)} [options={}] - options for constructor 81 | * @property {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache 82 | * @property {LRU} lruCache - LRU Cache 83 | * @property {vueServerRenderer} renderer - instance of vue server renderer 84 | * @property {String} pagesPath 85 | * @property {String} nodeModulesPath 86 | * @property {String} vueVersion 87 | * @prop {MFS} MFS 88 | * @prop {Object} webpackServerConfig 89 | * @prop {Object} webpackClientConfig 90 | * @prop {string} mode 91 | */ 92 | constructor(options = {}) { 93 | this.cacheOptions = options.cacheOptions || { 94 | max: 500, 95 | maxAge: 1000 * 60 * 60, 96 | }; 97 | this.lruCache = new LRU(this.cacheOptions); 98 | this.internalCache = new Cache(); 99 | this.head = options.head || { 100 | scripts: [], 101 | }; 102 | this.data = options.data || {}; 103 | this.template = options.template || {}; 104 | this.pagesPath = findRootPath(options.pagesPath); 105 | this.baseUrl = this.parseBaseURL(options.baseUrl); 106 | this.nodeModulesPath = findNodeModules(this.pagesPath); 107 | this.rootPath = path.join(this.nodeModulesPath, "../"); 108 | this.mfs = new MFS(); 109 | this.mfs.mkdirpSync(this.rootPath); 110 | const mode = (!process.env.VUE_DEV || (process.env.VUE_DEV && process.env.VUE_DEV === "false")) ? "production" : "development"; 111 | this.mode = mode; 112 | const {server, client} = config.bootstrap( 113 | options.webpack ? options.webpack.server : {}, 114 | options.webpack ? options.webpack.client : {}, 115 | mode, 116 | ); 117 | this.webpackServerConfig = server; 118 | this.webpackClientConfig = client; 119 | this.vue = options.vue ? 120 | {app: options.vue.app, server: options.vue.server, client: options.vue.client} : 121 | {app: undefined, server: undefined, client: undefined}; 122 | this.expressVueFolder = path.join(this.rootPath, ".expressvue"); 123 | } 124 | /** 125 | * @param {string} baseUrl 126 | * @returns {string} 127 | */ 128 | parseBaseURL(baseUrl = "") { 129 | if (baseUrl.endsWith("/")) { 130 | baseUrl = baseUrl.slice(0, -1); 131 | } 132 | 133 | return baseUrl; 134 | } 135 | /** 136 | * @param {boolean} [forceBuild=false] 137 | * @returns {Promise} 138 | */ 139 | async Bootstrap(forceBuild = false) { 140 | const vm = this; 141 | await mkdirp.sync(vm.expressVueFolder); 142 | if (forceBuild) { 143 | await this.Build(); 144 | return true; 145 | } 146 | // Check for prebuilt bundles and load them to memory 147 | let bundleFilePaths = find.fileSync(/bundle\.js$/, this.expressVueFolder); 148 | switch (this.mode.toUpperCase()) { 149 | case "PRODUCTION": 150 | if (!forceBuild && bundleFilePaths.length > 0) { 151 | await this._loadBundleFilesToMemory(bundleFilePaths); 152 | console.info("Loaded Prebuilt Bundles to memory"); 153 | return true; 154 | } else { 155 | await this.Build(); 156 | bundleFilePaths = find.fileSync(/bundle\.js$/, this.expressVueFolder); 157 | await this._loadBundleFilesToMemory(bundleFilePaths); 158 | return true; 159 | } 160 | case "DEVELOPMENT": 161 | return false; 162 | default: 163 | return false; 164 | } 165 | } 166 | /** 167 | * @returns {Promise} 168 | */ 169 | async Build() { 170 | const vm = this; 171 | // find all the vue files and try to precompile them and store them in cache 172 | const filepaths = find.fileSync(/\.vue$/, vm.pagesPath); 173 | console.info("Starting Webpack Compilation\n--------"); 174 | const promiseArray = []; 175 | for (let index = 0; index < filepaths.length; index++) { 176 | const filepath = filepaths[index]; 177 | try { 178 | const builtConfig = await vm._buildConfig(filepath); 179 | promiseArray.push(vm._webPackCompile(builtConfig.config, filepath)); 180 | } catch (error) { 181 | console.error(`Error Precacheing ${filepath} \n\n ${error}`); 182 | throw error; 183 | } 184 | } 185 | try { 186 | await Promise.all(promiseArray); 187 | console.info("Webpack Compilation Finished\n--------"); 188 | return true; 189 | } catch (error) { 190 | console.error(error); 191 | throw error; 192 | } 193 | } 194 | /** 195 | * @param {object[]} filepaths 196 | * @returns {Promise} 197 | */ 198 | async _loadBundleFilesToMemory(filepaths) { 199 | const vm = this; 200 | for (var i = 0, len = filepaths.length; i < len; i++) { 201 | const filepath = filepaths[i]; 202 | const bundlePath = this.getBundleFilePath(filepath); 203 | vm.mfs.mkdirpSync(bundlePath.memory.base); 204 | const file = await promiseFS.readFile(filepath); 205 | vm.mfs.writeFileSync(`${bundlePath.memory.path}.js`, file); 206 | } 207 | return; 208 | } 209 | /** 210 | * @param {string} filePath 211 | * @returns {BundleFileType} 212 | */ 213 | getBundleFilePath(filePath) { 214 | const parsedFilePath = filePath.replace(this.rootPath, "").replace(".vue", ""); 215 | const expressVueFolder = path.join(this.rootPath, ".expressvue"); 216 | const configFolder = path.join(expressVueFolder, parsedFilePath); 217 | const serverFilename = "server.bundle.js"; 218 | const clientFilename = "client.bundle.js"; 219 | let memoryParsed; 220 | if (filePath.includes(".expressvue")) { 221 | memoryParsed = path.parse(filePath.replace(".expressvue" + path.sep, "").split(this.pagesPath)[1]); 222 | } else { 223 | memoryParsed = path.parse(filePath.split(this.pagesPath)[1]); 224 | } 225 | 226 | const memoryBase = path.resolve(path.join(`/${this.baseUrl}`, '/expressvue/bundles', memoryParsed.dir)); // add C: for memory file system for windows machines 227 | const memoryPath = path.join(memoryBase, memoryParsed.name); 228 | 229 | return { 230 | base: configFolder, 231 | memory: { 232 | base: memoryBase, 233 | path: memoryPath, 234 | filename: { 235 | client: path.join(memoryPath, clientFilename), 236 | server: path.join(memoryPath, serverFilename), 237 | }, 238 | }, 239 | server: { 240 | path: path.join(configFolder, serverFilename), 241 | filename: serverFilename, 242 | }, 243 | client: { 244 | path: path.join(configFolder, clientFilename), 245 | filename: clientFilename, 246 | }, 247 | }; 248 | } 249 | /** 250 | * @param {string} filePath 251 | * @returns {Promise} 252 | */ 253 | async _buildConfig(filePath) { 254 | const bundlePath = this.getBundleFilePath(filePath); 255 | const {app, server, client} = config.appConfig(filePath, this.vue); 256 | 257 | try { 258 | await promiseFS.statIsDirectory(bundlePath.base); 259 | } catch (error) { 260 | mkdirp.sync(bundlePath.base); 261 | } 262 | 263 | const appPath = path.join(bundlePath.base, "app.js"); 264 | const serverPath = path.join(bundlePath.base, "entry-server.js"); 265 | const clientPath = path.join(bundlePath.base, "entry-client.js"); 266 | 267 | await Promise.all([ 268 | promiseFS.writeFile(appPath, app), 269 | promiseFS.writeFile(serverPath, server), 270 | promiseFS.writeFile(clientPath, client), 271 | ]); 272 | 273 | const entryPaths = { 274 | server: serverPath, 275 | client: clientPath, 276 | }; 277 | const webpackServerConfig = Object.assign({}, this.webpackServerConfig); 278 | const webpackClientConfig = Object.assign({}, this.webpackClientConfig); 279 | 280 | webpackServerConfig.entry = entryPaths.server; 281 | // @ts-ignore 282 | webpackServerConfig.output.path = bundlePath.base; 283 | // @ts-ignore 284 | webpackServerConfig.output.filename = bundlePath.server.filename; 285 | 286 | webpackClientConfig.entry = entryPaths.client; 287 | // @ts-ignore 288 | webpackClientConfig.output.path = bundlePath.base; 289 | // @ts-ignore 290 | webpackClientConfig.output.filename = bundlePath.client.filename; 291 | 292 | return { 293 | server: entryPaths.server, 294 | client: entryPaths.client, 295 | config: [webpackServerConfig, webpackClientConfig], 296 | }; 297 | } 298 | /** 299 | * 300 | * @param {WebpackConfigType} webpackConfig 301 | * @param {string} filePath 302 | * @returns {Promise<{client: string, server: string, clientBundlePath: string}>} 303 | */ 304 | async _makeBundle(webpackConfig, filePath) { 305 | const bundlePath = this.getBundleFilePath(filePath); 306 | //file was not found so make it 307 | await this._webPackCompile(webpackConfig.config, filePath); 308 | const bundleFilePaths = find.fileSync(/bundle\.js$/, this.expressVueFolder); 309 | await this._loadBundleFilesToMemory(bundleFilePaths); 310 | let serverBundle = this.mfs.readFileSync(bundlePath.memory.filename.server, "utf-8"); 311 | let clientBundle = this.mfs.readFileSync(bundlePath.memory.filename.client, "utf-8"); 312 | if (!serverBundle || !clientBundle) { 313 | throw new Error("Couldn't load bundle"); 314 | } 315 | return { 316 | server: serverBundle, 317 | client: clientBundle, 318 | clientBundlePath: bundlePath.memory.filename.client, 319 | }; 320 | } 321 | /** 322 | * @param {object} webpackConfig 323 | * @param {string} filepath 324 | * @returns {Promise} 325 | */ 326 | async _webPackCompile(webpackConfig, filepath) { 327 | const compiler = webpack(webpackConfig); 328 | return new Promise((resolve, reject) => { 329 | compiler.run((err, stats) => { 330 | if (err) { 331 | reject(err); 332 | } else { 333 | if (stats) { 334 | // @ts-ignore 335 | if (stats.stats) { 336 | // @ts-ignore 337 | stats.stats.forEach(stat => { 338 | if (stat.hasErrors()) { 339 | reject(stat.compilation.errors); 340 | } 341 | }); 342 | } else if (stats.hasErrors()) { 343 | reject(stats.compilation.errors); 344 | } 345 | } 346 | console.info(`Compiled ${filepath}`); 347 | resolve(stats); 348 | } 349 | }); 350 | }); 351 | } 352 | /** 353 | * 354 | * @param {string} filePath 355 | * @param {object} context 356 | * @returns {Promise<{renderer: {renderToStream: Function, renderToString: Function}, client: string, clientBundlePath: string}>} 357 | */ 358 | async _makeVueClass(filePath, context) { 359 | //Check if the bundle exists if not make a new one 360 | try { 361 | const bundlePath = this.getBundleFilePath(filePath); 362 | context.script = path.join('/', path.relative(path.resolve('/'), bundlePath.memory.filename.client)); // strip drive letter for windows systems 363 | const layout = buildLayoutWebpack(context); 364 | 365 | const clientBundle = this.mfs.readFileSync(bundlePath.memory.filename.client, "utf-8"); 366 | const serverBundle = this.mfs.readFileSync(bundlePath.memory.filename.server, "utf-8"); 367 | const rendererOptions = { 368 | runInNewContext: false, 369 | template: layout.toString(), 370 | }; 371 | 372 | const renderer = createBundleRenderer( 373 | serverBundle, 374 | rendererOptions, 375 | ); 376 | return { 377 | renderer: renderer, 378 | client: clientBundle, 379 | clientBundlePath: bundlePath.memory.filename.client, 380 | }; 381 | } catch (error) { 382 | //Make Bundle 383 | const webpackConfig = await this._buildConfig(filePath); 384 | const bundle = await this._makeBundle(webpackConfig, filePath); 385 | context.script = path.join('/', path.relative(path.resolve('/'), bundle.clientBundlePath)); // strip drive letter for windows systems 386 | const layout = buildLayoutWebpack(context); 387 | 388 | const rendererOptions = { 389 | runInNewContext: false, 390 | template: layout.toString(), 391 | }; 392 | 393 | const renderer = createBundleRenderer( 394 | bundle.server, 395 | rendererOptions, 396 | ); 397 | return { 398 | renderer: renderer, 399 | client: bundle.client, 400 | clientBundlePath: bundle.clientBundlePath, 401 | }; 402 | } 403 | } 404 | /** 405 | * 406 | * @param {String} vueFile 407 | * @param {String} [parentRoute=""] 408 | * @returns {Promise} 409 | */ 410 | async _findFile(vueFile, parentRoute = "") { 411 | const cacheKey = vueFile + parentRoute; 412 | const cached = this.lruCache.get(cacheKey); 413 | let pathToTest = ""; 414 | if (cached) { 415 | return cached; 416 | } else { 417 | if (this.pagesPath === undefined) { 418 | pathToTest = vueFile; 419 | } else { 420 | pathToTest = path.join(this.pagesPath, vueFile); 421 | } 422 | await promiseFS.access(pathToTest); 423 | this.lruCache.set(cacheKey, pathToTest); 424 | } 425 | return pathToTest; 426 | } 427 | /** 428 | * renderToString returns a string from res.renderVue to the client 429 | * @param {string} vueFile - full path to vue component 430 | * @param {Object} data - data to be inserted when generating vue class 431 | * @param {Object} vueOptions - vue options to be used when generating head 432 | * @returns {Promise} 433 | */ 434 | async RenderToString(vueFile, data, vueOptions) { 435 | const filePath = await this._findFile(vueFile); 436 | const mergedHeadObject = mergeHead( 437 | vueOptions.head, 438 | this.head, 439 | ); 440 | const mergedData = Object.assign({}, this.data, data); 441 | 442 | const template = Object.assign({}, 443 | this.template, 444 | vueOptions.template, 445 | ); 446 | const context = { 447 | head: buildHead(mergedHeadObject, mergedData), 448 | template: template, 449 | }; 450 | const vueClass = await this._makeVueClass(filePath, context); 451 | return await vueClass.renderer.renderToString(mergedData); 452 | } 453 | /** 454 | * renderToStream returns a stream from res.renderVue to the client 455 | * @param {string} vueFile - full path to .vue component 456 | * @param {Object} data - data to be inserted when generating vue class 457 | * @param {VueOptionsType} vueOptions - vue options to be used when generating head 458 | * @return {Promise} 459 | */ 460 | async RenderToStream(vueFile, data, vueOptions) { 461 | const filePath = await this._findFile(vueFile); 462 | const mergedHeadObject = mergeHead( 463 | vueOptions.head, 464 | this.head, 465 | ); 466 | const mergedData = Object.assign({}, this.data, data); 467 | const headString = buildHead( 468 | mergedHeadObject, 469 | mergedData, 470 | ); 471 | const template = Object.assign({}, 472 | this.template, 473 | vueOptions.template, 474 | ); 475 | //Init Renderer 476 | const context = { 477 | head: headString, 478 | template: template, 479 | }; 480 | const vueClass = await this._makeVueClass(filePath, context); 481 | const vueStream = vueClass.renderer.renderToStream(mergedData); 482 | const htmlStream = new StreamUtils(); 483 | return vueStream.pipe(htmlStream); 484 | } 485 | /** 486 | * @param {string} bundleFileName 487 | * @returns {string} bundle 488 | */ 489 | getBundleFile(bundleFileName) { 490 | const fullFileName = path.resolve(bundleFileName); // add C: for memory file system for windows machines 491 | if (fullFileName.endsWith("server.bundle.js")) { 492 | return ""; 493 | } else { 494 | return this.mfs.readFileSync(fullFileName, "utf-8"); 495 | } 496 | } 497 | } 498 | 499 | module.exports = Renderer; 500 | -------------------------------------------------------------------------------- /lib/utils/cache.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | class Cache { 4 | /** 5 | * Cache For Internal Use 6 | * @param {object} [options] 7 | */ 8 | constructor(options) { 9 | this.options = options || null; 10 | this.cache = {}; 11 | } 12 | /** 13 | * @param {string} cacheKey 14 | * @returns {object} 15 | */ 16 | get(cacheKey) { 17 | // @ts-ignore 18 | if (this.cache[cacheKey]) { 19 | // @ts-ignore 20 | return this.cache[cacheKey]; 21 | } else { 22 | return undefined; 23 | } 24 | } 25 | /** 26 | * 27 | * @param {string} cacheKey 28 | * @param {object} object 29 | * @returns {void} 30 | */ 31 | set(cacheKey, object) { 32 | // @ts-ignore 33 | this.cache[cacheKey] = object; 34 | } 35 | } 36 | 37 | module.exports = Cache; 38 | -------------------------------------------------------------------------------- /lib/utils/config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const {VueLoaderPlugin} = require("vue-loader"); 3 | 4 | /** 5 | * @typedef BootstrapType 6 | * @prop {import("webpack").Configuration} server 7 | * @prop {import("webpack").Configuration} client 8 | */ 9 | 10 | /** 11 | * 12 | * @param {Object} serverConfig 13 | * @param {Object} clientConfig 14 | * @param {"development"|"production"} [mode] 15 | * @returns {BootstrapType} 16 | */ 17 | module.exports.bootstrap = function bootstrap(serverConfig, clientConfig, mode) { 18 | const server = merge.smart({ 19 | entry: "./src/entry-server.js", 20 | mode: mode, 21 | target: "node", 22 | output: { 23 | filename: "server.js", 24 | libraryTarget: "commonjs2", 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.vue$/, 30 | loader: "vue-loader", 31 | }, 32 | { 33 | test: /\.js$/, 34 | loader: "babel-loader", 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: [ 39 | "vue-style-loader", 40 | "css-loader", 41 | ], 42 | }, 43 | ], 44 | }, 45 | resolve: { 46 | extensions: [ 47 | ".js", 48 | ".vue", 49 | ], 50 | }, 51 | plugins: [ 52 | new VueLoaderPlugin(), 53 | ], 54 | }, serverConfig); 55 | 56 | const client = merge.smart({ 57 | entry: "./src/entry-client.js", 58 | output: { 59 | filename: "client.js", 60 | }, 61 | mode: mode, 62 | module: { 63 | rules: [ 64 | { 65 | test: /\.vue$/, 66 | loader: "vue-loader", 67 | }, 68 | { 69 | test: /\.js$/, 70 | loader: "babel-loader", 71 | }, 72 | { 73 | test: /\.css$/, 74 | use: [ 75 | "vue-style-loader", 76 | "css-loader", 77 | ], 78 | }, 79 | ], 80 | }, 81 | resolve: { 82 | extensions: [ 83 | ".js", 84 | ".vue", 85 | ], 86 | }, 87 | plugins: [ 88 | new VueLoaderPlugin(), 89 | ], 90 | }, clientConfig); 91 | 92 | return { 93 | server, 94 | client, 95 | }; 96 | }; 97 | 98 | /** 99 | * @param {String} filePath 100 | * @param {{app:string, server: string, client:string}} config 101 | * @returns {{app:string, server:string, client:string}} 102 | */ 103 | module.exports.appConfig = function appConfig(filePath, config) { 104 | if (config && config.app && config.client) { 105 | return { 106 | app: config.app, 107 | server: config.server ? config.server : config.app, 108 | client: config.client, 109 | }; 110 | } 111 | 112 | const app = `import Vue from "vue"; 113 | import App from ${JSON.stringify(filePath)}; 114 | 115 | export function createApp(data) { 116 | const mergedData = Object.assign(App.data ? App.data() : {}, data); 117 | App.data = () => (mergedData) 118 | 119 | const app = new Vue({ 120 | data, 121 | render: h => h(App), 122 | }); 123 | return { app }; 124 | }`; 125 | 126 | const server = `import { createApp } from "./app"; 127 | export default context => { 128 | return new Promise((resolve, reject) => { 129 | const { app } = createApp(context); 130 | resolve(app); 131 | }); 132 | };`; 133 | 134 | 135 | const client = `import { createApp } from "./app"; 136 | const store = window.__INITIAL_STATE__; 137 | const { app } = createApp(store ? store : {}); 138 | app.$mount("#app"); 139 | `; 140 | 141 | return { 142 | app, 143 | server, 144 | client, 145 | } 146 | }; 147 | 148 | -------------------------------------------------------------------------------- /lib/utils/findPaths.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require("path"); 3 | // @ts-ignore 4 | const nodeModules = require("find-node-modules"); 5 | 6 | /** 7 | * @param {string | undefined} [rootPath] 8 | * @returns {string} 9 | */ 10 | function findRootPath(rootPath) { 11 | if (!rootPath) { 12 | const currentPath = path.resolve(__dirname); 13 | const nodeModulesPath = findNodeModules(currentPath); 14 | rootPath = path.resolve(nodeModulesPath, "../"); 15 | } 16 | 17 | return rootPath; 18 | } 19 | /** 20 | * @param {String} cwdPath 21 | * @returns {String} 22 | */ 23 | function findNodeModules(cwdPath) { 24 | const nodeModulesPath = nodeModules({cwd: cwdPath}); 25 | const nodeModulesPathResolved = path.join(cwdPath, nodeModulesPath[0]); 26 | return nodeModulesPathResolved; 27 | } 28 | 29 | module.exports.findRootPath = findRootPath; 30 | module.exports.findNodeModules = findNodeModules; 31 | -------------------------------------------------------------------------------- /lib/utils/head.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const deepmerge = require("deepmerge"); 3 | // @ts-ignore 4 | const jsToString = require("js-to-string"); 5 | /** 6 | * BuildHead takes the array and splits it up for processing 7 | * @param {object} metaObject 8 | * @param {object} [data] 9 | * @returns {String} 10 | */ 11 | function buildHead(metaObject = {}, data) { 12 | const metaCopy = Object.assign({}, metaObject); 13 | let finalString = ""; 14 | if (metaCopy.title) { 15 | finalString += `${metaCopy.title}`; 16 | } 17 | 18 | if (metaCopy.meta) { 19 | throw new Error("WARNING - DEPRECATED: It looks like you're using the old meta object, please migrate to the new one"); 20 | } 21 | 22 | if (metaCopy.metas) { 23 | for (let index = 0; index < metaCopy.metas.length; index++) { 24 | const headItem = metaCopy.metas[index]; 25 | const processedItem = processMetaItem(headItem); 26 | if (processedItem !== undefined) { 27 | finalString += processedItem; 28 | } 29 | } 30 | } 31 | 32 | if (metaCopy.scripts) { 33 | for (let index = 0; index < metaCopy.scripts.length; index++) { 34 | const headItem = metaCopy.scripts[index]; 35 | const processedItem = processScriptItem(headItem); 36 | if (processedItem !== undefined) { 37 | finalString += processedItem; 38 | } 39 | } 40 | } 41 | 42 | if (metaCopy.styles) { 43 | for (let index = 0; index < metaCopy.styles.length; index++) { 44 | const headItem = metaCopy.styles[index]; 45 | const processedItem = processStyleItem(headItem); 46 | if (processedItem !== undefined) { 47 | finalString += processedItem; 48 | } 49 | } 50 | } 51 | 52 | if (metaCopy.structuredData) { 53 | finalString += ``; 54 | } 55 | 56 | if (data) { 57 | finalString += ``; 58 | } 59 | 60 | return finalString; 61 | } 62 | 63 | /** 64 | * 65 | * @param {object} itemObject 66 | * @returns {String} 67 | */ 68 | function processScriptItem(itemObject) { 69 | let finalString = "`; 112 | 113 | return finalString; 114 | } 115 | 116 | /** 117 | * 118 | * @param {object} itemObject 119 | * @returns {String} 120 | */ 121 | function processMetaItem(itemObject) { 122 | let finalString = ""; 123 | 124 | if (itemObject.rel) { 125 | finalString = ``; 126 | } else { 127 | finalString = ``; 128 | } 129 | 130 | return finalString; 131 | } 132 | 133 | /** 134 | * 135 | * @param {object} itemObject 136 | * @param {Array} [keysToIgnore] 137 | * @returns {String} 138 | */ 139 | function keysToString(itemObject, keysToIgnore) { 140 | if (!keysToIgnore) { keysToIgnore = []; } 141 | const keys = Object.keys(itemObject); 142 | let finalString = ""; 143 | for (let index = 0; index < keys.length; index++) { 144 | const key = keys[index]; 145 | const value = itemObject[key]; 146 | if (!keysToIgnore.includes(key)) { 147 | const processedItem = ` ${key}="${value}"`; 148 | if (processedItem !== undefined) { 149 | finalString += processedItem; 150 | } 151 | } 152 | 153 | } 154 | return finalString; 155 | } 156 | 157 | /** 158 | * 159 | * @param {*} destinationArray 160 | * @param {*} sourceArray 161 | * @returns {*} 162 | */ 163 | function concatMerge(destinationArray, sourceArray) { 164 | let finalArray = destinationArray.concat(sourceArray); 165 | return finalArray; 166 | } 167 | 168 | /** 169 | * 170 | * @param {object} newObject 171 | * @param {object} oldObject 172 | * @returns {object} 173 | */ 174 | function mergeHead(newObject = {}, oldObject = {}) { 175 | //Dupe objects to avoid any changes to original object 176 | const oldCopy = Object.assign({}, oldObject); 177 | const newCopy = Object.assign({}, newObject); 178 | 179 | /** * @type {deepmerge.Options} */ 180 | const deepoptions = { arrayMerge: concatMerge }; 181 | const mergedObject = deepmerge(oldCopy, newCopy, deepoptions); 182 | return mergedObject; 183 | } 184 | 185 | module.exports.buildHead = buildHead; 186 | module.exports.mergeHead = mergeHead; 187 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | module.exports.buildHead = require("./head").buildHead; 3 | module.exports.mergeHead = require("./head").mergeHead; 4 | module.exports.buildLayout = require("./layout"); 5 | module.exports.buildLayoutWebpack = require("./layout-webpack"); 6 | module.exports.vueVersion = require("./vueVersion"); 7 | module.exports.findRootPath = require("./findPaths").findRootPath; 8 | module.exports.findNodeModules = require("./findPaths").findNodeModules; 9 | module.exports.config = require("./config"); 10 | module.exports.promiseFS = require("./promiseFS"); 11 | //models 12 | module.exports.StreamUtils = require("./stream"); 13 | module.exports.Cache = require("./cache"); 14 | -------------------------------------------------------------------------------- /lib/utils/layout-webpack.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const Layout = require("../models/layout").Layout; 3 | /** 4 | * BuildLayout 5 | * @param {{head: string, template: Layout, script: string}} layoutObject 6 | * @returns {{start: string, end: string, toString: function}} 7 | */ 8 | function buildLayout(layoutObject) { 9 | let finishedLayout = { 10 | start: "", 11 | end: "", 12 | }; 13 | 14 | if (layoutObject) { 15 | const layout = new Layout(layoutObject.template); 16 | finishedLayout.start = `${layout.html.start}${layoutObject.head}{{{ renderStyles() }}}${layout.body.start}${layout.template.start}`; 17 | finishedLayout.end = `${layout.template.end}${buildScript(layoutObject.script)}${layout.body.end}${layout.html.end}`; 18 | finishedLayout.toString = function() { 19 | return `${finishedLayout.start}${finishedLayout.end}`; 20 | }; 21 | } else { 22 | throw new Error("Missing Layout Object"); 23 | } 24 | 25 | return finishedLayout; 26 | } 27 | 28 | /** 29 | * 30 | * @param {String} script 31 | * @returns {String} 32 | */ 33 | function buildScript(script) { 34 | return ``; 35 | } 36 | 37 | module.exports = buildLayout; 38 | -------------------------------------------------------------------------------- /lib/utils/layout.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const Layout = require("../models/layout").Layout; 3 | /** 4 | * buildLayout 5 | * @param {{head: string, template: Layout, css: string, script: string}} layoutObject 6 | * @returns {{start: string, end: string}} 7 | */ 8 | function buildLayout(layoutObject) { 9 | let finishedLayout = { 10 | start: "", 11 | end: "", 12 | }; 13 | 14 | if (layoutObject) { 15 | const layout = new Layout(layoutObject.template); 16 | finishedLayout.start = `${layout.html.start}${layoutObject.head}${layout.body.start}${layout.template.start}`; 17 | finishedLayout.end = `${layout.template.end}${BuildScript(layoutObject.script)}${layout.body.end}${layout.html.end}`; 18 | } else { 19 | throw new Error("Missing Layout Object"); 20 | } 21 | 22 | return finishedLayout; 23 | } 24 | 25 | /** 26 | * 27 | * @param {String} script 28 | * @returns {String} 29 | */ 30 | function BuildScript(script) { 31 | let debugToolsString = ""; 32 | 33 | if (process.env.VUE_DEV && process.env.VUE_DEV === "true") { 34 | debugToolsString = "Vue.config.devtools = true;"; 35 | } 36 | 37 | let vueString = `var createApp = function () {return new Vue({})};`; 38 | if (script) { 39 | vueString = `var createApp = function () {return new Vue(${script})};`; 40 | } 41 | 42 | const javaScriptString = `(function(){"use strict";${vueString}"undefined"!=typeof module&&module.exports?module.exports=createApp:this.app=createApp()}).call(this),${debugToolsString}app.$mount("#app");`; 43 | return ``; 44 | } 45 | 46 | module.exports = buildLayout; 47 | -------------------------------------------------------------------------------- /lib/utils/promiseFS.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | /** 4 | * @param {string | Buffer | URL | number} filePath 5 | * @param {string | Buffer | DataView} fileContent 6 | */ 7 | module.exports.writeFile = async function(filePath, fileContent) { 8 | return new Promise((resolve, reject) => { 9 | fs.writeFile(filePath, fileContent, err => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(); 14 | } 15 | }); 16 | }); 17 | }; 18 | 19 | /** 20 | * @param {string | Buffer | URL | number} filePath 21 | * @returns {Promise} 22 | */ 23 | module.exports.readFile = async function(filePath) { 24 | return new Promise((resolve, reject) => { 25 | fs.readFile(filePath, "utf8", (err, data) => { 26 | if (err) { 27 | reject(err); 28 | } else { 29 | resolve(data); 30 | } 31 | }); 32 | }); 33 | }; 34 | 35 | /** 36 | * @param {string | Buffer | URL} filePath 37 | * @param {number} [mode="fs.constants.F.OK"] 38 | */ 39 | module.exports.access = async function(filePath, mode) { 40 | if (!mode) { 41 | mode = fs.constants.F_OK; 42 | } 43 | return new Promise((resolve, reject) => { 44 | fs.access(filePath, mode, error => { 45 | if (error) { 46 | reject(error); 47 | } else { 48 | resolve(); 49 | } 50 | }); 51 | }); 52 | }; 53 | 54 | /** 55 | * @param {string | Buffer | URL} filePath 56 | * @returns {Promise} 57 | */ 58 | module.exports.statIsDirectory = async function(filePath) { 59 | return new Promise((resolve, reject) => { 60 | fs.stat(filePath, (error, stats) => { 61 | if (error) { 62 | reject(error); 63 | } else { 64 | if (stats.isDirectory()) { 65 | resolve(true); 66 | } else { 67 | reject(new Error("Is not a directory")); 68 | } 69 | } 70 | }); 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /lib/utils/stream.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const {Transform} = require("stream"); 3 | 4 | /** 5 | * Class for StreamTransform 6 | * @class 7 | * @extends Transform 8 | */ 9 | class RenderStreamTransform extends Transform { 10 | /** 11 | * @constructor 12 | * @param {String} [head] 13 | * @param {String} [tail] 14 | * @param {object} options 15 | * @property {String} head 16 | * @property {String} tail 17 | * @property {Boolean} flag 18 | */ 19 | constructor(head = "", tail = "", options = {}) { 20 | super(options); 21 | this.head = head; 22 | this.tail = tail; 23 | this.flag = true; 24 | } 25 | /** 26 | * @constructor 27 | * @param {*} chunk 28 | * @param {String} encoding 29 | * @param {Function} callback 30 | */ 31 | _transform(chunk, encoding, callback) { 32 | if (this.flag) { 33 | this.push(Buffer.from(this.head)); 34 | this.flag = false; 35 | } 36 | this.push(chunk); 37 | callback(); 38 | } 39 | end() { 40 | this.push(Buffer.from(this.tail)); 41 | this.push(null); 42 | } 43 | } 44 | 45 | module.exports = RenderStreamTransform; 46 | -------------------------------------------------------------------------------- /lib/utils/vueVersion.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef vueVersionReturnType 5 | * @prop {Boolean} enabled 6 | * @prop {Object} [script] 7 | */ 8 | 9 | /** 10 | * Set VueVersion 11 | * @param {(String|Object)} version 12 | * @returns {vueVersionReturnType} 13 | */ 14 | module.exports = function(version) { 15 | /** @type vueVersionReturnType */ 16 | let vueVersionReturnObject = { 17 | enabled: true, 18 | }; 19 | if (!version) { 20 | // @ts-ignore 21 | version = "latest"; 22 | } 23 | 24 | switch (typeof version) { 25 | case "string": 26 | if (process.env.VUE_DEV && process.env.VUE_DEV === "true") { 27 | vueVersionReturnObject.script = {src: `https://cdn.jsdelivr.net/npm/vue@${version}/dist/vue.js`}; 28 | } else { 29 | vueVersionReturnObject.script = {src: `https://cdn.jsdelivr.net/npm/vue@${version}/dist/vue.min.js`}; 30 | } 31 | vueVersionReturnObject.enabled = true; 32 | break; 33 | case "object": 34 | if (version.disabled) { 35 | vueVersionReturnObject.enabled = false; 36 | } 37 | break; 38 | } 39 | return vueVersionReturnObject; 40 | }; 41 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "execMap": { 9 | "js": "node" 10 | }, 11 | "watch": [ 12 | "lib/", 13 | "tests/example/" 14 | ], 15 | "env": { 16 | "DEBUG": true, 17 | "VUE_DEV": true 18 | }, 19 | "ext": "js json vue" 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pronto", 3 | "version": "2.4.0", 4 | "description": "Seriously fast vue server renderer", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/**/*.js", 8 | ".babelrc" 9 | ], 10 | "dependencies": { 11 | "@typescript-eslint/eslint-plugin": "^1.13.0", 12 | "@typescript-eslint/parser": "^1.13.0", 13 | "autoprefixer": "^9.6.1", 14 | "babel-eslint": "^10.0.2", 15 | "babel-loader": "^8.0.6", 16 | "browserify": "^16.3.0", 17 | "clean-css": "^4.2.1", 18 | "css-loader": "^3.1.0", 19 | "deepmerge": "^3.3.0", 20 | "eslint": "^6.1.0", 21 | "eslint-plugin-vue": "^5.2.3", 22 | "find": "^0.3.0", 23 | "find-node-modules": "^2.0.0", 24 | "js-to-string": "^0.4.8", 25 | "lru-cache": "^5.1.1", 26 | "mz": "^2.7.0", 27 | "object-hash": "^1.3.0", 28 | "require-from-string": "^2.0.2", 29 | "typescript": "^3.5.3", 30 | "uglify-js": "^3.6.0", 31 | "vue": "^2.6.10", 32 | "vue-loader": "^15.7.1", 33 | "vue-server-renderer": "^2.6.10", 34 | "vue-template-compiler": "^2.6.10", 35 | "vueify": "^9.4.1", 36 | "webpack": "^4.37.0", 37 | "webpack-chain": "^6.0.0", 38 | "webpack-merge": "^4.2.1", 39 | "xss": "^1.0.6" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.5.5", 43 | "@babel/preset-env": "^7.5.5", 44 | "@babel/preset-es2015": "^7.0.0-beta.53", 45 | "@types/autoprefixer": "^9.5.0", 46 | "@types/clean-css": "^4.2.1", 47 | "@types/lru-cache": "^5.1.0", 48 | "@types/memory-fs": "^0.3.2", 49 | "@types/mkdirp": "^0.5.2", 50 | "@types/mz": "0.0.32", 51 | "@types/node": "^12.6.8", 52 | "@types/object-hash": "^1.3.0", 53 | "@types/require-from-string": "^1.2.0", 54 | "@types/uglify-js": "^3.0.3", 55 | "@types/webpack": "^4.32.1", 56 | "@types/webpack-merge": "^4.1.5", 57 | "ava": "^2.2.0", 58 | "axios": "^0.19.0", 59 | "babel-core": "^6.26.3", 60 | "babel-preset-env": "^1.7.0", 61 | "codecov": "^3.5.0", 62 | "express": "^4.17.1", 63 | "flame": "^0.1.20", 64 | "generate-release": "^1.1.1", 65 | "nodemon": "^1.19.1", 66 | "nyc": "^14.1.1", 67 | "simple-vue-component-test": "^1.0.0", 68 | "sleep-ms": "^2.0.1" 69 | }, 70 | "scripts": { 71 | "release": "npm test && generate-release", 72 | "debug": "nodemon --inspect tests/example/index-webpack.js", 73 | "start": "node tests/example/index-webpack.js", 74 | "debug-windows": "npm run build-windows && node --inspect tests/example/index-webpack.js", 75 | "start-windows": "npm run build-windows && node tests/example/index-webpack.js", 76 | "debug-vueify": "nodemon --inspect tests/example/index-vueify.js", 77 | "start-vueify": "node tests/example/index-vueify.js", 78 | "debug-vueify-windows": "npm run build-windows && node --inspect tests/example/index-vueify.js", 79 | "start-vueify-windows": "npm run build-windows && node tests/example/index-vueify.js", 80 | "lint": "eslint lib", 81 | "pretest": "npm run lint", 82 | "test": "TEST=true nyc ava", 83 | "coverage": "nyc report --reporter=lcov > coverage.lcov && codecov", 84 | "preversion": "npm test" 85 | }, 86 | "ava": { 87 | "files": [ 88 | "tests/**/*.js", 89 | "!tests/example/**/*.js" 90 | ], 91 | "sources": [ 92 | "lib/**/*.js" 93 | ], 94 | "failFast": false, 95 | "powerAssert": false, 96 | "babel": { 97 | "testOptions": { 98 | "babelrc": false, 99 | "configFile": false 100 | } 101 | } 102 | }, 103 | "nyc": { 104 | "include": [ 105 | "lib/**/*.js" 106 | ], 107 | "exclude": [ 108 | "lib/assets/**/*", 109 | "lib/renderer/process-style.js" 110 | ], 111 | "reporter": [ 112 | "text-summary" 113 | ] 114 | }, 115 | "eslintConfig": { 116 | "env": { 117 | "es6": true, 118 | "node": true 119 | }, 120 | "extends": [ 121 | "eslint:recommended" 122 | ], 123 | "globals": { 124 | "Atomics": "readonly", 125 | "SharedArrayBuffer": "readonly" 126 | }, 127 | "parserOptions": { 128 | "ecmaVersion": 2018, 129 | "sourceType": "module", 130 | "project": "./tsconfig.json" 131 | }, 132 | "parser": "@typescript-eslint/parser", 133 | "plugins": [ 134 | "vue", 135 | "@typescript-eslint" 136 | ], 137 | "rules": { 138 | "no-useless-escape": "off", 139 | "no-prototype-builtins": "off" 140 | } 141 | }, 142 | "eslintIgnore": [ 143 | "coverage", 144 | "example", 145 | "tests" 146 | ], 147 | "author": "Daniel Cherubini", 148 | "license": "ISC", 149 | "repository": { 150 | "type": "git", 151 | "url": "git+https://github.com/express-vue/vue-pronto.git" 152 | }, 153 | "keywords": [ 154 | "vue", 155 | "express", 156 | "node", 157 | "ssr", 158 | "javascript", 159 | "stream", 160 | "io" 161 | ], 162 | "bugs": { 163 | "url": "https://github.com/express-vue/vue-pronto/issues" 164 | }, 165 | "homepage": "https://github.com/express-vue/vue-pronto#readme" 166 | } 167 | -------------------------------------------------------------------------------- /tests/example/components/component.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /tests/example/components/copy/component.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /tests/example/components/inner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /tests/example/components/message-comp.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /tests/example/components/subcomponent.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /tests/example/components/users.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /tests/example/express.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use strict"; 3 | const {ProntoVueify, ProntoWebpack} = require("../../lib"); 4 | 5 | /** 6 | * @typedef VueOptionsType 7 | * @prop {String} title 8 | * @prop {Object} head 9 | * @prop {Object[]} head.scripts 10 | * @prop {Object[]} head.metas 11 | * @prop {Object[]} head.styles 12 | * @prop {Object} template 13 | */ 14 | 15 | /** 16 | * @typedef ConfigObjectType 17 | * @prop {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache 18 | * @prop {String} rootPath 19 | * @prop {String} vueVersion 20 | * @prop {VueOptionsType} head 21 | * @prop {Object | boolean} webpack 22 | */ 23 | 24 | /** 25 | * Middleware Init function for ExpressVue 26 | * @param {ConfigObjectType} options 27 | * @returns {Function} 28 | */ 29 | function init(options, renderer) { 30 | //Make new object 31 | let Renderer = {}; 32 | if (renderer) { 33 | Renderer = renderer; 34 | } else { 35 | Renderer = new ProntoVueify(options); 36 | } 37 | 38 | /** 39 | * @param {Object} req 40 | * @param {Object} req.vueOptions 41 | * @param {Object} res 42 | * @param {Function} res.renderVue 43 | * @param {Function} res.set 44 | * @param {Function} res.write 45 | * @param {Function} res.end 46 | * @param {Function} res.send 47 | * @param {Function} next 48 | */ 49 | function expressVueMiddleware(req, res, next) { 50 | /** 51 | * @param {NodeJS.ReadableStream} stream 52 | */ 53 | function StreamToClient(stream) { 54 | stream.on("data", /** @param {String} chunk */function(chunk) { 55 | return res.write(chunk); 56 | }); 57 | stream.on("end", function() { 58 | return res.end(); 59 | }); 60 | } 61 | 62 | /** 63 | * @param {Error} error 64 | */ 65 | function ErrorToClient(error) { 66 | console.error(error); 67 | res.send(error); 68 | } 69 | 70 | req.vueOptions = { 71 | title: "", 72 | head: { 73 | scripts: [], 74 | styles: [], 75 | metas: [], 76 | }, 77 | }; 78 | /** 79 | * Res RenderVUE function 80 | * @param {String} componentPath 81 | * @param {Object} [data={}] 82 | * @param {Object} [vueOptions={}] 83 | */ 84 | res.renderVue = function(componentPath, data = {}, vueOptions = {}) { 85 | res.set("Content-Type", "text/html"); 86 | Renderer.RenderToStream(componentPath, data, vueOptions) 87 | .then(StreamToClient) 88 | .catch(ErrorToClient); 89 | }; 90 | 91 | return next(); 92 | } 93 | 94 | //Middleware init 95 | return expressVueMiddleware; 96 | } 97 | 98 | /** 99 | * Takes an ExpressJS Instance and options and returns a promise 100 | * @param {Object} expressApp ExpressJS instance 101 | * @param {Object} options 102 | * @returns {Promise} 103 | */ 104 | async function use(expressApp, options) { 105 | const renderer = new ProntoWebpack(options); 106 | await renderer.Bootstrap(); 107 | const expressVue = init(options, renderer); 108 | expressApp.use(expressVue); 109 | 110 | expressApp.get( 111 | "/expressvue/bundles/*", 112 | function(req, res, next) { 113 | const fileUrl = req.baseUrl + req.path; 114 | const bundle = renderer.getBundleFile(fileUrl); 115 | if (!bundle) { 116 | res.status(404); 117 | res.send("file not found"); 118 | } else { 119 | res.setHeader("Content-Type", "application/javascript"); 120 | res.send(bundle); 121 | } 122 | }, 123 | ); 124 | return expressApp; 125 | } 126 | 127 | module.exports.init = init; 128 | module.exports.use = use; 129 | -------------------------------------------------------------------------------- /tests/example/expressvue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const basicWebpackConfig = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.js$/, 8 | loader: "babel-loader", 9 | options: { 10 | babelrc: false, 11 | presets: ["@babel/preset-env"], 12 | }, 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | module.exports = { 19 | pagesPath: path.normalize(path.join(__dirname, "views")), 20 | webpack: { 21 | server: basicWebpackConfig, 22 | client: basicWebpackConfig, 23 | }, 24 | data: { 25 | foo: true, 26 | globalData: true, 27 | }, 28 | head: { 29 | title: "Test", 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /tests/example/index-vueify.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const path = require("path"); 4 | const ExpressVue = require("./express"); 5 | const axios = require("axios").default; 6 | 7 | const evrOptions = { 8 | rootPath: path.normalize(path.join(__dirname, "views")), 9 | webpack: true, 10 | data: { 11 | foo: true, 12 | }, 13 | head: { 14 | title: "Test", 15 | }, 16 | }; 17 | 18 | evrOptions.babel = { 19 | "presets": [ 20 | ["env", { 21 | "targets": { 22 | "browsers": ["last 2 versions"], 23 | }, 24 | }], 25 | ], 26 | }; 27 | const ev = ExpressVue.init(evrOptions); 28 | app.use(ev); 29 | // ExpressVue.use(app, evrOptions); 30 | 31 | app.get("/", function(req, res) { 32 | const sentence = `one 33 | two 34 | three 35 | four five`; 36 | const data = { 37 | bar: true, 38 | sentence: sentence, 39 | fakehtml: "

FAKEHTML

", 40 | }; 41 | 42 | const vueOptions = { 43 | head: { 44 | title: "Test2", 45 | metas: [ 46 | {property: "og:title", content: "pageTitle"}, 47 | {name: "twitter:title", content: "pageTitle"}, 48 | {name: "viewport", content: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"}, 49 | ], 50 | structuredData: { 51 | "@context": "http://schema.org", 52 | "@type": "Organization", 53 | "url": "http://www.your-company-site.com", 54 | "contactPoint": [{ 55 | "@type": "ContactPoint", 56 | "telephone": "+1-401-555-1212", 57 | "contactType": "customer service", 58 | }], 59 | }, 60 | }, 61 | }; 62 | res.renderVue("index/index.vue", data, vueOptions); 63 | }); 64 | 65 | app.get("/example2", function(req, res) { 66 | const data = { 67 | bar: true, 68 | fakehtml: "

FAKEHTML

", 69 | }; 70 | 71 | const vueOptions = { 72 | head: { 73 | title: "Test", 74 | scripts: [ 75 | { src: "https://unpkg.com/vue@2.4.4/dist/vue.js" }, 76 | ], 77 | }, 78 | layout: { 79 | }, 80 | }; 81 | res.renderVue("../example2/views/index.vue", data, vueOptions); 82 | }); 83 | 84 | app.listen(3000, function() { 85 | // tslint:disable-next-line:no-console 86 | console.log("Example app listening on port 3000!"); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/example/index-webpack.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const path = require("path"); 4 | const ExpressVue = require("./express"); 5 | const axios = require("axios").default; 6 | 7 | const evrOptions = require("./expressvue.config"); 8 | 9 | ExpressVue.use(app, evrOptions).then(() => { 10 | 11 | app.get("/", function(req, res) { 12 | const sentence = `one 13 | two 14 | three 15 | four five`; 16 | const data = { 17 | bar: true, 18 | sentence: sentence, 19 | fakehtml: "

FAKEHTML

", 20 | }; 21 | 22 | const vueOptions = { 23 | head: { 24 | title: "Test2", 25 | metas: [ 26 | {property: "og:title", content: "pageTitle"}, 27 | {name: "twitter:title", content: "pageTitle"}, 28 | {name: "viewport", content: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"}, 29 | ], 30 | structuredData: { 31 | "@context": "http://schema.org", 32 | "@type": "Organization", 33 | "url": "http://www.your-company-site.com", 34 | "contactPoint": [{ 35 | "@type": "ContactPoint", 36 | "telephone": "+1-401-555-1212", 37 | "contactType": "customer service", 38 | }], 39 | }, 40 | }, 41 | }; 42 | res.renderVue("index/index-webpack.vue", data, vueOptions); 43 | }); 44 | 45 | app.get("/example2", function(req, res) { 46 | const data = { 47 | bar: true, 48 | fakehtml: "

FAKEHTML

", 49 | }; 50 | 51 | const vueOptions = { 52 | head: { 53 | title: "Test", 54 | scripts: [ 55 | { src: "https://unpkg.com/vue@2.4.4/dist/vue.js" }, 56 | ], 57 | }, 58 | layout: { 59 | }, 60 | }; 61 | res.renderVue("error.vue", data, vueOptions); 62 | }); 63 | 64 | app.listen(3000, function() { 65 | // tslint:disable-next-line:no-console 66 | console.log("Example app listening on port 3000!"); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/example/mixins/exampleMixin.js: -------------------------------------------------------------------------------- 1 | var exampleMixin = { 2 | methods: { 3 | hello: function(str) { 4 | console.log(str); 5 | }, 6 | }, 7 | }; 8 | 9 | module.exports = exampleMixin; 10 | -------------------------------------------------------------------------------- /tests/example/views/error.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /tests/example/views/index/index-webpack.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 57 | 62 | -------------------------------------------------------------------------------- /tests/example/views/index/index-with-props.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /tests/example/views/index/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /tests/expected/stream-full-object.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

true

FAKEHTML

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/expected/stream-no-object.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

false

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/expected/string-full-config.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

true

FAKEHTML

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/expected/string-some-config.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

true

FAKEHTML

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/expected/string-zero-config.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

true

FAKEHTML

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/expected/string-zero-object.html: -------------------------------------------------------------------------------- 1 |

Hello world!

Hello from component

Hello from subcomponent

false

Welcome to the demo. Click a link:

Say Foo

Hello From Component in node_modules

-------------------------------------------------------------------------------- /tests/renderer-vueify.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const test = require("ava"); 3 | const path = require("path"); 4 | const Pronto = require("../lib").ProntoVueify; 5 | 6 | const vueFile = path.join("index/index.vue"); 7 | const vueFileWithProps = path.join("index/index-with-props.vue"); 8 | const rootPath = path.normalize(path.join(__dirname, "../tests/example/views")); 9 | const babelConfig = { 10 | "presets": [ 11 | ["env", { 12 | "targets": { 13 | "browsers": ["last 2 versions"], 14 | }, 15 | }], 16 | ], 17 | }; 18 | 19 | function fixForNode8(expected) { 20 | if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 10) { 21 | expected = expected 22 | .replace("function render ()", "function render()") 23 | .replace(new RegExp(/function\(str\)/, "gm"), "function (str)") 24 | .replace(`"render":function()`, `"render":function ()`); 25 | } 26 | return expected; 27 | } 28 | 29 | //@ts-ignore 30 | test("String returns with zero config", t => { 31 | // @ts-ignore 32 | const renderer = new Pronto({babel: babelConfig}); 33 | const data = { 34 | bar: true, 35 | fakehtml: '

FAKEHTML

', 36 | }; 37 | const templateLiteral = `\n\n\n{{title}}\n\n\n\n

FOOOOO

\n\n\n`; 38 | 39 | const vueOptions = { 40 | title: "Test", 41 | template: templateLiteral, 42 | }; 43 | 44 | let expected = `\n\n\n

FOOOOO

\n\n\n`; 112 | 113 | const vueOptions = { 114 | title: "Test", 115 | template: templateLiteral, 116 | }; 117 | 118 | let expected = `\n\n\n

FOOOOO

\n\n\n`; 188 | 189 | const vueOptions = { 190 | head: {}, 191 | template: templateLiteral, 192 | }; 193 | let expected = `\n\n\n

FOOOOO

\n\n\n`; 255 | 256 | const vueOptions = { 257 | head: {}, 258 | template: templateLiteral, 259 | }; 260 | let expected = `\n\n\n

FOOOOO

\n\n\n`; 18 | 19 | const vueOptions = { 20 | title: "Test", 21 | template: templateLiteral, 22 | }; 23 | const expectedFile = path.join(expectedPath, "string-zero-config.html"); 24 | const expected = fs.readFileSync(expectedFile).toString(); 25 | const rendered = await renderer.RenderToString("index/index-webpack.vue", data, vueOptions); 26 | t.is(rendered, expected); 27 | }); 28 | 29 | //@ts-ignore 30 | test("String returns with some config", async t => { 31 | // @ts-ignore 32 | const renderer = new Pronto({ 33 | pagesPath: pagesPath, 34 | template: { 35 | html: { 36 | start: '', 37 | }, 38 | body: { 39 | start: '', 40 | }, 41 | }, 42 | }); 43 | const data = { 44 | bar: true, 45 | fakehtml: '

FAKEHTML

', 46 | }; 47 | 48 | const vueOptions = { 49 | title: "Test", 50 | }; 51 | const expectedFile = path.join(expectedPath, "string-some-config.html"); 52 | const expected = fs.readFileSync(expectedFile).toString(); 53 | const rendered = await renderer.RenderToString("index/index-webpack.vue", data, vueOptions); 54 | t.is(rendered, expected); 55 | }); 56 | 57 | //@ts-ignore 58 | test("String returns with full object", async t => { 59 | // @ts-ignore 60 | const renderer = new Pronto({ pagesPath: pagesPath, data: {globalData: true}}); 61 | const data = { 62 | bar: true, 63 | fakehtml: '

FAKEHTML

', 64 | }; 65 | const templateLiteral = `\n\n\n{{title}}\n\n\n\n

FOOOOO

\n\n\n`; 66 | 67 | const vueOptions = { 68 | title: "Test", 69 | template: templateLiteral, 70 | }; 71 | const expectedFile = path.join(expectedPath, "string-full-config.html"); 72 | const expected = fs.readFileSync(expectedFile).toString(); 73 | const rendered = await renderer.RenderToString(vueFile, data, vueOptions); 74 | t.is(rendered, expected); 75 | }); 76 | 77 | //@ts-ignore 78 | test("String returns with no object", async t => { 79 | // @ts-ignore 80 | const renderer = new Pronto({ pagesPath: pagesPath}); 81 | const expectedFile = path.join(expectedPath, "string-zero-object.html"); 82 | const expected = fs.readFileSync(expectedFile).toString(); 83 | const rendered = await renderer.RenderToString(vueFile, {}, {}); 84 | t.is(rendered, expected); 85 | }); 86 | 87 | //@ts-ignore 88 | test.cb("Stream returns with full object", t => { 89 | // @ts-ignore 90 | const data = { 91 | bar: true, 92 | fakehtml: '

FAKEHTML

', 93 | }; 94 | const renderer = new Pronto({ pagesPath: pagesPath, data: {globalData: true} }); 95 | 96 | const templateLiteral = `\n\n\n{{title}}\n\n\n\n

FOOOOO

\n\n\n`; 97 | 98 | const vueOptions = { 99 | head: {}, 100 | template: templateLiteral, 101 | }; 102 | const expectedFile = path.join(expectedPath, "stream-full-object.html"); 103 | const expected = fs.readFileSync(expectedFile).toString(); 104 | // @ts-ignore 105 | renderer 106 | // @ts-ignore 107 | .RenderToStream(vueFile, data, vueOptions) 108 | .then(stream => { 109 | let rendered = ""; 110 | stream.on("data", chunk => (rendered += chunk)); 111 | stream.on("end", () => { 112 | t.is(rendered, expected); 113 | t.end(); 114 | }); 115 | }) 116 | .catch(error => { 117 | t.fail(error); 118 | }); 119 | }); 120 | 121 | //@ts-ignore 122 | test.cb("Stream returns with no object", t => { 123 | // @ts-ignore 124 | const renderer = new Pronto({ pagesPath: pagesPath }); 125 | const expectedFile = path.join(expectedPath, "stream-no-object.html"); 126 | const expected = fs.readFileSync(expectedFile).toString(); 127 | // @ts-ignore 128 | renderer 129 | // @ts-ignore 130 | .RenderToStream(vueFile, {}, {}) 131 | .then(stream => { 132 | let rendered = ""; 133 | stream.on("data", chunk => (rendered += chunk)); 134 | stream.on("end", () => { 135 | t.is(rendered, expected); 136 | t.end(); 137 | }); 138 | }) 139 | .catch(error => { 140 | t.fail(error); 141 | }); 142 | }); 143 | 144 | // @ts-ignore 145 | test("Bootstrap prerenders", async t => { 146 | process.env.VUE_DEV = "false"; 147 | const renderer = new Pronto({ pagesPath: pagesPath }); 148 | const result = await renderer.Bootstrap(); 149 | t.is(result, true); 150 | }); 151 | 152 | // @ts-ignore 153 | test("Bootstrap does not prerender", async t => { 154 | process.env.VUE_DEV = "true"; 155 | const renderer = new Pronto({ pagesPath: pagesPath }); 156 | const result = await renderer.Bootstrap(); 157 | t.is(result, false); 158 | }); 159 | -------------------------------------------------------------------------------- /tests/renderer.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/express-vue/vue-pronto/24e6a35bad59296501598635c30b0a2261914554/tests/renderer.json -------------------------------------------------------------------------------- /tests/utils/cache.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {Cache} = require("../../lib/utils"); 3 | 4 | test("Default Cache", t => { 5 | const cache = new Cache(); 6 | cache.set("foo", "bar"); 7 | const result = cache.get("foo"); 8 | t.is(result, "bar"); 9 | }); 10 | 11 | test("Default Cache Error", t => { 12 | const cache = new Cache(); 13 | cache.set("foo", "bar"); 14 | const result = cache.get("foos"); 15 | t.is(result, undefined); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/utils/config.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {config} = require("../../lib/utils"); 3 | const {VueLoaderPlugin} = require("vue-loader"); 4 | const merge = require("webpack-merge"); 5 | 6 | const defaultServer = { 7 | entry: "./src/entry-server.js", 8 | mode: undefined, 9 | target: "node", 10 | output: { 11 | filename: "server.js", 12 | libraryTarget: "commonjs2", 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.vue$/, 18 | loader: "vue-loader", 19 | }, 20 | { 21 | test: /\.js$/, 22 | loader: "babel-loader", 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | "vue-style-loader", 28 | "css-loader", 29 | ], 30 | }, 31 | ], 32 | }, 33 | resolve: { 34 | extensions: [ 35 | ".js", 36 | ".vue", 37 | ], 38 | }, 39 | plugins: [ 40 | new VueLoaderPlugin(), 41 | ], 42 | }; 43 | 44 | const defaultClient = { 45 | entry: "./src/entry-client.js", 46 | output: { 47 | filename: "client.js", 48 | }, 49 | mode: undefined, 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.vue$/, 54 | loader: "vue-loader", 55 | }, 56 | { 57 | test: /\.js$/, 58 | loader: "babel-loader", 59 | }, 60 | { 61 | test: /\.css$/, 62 | use: [ 63 | "vue-style-loader", 64 | "css-loader", 65 | ], 66 | }, 67 | ], 68 | }, 69 | resolve: { 70 | extensions: [ 71 | ".js", 72 | ".vue", 73 | ], 74 | }, 75 | plugins: [ 76 | new VueLoaderPlugin(), 77 | ], 78 | }; 79 | 80 | test("Default Server Config", t => { 81 | const result = config.bootstrap({}, {}); 82 | t.deepEqual(result.server, defaultServer); 83 | }); 84 | 85 | test("Default Client Config", t => { 86 | const result = config.bootstrap({}, {}); 87 | t.deepEqual(result.client, defaultClient); 88 | }); 89 | 90 | test("Merges Module Server Config", t => { 91 | const extraOptions = { 92 | module: { 93 | rules: [ 94 | { 95 | test: /\.js$/, 96 | loader: "babel-loader", 97 | options: { 98 | babelrc: false, 99 | presets: ["@babel/preset-env"], 100 | }, 101 | }, 102 | ], 103 | }, 104 | }; 105 | const result = config.bootstrap(extraOptions, {}); 106 | const expected = merge.smart(defaultServer, extraOptions); 107 | t.deepEqual(result.server, expected); 108 | }); 109 | 110 | test("Merges Module Client Config", t => { 111 | const extraOptions = { 112 | module: { 113 | rules: [ 114 | { 115 | test: /\.js$/, 116 | loader: "babel-loader", 117 | options: { 118 | babelrc: false, 119 | presets: ["@babel/preset-env"], 120 | }, 121 | }, 122 | ], 123 | }, 124 | }; 125 | const result = config.bootstrap({}, extraOptions); 126 | const expected = merge.smart(defaultClient, extraOptions); 127 | t.deepEqual(result.client, expected); 128 | }); 129 | 130 | test("Gets Default App", t => { 131 | const result = config.appConfig("foo"); 132 | const expected = `import Vue from "vue"; 133 | import App from "foo"; 134 | 135 | export function createApp(data) { 136 | const mergedData = Object.assign(App.data ? App.data() : {}, data); 137 | App.data = () => (mergedData) 138 | 139 | const app = new Vue({ 140 | data, 141 | render: h => h(App), 142 | }); 143 | return { app }; 144 | }`; 145 | t.is(result.app, expected); 146 | }); 147 | 148 | test("Gets Default Client", t => { 149 | const result = config.appConfig(); 150 | const expected = `import { createApp } from "./app"; 151 | const store = window.__INITIAL_STATE__; 152 | const { app } = createApp(store ? store : {}); 153 | app.$mount("#app"); 154 | `; 155 | t.is(result.client, expected); 156 | }); 157 | 158 | test("Gets Default Server", t => { 159 | const result = config.appConfig(); 160 | const expected = `import { createApp } from "./app"; 161 | export default context => { 162 | return new Promise((resolve, reject) => { 163 | const { app } = createApp(context); 164 | resolve(app); 165 | }); 166 | };`; 167 | t.is(result.server, expected); 168 | }); 169 | 170 | test("Gets Modified App", t => { 171 | const result = config.appConfig("foo", {app: "bar", client: "bar"}); 172 | const expected = `bar`; 173 | t.is(result.app, expected); 174 | }); 175 | 176 | test("Gets Modified Client", t => { 177 | const result = config.appConfig("foo", {app: "bar", client: "bar"}); 178 | const expected = `bar`; 179 | t.is(result.client, expected); 180 | }); 181 | 182 | test("Gets Modified Server", t => { 183 | const result = config.appConfig("foo", {app:"bar", client: "bar", server: "bar"}); 184 | const expected = `bar`; 185 | t.is(result.server, expected); 186 | }); 187 | -------------------------------------------------------------------------------- /tests/utils/findPaths.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const path = require("path"); 3 | const {findRootPath, findNodeModules} = require("../../lib/utils"); 4 | 5 | test("FindRootPath no root", t => { 6 | const rootPath = findRootPath(); 7 | const expected = path.resolve(__dirname, "../../"); 8 | t.is(rootPath, expected); 9 | }); 10 | 11 | test("FindRootPath with root", t => { 12 | const rootPath = findRootPath(__dirname); 13 | const expected = path.resolve(__dirname); 14 | t.is(rootPath, expected); 15 | }); 16 | 17 | test("FindNodeModules", t => { 18 | const nodeModulesPath = findNodeModules(__dirname); 19 | const expected = path.resolve(__dirname, "../../node_modules"); 20 | t.is(nodeModulesPath, expected); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/utils/head.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {buildHead} = require("../../lib/utils"); 3 | 4 | test.cb("General Head", t => { 5 | const head = { 6 | metas: [ 7 | { name: "application-name", content: "Name of my application" }, 8 | { name: "description", content: "A description of the page", id: "desc" }, 9 | { name: "twitter:title", content: "Content Title" }, 10 | { property: "fb:app_id", content: "123456789" }, 11 | { property: "og:title", content: "Content Title" }, 12 | { rel: "icon", type: "image/png", href: "/assets/favicons/favicon-32x32.png", sizes: "32x32" }, 13 | ], 14 | scripts: [ 15 | { src: "/assets/scripts/hammer.min.js" }, 16 | { src: "/assets/scripts/vue-touch.min.js", charset: "utf-8" }, 17 | ], 18 | styles: [ 19 | { style: "/assets/rendered/style.css" }, 20 | { style: "/assets/rendered/style.css", type: "text/css" }, 21 | ], 22 | }; 23 | const expected = ``; 24 | const result = buildHead(head); 25 | t.is(result, expected); 26 | t.end(); 27 | }); 28 | 29 | test.cb("Script Head", t => { 30 | const head = { 31 | scripts: [ 32 | { src: "/assets/scripts/hammer.min.js" }, 33 | { src: "/assets/scripts/hammer.min.js", type: "text/javascript" }, 34 | { src: "/assets/scripts/vue-touch.min.js", charset: "utf-8" }, 35 | { src: "/assets/scripts/hammer.min.js", async: true }, 36 | { src: "/assets/scripts/hammer.min.js", defer: true }, 37 | { src: "/assets/scripts/hammer.min.js", defer: true, async: true }, 38 | ], 39 | }; 40 | const expected = ``; 41 | const result = buildHead(head); 42 | t.is(result, expected); 43 | t.end(); 44 | }); 45 | 46 | test.cb("Style Head", t => { 47 | const head = { 48 | styles: [ 49 | { style: "/assets/rendered/style.css" }, 50 | { style: "/assets/rendered/style.css", type: "text/css" }, 51 | { src: "/assets/rendered/style.css" }, 52 | { src: "/assets/rendered/style.css", type: "text/css" }, 53 | ], 54 | }; 55 | const expected = ``; 56 | const result = buildHead(head); 57 | t.is(result, expected); 58 | t.end(); 59 | }); 60 | 61 | test.cb("Head Title", t => { 62 | const head = { 63 | title: "Test Title", 64 | }; 65 | const expected = `Test Title`; 66 | const result = buildHead(head); 67 | t.is(result, expected); 68 | t.end(); 69 | }); 70 | 71 | test.cb("Structured Data", t => { 72 | const head = { 73 | structuredData: { 74 | "@context": "http://schema.org", 75 | "@type": "Organization", 76 | "url": "http://www.your-company-site.com", 77 | "contactPoint": [{ 78 | "@type": "ContactPoint", 79 | "telephone": "+1-401-555-1212", 80 | "contactType": "customer service", 81 | }], 82 | }, 83 | }; 84 | const expected = ``; 85 | const result = buildHead(head); 86 | t.is(result, expected); 87 | t.end(); 88 | }); 89 | 90 | test.cb("Meta Error", t => { 91 | const head = { 92 | meta: [ 93 | { src: "/assets/scripts/hammer.min.js" }, 94 | ], 95 | }; 96 | 97 | const error = t.throws(() => { 98 | buildHead(head); 99 | }, Error); 100 | const expected = "WARNING - DEPRECATED: It looks like you're using the old meta object, please migrate to the new one"; 101 | t.is(error.message, expected); 102 | t.end(); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/utils/layout.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {buildLayout} = require("../../lib/utils"); 3 | 4 | test.cb("Layout Default", t => { 5 | const context = { 6 | head: "", 7 | css: "", 8 | script: "", 9 | }; 10 | const layout = buildLayout(context); 11 | const expected = { 12 | start: '
', 13 | end: `
`, 14 | }; 15 | t.deepEqual(layout, expected); 16 | t.end(); 17 | }); 18 | 19 | test.cb("Layout With Stuff", t => { 20 | const context = { 21 | head: "", 22 | css: "", 23 | script: "", 24 | template: { 25 | html: { 26 | start: '', 27 | }, 28 | body: { 29 | start: '', 30 | }, 31 | }, 32 | }; 33 | const layout = buildLayout(context); 34 | const expected = { 35 | start: '
', 36 | end: `
`, 37 | }; 38 | t.deepEqual(layout, expected); 39 | t.end(); 40 | }); 41 | 42 | // test.cb("Layout With Javascript error", t => { 43 | // const context = { 44 | // head: "", 45 | // css: "", 46 | // script: "const foo = true", 47 | // template: { 48 | // html: { 49 | // start: '', 50 | // }, 51 | // body: { 52 | // start: '', 53 | // }, 54 | // }, 55 | // }; 56 | 57 | // const layout = t.throws(() => { 58 | // Utils.BuildLayout(context); 59 | // }); 60 | 61 | // t.is(layout.message, "Unexpected token: keyword (const)"); 62 | // t.end(); 63 | // }); 64 | -------------------------------------------------------------------------------- /tests/utils/stream.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {StreamUtils} = require("../../lib/utils"); 3 | 4 | const htmlStream = new StreamUtils("foo", "bar"); 5 | 6 | test("it should have a head", t => { 7 | t.is(htmlStream.head , "foo"); 8 | }); 9 | 10 | test("it should have a tail", t => { 11 | t.is(htmlStream.tail, "bar"); 12 | }); 13 | 14 | test.cb("it should end", t => { 15 | htmlStream._transform(Buffer.from("qux"), "utf-8", () => { 16 | t.pass(); 17 | t.end(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/utils/vueVersion.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const {vueVersion} = require("../../lib/utils"); 3 | 4 | test("VueVersion no version", t => { 5 | const version = vueVersion(); 6 | 7 | let vuePackageVersion = "latest"; 8 | const expected = `https://cdn.jsdelivr.net/npm/vue@${vuePackageVersion}/dist/vue.min.js`; 9 | 10 | t.is(version.script.src, expected); 11 | }); 12 | 13 | test("VueVersion with version", t => { 14 | const version = vueVersion("2.2.2"); 15 | const expected = "https://cdn.jsdelivr.net/npm/vue@2.2.2/dist/vue.min.js"; 16 | t.is(version.script.src, expected); 17 | }); 18 | 19 | test("VueVersion disabled", t => { 20 | const version = vueVersion({disabled: true}); 21 | t.is(version.enabled, false); 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "strict": true, 8 | "typeRoots": [ 9 | "node_modules/@types" 10 | ], 11 | "types": [ 12 | "node" 13 | ] 14 | }, 15 | "include": [ 16 | "lib" 17 | ], 18 | "exclude": [ 19 | "tests", 20 | "node_modules" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------