├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nycrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── babel.config.js ├── bin └── nuxt-generate.js ├── build └── rollup.config.js ├── index.js ├── jest.config.js ├── lib ├── async │ ├── index.js │ ├── master.js │ ├── mixins │ │ ├── index.js │ │ └── messaging.js │ └── worker.js ├── cluster │ ├── index.js │ ├── master.js │ └── worker.js ├── generate │ ├── commands.js │ ├── index.js │ ├── master.js │ ├── watchdog.js │ └── worker.js ├── index.js ├── mixins │ ├── hookable.js │ └── index.js └── utils │ ├── consola.js │ ├── index.js │ ├── message-broker.js │ ├── messaging.js │ ├── nuxt │ ├── imports.js │ └── index.js │ └── reporters │ ├── cluster.js │ └── index.js ├── package.json ├── test ├── cli │ └── cli.test.js ├── fixtures │ ├── basic │ │ ├── basic.test.js │ │ ├── middleware │ │ │ ├── -ignored.js │ │ │ ├── error.js │ │ │ ├── ignored.test.js │ │ │ ├── meta.js │ │ │ └── redirect.js │ │ ├── nuxt.config.js │ │ ├── pages │ │ │ ├── -ignored.vue │ │ │ ├── async-data.vue │ │ │ ├── await-async-data.vue │ │ │ ├── callback-async-data.vue │ │ │ ├── config.vue │ │ │ ├── css.vue │ │ │ ├── error-midd.vue │ │ │ ├── error-object.vue │ │ │ ├── error-string.vue │ │ │ ├── error.vue │ │ │ ├── error2.vue │ │ │ ├── extractCSS.vue │ │ │ ├── fn-midd.vue │ │ │ ├── head.vue │ │ │ ├── ignored.test.vue │ │ │ ├── index.vue │ │ │ ├── js-link.js │ │ │ ├── js-link.vue │ │ │ ├── jsx-link.js │ │ │ ├── jsx-link.jsx │ │ │ ├── jsx.js │ │ │ ├── meta.vue │ │ │ ├── no-ssr.vue │ │ │ ├── noloading.vue │ │ │ ├── pug.vue │ │ │ ├── redirect-external.vue │ │ │ ├── redirect-middleware.vue │ │ │ ├── redirect-name.vue │ │ │ ├── redirect.vue │ │ │ ├── router-guard.vue │ │ │ ├── special-state.vue │ │ │ ├── stateful.vue │ │ │ ├── stateless.vue │ │ │ ├── store-module.vue │ │ │ ├── store.vue │ │ │ ├── style.vue │ │ │ ├── users │ │ │ │ └── _id.vue │ │ │ ├── validate-async.vue │ │ │ ├── validate.vue │ │ │ └── тест雨.vue │ │ ├── plugins │ │ │ ├── dir-plugin │ │ │ │ └── index.js │ │ │ ├── inject.js │ │ │ ├── tailwind.js │ │ │ └── vuex-module.js │ │ ├── static │ │ │ └── body.js │ │ └── store │ │ │ ├── -ignored.js │ │ │ ├── bab │ │ │ └── index.js │ │ │ ├── foo │ │ │ ├── bar.js │ │ │ ├── blarg.js │ │ │ └── blarg │ │ │ │ ├── getters.js │ │ │ │ ├── index.js │ │ │ │ └── state.js │ │ │ ├── ignored.test.js │ │ │ └── index.js │ └── error-testing │ │ ├── nuxt.config.js │ │ └── pages │ │ └── _error.vue ├── unit │ ├── async.generate.test.js │ ├── async.messaging.test.js │ ├── cluster.generate.test.js │ ├── cluster.master.test.js │ ├── cluster.worker.test.js │ ├── consola.test.js │ ├── generate.watchdog.test.js │ ├── messaging.test.js │ ├── miscellaneous.test.js │ └── reporter.test.js └── utils │ ├── build.js │ ├── cluster.worker.js │ ├── index.js │ └── setup.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app 2 | !app/store.js 3 | node_modules 4 | dist 5 | .nuxt 6 | examples/coffeescript/pages/index.vue 7 | !examples/storybook/.storybook 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint", 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "@nuxtjs" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | examples/**/*/yarn.lock 4 | jspm_packages 5 | package-lock.json 6 | 7 | # Logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Other 12 | .nuxt* 13 | .cache 14 | 15 | # Dist folder 16 | dist* 17 | 18 | # Dist example generation 19 | examples/**/dist 20 | 21 | # Coverage support 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | .vscode 26 | 27 | # Intellij idea 28 | *.iml 29 | .idea 30 | 31 | # Codelite 32 | .codelite 33 | *.workspace 34 | 35 | # OSX 36 | *.DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "report-dir": "./coverage/", 3 | "include": [ 4 | "lib" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | cache: 6 | yarn: true 7 | directories: 8 | - node_modules 9 | install: 10 | - yarn install 11 | script: 12 | - yarn lint 13 | - yarn test 14 | after_success: 15 | - yarn coverage 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.6.1](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.6.0...v2.6.1) (2020-01-16) 6 | 7 | # [2.6.0](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.4.1...v2.6.0) (2019-04-11) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * fix consola reporting ([1381955](https://github.com/nuxt-community/nuxt-generate-cluster/commit/1381955)) 13 | 14 | 15 | 16 | 17 | ## [2.4.1](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.4.0...v2.4.1) (2019-02-04) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * also ignore error messages from Nuxt generator ([72053c2](https://github.com/nuxt-community/nuxt-generate-cluster/commit/72053c2)) 23 | * make nuxt a peer dependency ([75c8296](https://github.com/nuxt-community/nuxt-generate-cluster/commit/75c8296)) 24 | 25 | 26 | 27 | 28 | # [2.4.0](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.3.3...v2.4.0) (2019-01-30) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * windows tests ([8a7f539](https://github.com/nuxt-community/nuxt-generate-cluster/commit/8a7f539)) 34 | * windows tests 2 ([93aebad](https://github.com/nuxt-community/nuxt-generate-cluster/commit/93aebad)) 35 | 36 | 37 | ### Features 38 | 39 | * exit immediately when fatal error occurs ([e3a1176](https://github.com/nuxt-community/nuxt-generate-cluster/commit/e3a1176)) 40 | 41 | 42 | 43 | 44 | ## [2.3.3](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.3.2...v2.3.3) (2019-01-24) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * pass workerInfo to done method ([97df328](https://github.com/nuxt-community/nuxt-generate-cluster/commit/97df328)), closes [#15](https://github.com/nuxt-community/nuxt-generate-cluster/issues/15) 50 | 51 | 52 | 53 | 54 | ## [2.3.2](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.3.1...v2.3.2) (2019-01-13) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * rollup build ([f17a668](https://github.com/nuxt-community/nuxt-generate-cluster/commit/f17a668)) 60 | 61 | 62 | 63 | 64 | ## [2.3.1](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.3.0...v2.3.1) (2019-01-13) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * build new release ([cf1720d](https://github.com/nuxt-community/nuxt-generate-cluster/commit/cf1720d)) 70 | 71 | 72 | 73 | 74 | # [2.3.0](https://github.com/nuxt-community/nuxt-generate-cluster/compare/v2.0.2...v2.3.0) (2019-01-13) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * allow cli options to override stored data ([33a5666](https://github.com/nuxt-community/nuxt-generate-cluster/commit/33a5666)) 80 | * use removeListener for node8 support ([110d8c3](https://github.com/nuxt-community/nuxt-generate-cluster/commit/110d8c3)) 81 | 82 | 83 | ### Features 84 | 85 | * use [@nuxt](https://github.com/nuxt)/eslint-config ([598697f](https://github.com/nuxt-community/nuxt-generate-cluster/commit/598697f)) 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2017, pimlie 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-threaded generate command for Nuxt.js 2 | Build Status 3 | Windows Build Status 4 | Coverage Status 5 | [![npm](https://img.shields.io/npm/dt/nuxt-generate-cluster.svg)](https://www.npmjs.com/package/nuxt-generate-cluster) 6 | [![npm (scoped with tag)](https://img.shields.io/npm/v/nuxt-generate-cluster/latest.svg)](https://www.npmjs.com/package/nuxt-generate-cluster) 7 | 8 | 9 | > Use multiple workers to generate the static files for your Nuxt.js project 10 | 11 | ## Setup 12 | 13 | Install the package 14 | ``` 15 | yarn add nuxt-generate-cluster 16 | ``` 17 | 18 | Add a generate script to your `package.json` 19 | ```js 20 | "scripts": { 21 | "generate": "nuxt-generate -w 4" 22 | } 23 | ``` 24 | 25 | ## Nuxt config options 26 | 27 | Configure the generate options in `nuxt.config.js` 28 | ```js 29 | generate: { 30 | workers: 4, 31 | workerConcurrency: 500, 32 | concurrency: 500, 33 | routes (callback, params) { 34 | return axios.get('https://api.example.com/routes?since=' + params.lastFinished) 35 | .then((res) => { 36 | return res.data 37 | }) 38 | }, 39 | done ({ duration, errors, workerInfo }) { 40 | if (errors.length) { 41 | axios.post('https://api.example.com/routes', { generate_errors: errors }) 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | ### `workers` 48 | - Default: number of processors 49 | 50 | The number of workers that should be started. It probably has no use to start more workers then number of processors in your system. 51 | 52 | ### `workerConcurrency` 53 | - Default: `500` 54 | 55 | To even the load between workers they are sent batches of routes to generate, otherwise a worker with 'easy' routes might finish long before others. Workers will also still use the concurrency option from Nuxt. 56 | 57 | ### `routes` 58 | 59 | The default [Nuxt.js routes method](https://nuxtjs.org/api/configuration-generate#routes) has been extended so you can pass additional parameters to it, see params parameter in example config under Setup. By default 60 | it will list 3 timestamps: 61 | 62 | - `lastStarted` 63 | The unix timestamp when the nuxt-generate command was last executed, should be just now 64 | 65 | - `lastBuilt` 66 | The unix timestamp when the nuxt project was last built by nuxt-generate 67 | 68 | - `lastFinished` 69 | The unix timestamp when nuxt-generate last finished succesfully (eg not interrupted by `ctrl+c`) 70 | 71 | > Timestamps are locally stored in `~/.data-store/nuxt-generate-cluster.json`, see [data-store](https://github.com/jonschlinkert/data-store) 72 | 73 | ### `beforeWorkers` 74 | 75 | This method is called on the master just before the workers are started/forked. It receives the Nuxt options as first argument. 76 | 77 | Use this method if you experience serialization issues or when your Nuxt config is too big. The Nuxt options are stringified and then passed as environment variable to the workers, on Windows there seems to be a maximum size of 32KB for env variables. 78 | 79 | Properties which should be safe to remove are (not a complete list): 80 | - buildModules (and their options) 81 | - serverMiddleware 82 | 83 | ### `done` 84 | 85 | This method will be called when all workers are finished, it receives two arguments: 86 | 87 | - The first argument is an object with statistics: 88 | - `duration` 89 | The total time in seconds that the command ran 90 | 91 | - `errors` 92 | An array of all the errors that were encountered. Errors can have type `handled` or `unhandled`, for the latter the error message will contain the stacktrace 93 | 94 | ```js 95 | [ { type: 'handled', 96 | route: '/non-existing-link', 97 | error: 98 | { statusCode: 404, 99 | message: 'The message from your 404 page' } } 100 | ] 101 | ``` 102 | 103 | - `workerInfo` 104 | An object with detailed information about each worker. Data passed is from the watchdog object that we use internally to monitor the worker status. 105 | 106 | ```js 107 | {{ '6707': // the process id of the worker 108 | { start: [ 1929158, 859524606 ], // process.hrtime the worker was started 109 | duration: 109567, // how long the worker was active 110 | signal: 0, // the signal by which the worker was killed (if any) 111 | code: 0, // the exit status of the worker 112 | routes: 73, // the number of routes generated by this worker 113 | errors: [] }, // the errors this worker encountered, errors of all workers 114 | // combined is the error argument above 115 | } 116 | ``` 117 | 118 | - The second argument is the Nuxt instance 119 | 120 | ## Command-line options 121 | 122 | > Please note that you need to explicitly indicate with `-b` that you want to (re-)build your project 123 | 124 | ``` 125 | $ ./node_modules/.bin/nuxt-generate -h 126 | 127 | Multi-threaded generate for nuxt using cluster 128 | 129 | Description 130 | Generate a static web application (server-rendered) 131 | Usage 132 | $ nuxt-generate 133 | Options 134 | -b, --build Whether to (re-)build the nuxt project 135 | -c, --config-file Path to Nuxt.js config file (default: nuxt.config.js) 136 | -h, --help Displays this message 137 | -p, --params Extra parameters which should be passed to routes method 138 | (should be a JSON string or queryString) 139 | -q, --quiet Decrease verbosity (repeat to decrease more) 140 | -v, --verbose Increase verbosity (repeat to increase more) 141 | --fail-on-page-error Immediately exit when a page throws an unhandled error 142 | -w, --workers [NUM] How many workers should be started 143 | (default: # cpus) 144 | -wc [NUM], How many routes should be sent to 145 | --worker-concurrency [NUM] a worker per iteration 146 | 147 | ``` 148 | 149 | If you need to have more control which routes should be generated, use the `-p` option to pass additional parameters to your routes method. 150 | 151 | ``` 152 | # nuxt.config.js 153 | generate: { 154 | routes (callback, params) { 155 | console.log(params) 156 | } 157 | } 158 | 159 | $ nuxt-generate -w 2 -p id=1&id=2 160 | // will print => 161 | { id: [ '1', '2' ], 162 | lastStarted: 1508609323, 163 | lastBuilt: 0, 164 | lastFinished: 0 } 165 | ``` 166 | 167 | If you are using a npm script under bash use `--` to pass the parameters to nuxt-generate instead of npm: 168 | 169 | ``` 170 | $ npm run generate -- -p '{ "id": [1,2,3] }' 171 | // will print => 172 | { id: [ 1, 2, 3 ], 173 | lastStarted: 1508786574, 174 | lastBuilt: 0, 175 | lastFinished: 0 } 176 | ``` 177 | 178 | ## Logging 179 | 180 | You can use multiple `-v` or `-q` on the command-line to increase or decrease verbosity. 181 | We use consola for logging and set a default log level of 3 unless DEBUG is set, then its 5. 182 | If you want to log debug messages without setting DEBUG you can use `-vv` on the command-line 183 | 184 | The difference between log levels 2, 3 and 4 are: 185 | 186 | - `Level 2` (-q) 187 | Only print master messages like worker started / exited. No worker messages. 188 | 189 | - `Level 3` 190 | Also print which routes the workers generated 191 | 192 | - `Level 4` (-v) 193 | Also print how much time the route generation took and messages between master and workers 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "Current" 4 | 5 | cache: 6 | - 'node_modules -> yarn.lock' 7 | - '%LOCALAPPDATA%\\Yarn -> yarn.lock' 8 | 9 | #build: off 10 | 11 | skip_branch_with_pr: false 12 | 13 | # Install scripts. (runs after repo cloning) 14 | install: 15 | - ps: Install-Product node $env:nodejs_version x64 16 | - node --version 17 | - yarn --version 18 | - yarn install 19 | 20 | test_script: 21 | - yarn test 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | ["@babel/preset-env", { 6 | "targets": { "node": "current" } 7 | }] 8 | ], 9 | "plugins": ["dynamic-import-node"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/nuxt-generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cluster = require('cluster') 4 | 5 | if (cluster.isMaster) { 6 | const meow = require('meow') 7 | 8 | const cli = meow(` 9 | Description 10 | Generate a static web application (server-rendered) 11 | Usage 12 | $ nuxt-generate 13 | Options 14 | -b, --build Whether to (re-)build the nuxt project 15 | -c, --config-file Path to Nuxt.js config file (default: nuxt.config.js) 16 | -h, --help Displays this message 17 | -p, --params Extra parameters which should be passed to routes method 18 | (should be a JSON string or queryString) 19 | -q, --quiet Decrease verbosity (repeat to decrease more) 20 | -v, --verbose Increase verbosity (repeat to increase more) 21 | --fail-on-page-error Immediately exit when a page throws an unhandled error 22 | -w, --workers [NUM] How many workers should be started 23 | (default: # cpus) 24 | -wc [NUM], How many routes should be sent to 25 | --worker-concurrency [NUM] a worker per iteration 26 | 27 | `, { 28 | flags: { 29 | build: { 30 | type: 'boolean', 31 | default: false, 32 | alias: 'b' 33 | }, 34 | config: { 35 | type: 'string', 36 | default: 'nuxt.config.js', 37 | alias: 'c' 38 | }, 39 | help: { 40 | type: 'boolean', 41 | default: false, 42 | alias: 'h' 43 | }, 44 | params: { 45 | type: 'string', 46 | default: '', 47 | alias: 'p' 48 | }, 49 | quiet: { 50 | type: 'boolean', 51 | default: false 52 | }, 53 | verbose: { 54 | type: 'boolean', 55 | default: false 56 | }, 57 | workers: { 58 | type: 'number', 59 | default: 0, 60 | alias: 'w' 61 | }, 62 | 'worker-concurrency': { 63 | type: 'boolean', 64 | default: false, 65 | alias: 'wc' 66 | }, 67 | 'fail-on-page-error': { 68 | type: 'boolean', 69 | default: false 70 | } 71 | } 72 | }) 73 | 74 | const resolve = require('path').resolve 75 | const existsSync = require('fs').existsSync 76 | const store = new (require('data-store'))('nuxt-generate-cluster') 77 | 78 | const rootDir = resolve(cli.input[0] || '.') 79 | const nuxtConfigFile = resolve(rootDir, cli.flags.config) 80 | 81 | let options = null 82 | if (existsSync(nuxtConfigFile)) { 83 | const esm = require('esm')(module, { 84 | cache: false, 85 | cjs: { 86 | cache: true, 87 | vars: true, 88 | namedExports: true 89 | } 90 | }) 91 | 92 | delete require.cache[nuxtConfigFile] 93 | options = esm(nuxtConfigFile) 94 | options = options.default || options 95 | } else if (cli.flags.config && cli.flags.config !== 'nuxt.config.js') { 96 | console.error(`> Could not load config file ${cli.flags.config}`) // eslint-disable-line no-console 97 | process.exit(1) 98 | } 99 | 100 | if (!options) { 101 | cli.showHelp() 102 | } 103 | 104 | options.rootDir = typeof options.rootDir === 'string' ? options.rootDir : rootDir 105 | options.dev = false // Force production mode (no webpack middleware called) 106 | 107 | let params 108 | if (cli.flags.params) { 109 | try { 110 | params = JSON.parse(cli.flags.params) 111 | } catch (e) {} 112 | 113 | params = params || require('querystring').parse(cli.flags.params) 114 | } 115 | 116 | const countFlags = (flag) => { 117 | return cli.flags[flag] === true 118 | ? 1 119 | : ( 120 | Array.isArray(cli.flags[flag]) 121 | ? cli.flags[flag].length 122 | : 0 123 | ) 124 | } 125 | 126 | const storeTime = (key, time) => { 127 | timers[key] = time || Math.round(new Date().getTime() / 1000) 128 | store.set(timers) 129 | store.save() 130 | } 131 | 132 | const timers = Object.assign({ 133 | lastStarted: 0, 134 | lastBuilt: 0, 135 | lastFinished: 0 136 | }, store.data || {}) 137 | 138 | const { Master } = require('..') 139 | 140 | // require consola after importing Master 141 | const consola = require('consola') 142 | consola.addReporter({ 143 | log (logObj) { 144 | if (logObj.type === 'fatal') { 145 | // Exit immediately on fatal error 146 | // the error itself is already printed by the other reporter 147 | // because logging happens sync and this reporter is added 148 | // after the normal one 149 | process.exit(1) 150 | } 151 | } 152 | }) 153 | 154 | storeTime('lastStarted') 155 | const master = new Master(options, { 156 | adjustLogLevel: countFlags('v') - countFlags('q'), 157 | workerCount: cli.flags.workers, 158 | workerConcurrency: cli.flags.workerConcurrency, 159 | failOnPageError: cli.flags.failOnPageError 160 | }) 161 | 162 | master.hook('built', (params) => { 163 | storeTime('lastBuilt') 164 | }) 165 | 166 | master.hook('done', ({ duration, errors, workerInfo }) => { 167 | storeTime('lastFinished') 168 | 169 | consola.log(`HTML Files generated in ${duration}s`) 170 | 171 | if (errors.length) { 172 | const report = errors.map(({ type, route, error }) => { 173 | /* istanbul ignore if */ 174 | if (type === 'unhandled') { 175 | return `Route: '${route}'\n${error.stack}` 176 | } else { 177 | return `Route: '${route}' thrown an error: \n` + JSON.stringify(error) 178 | } 179 | }) 180 | consola.error('==== Error report ==== \n' + report.join('\n\n')) 181 | } 182 | }) 183 | 184 | params = Object.assign({}, store.data || {}, params || {}) 185 | master.run({ build: cli.flags.build, params }) 186 | } else { 187 | const { Worker } = require('..') 188 | Worker.start() 189 | } 190 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import commonJS from 'rollup-plugin-commonjs' 3 | import nodeResolve from 'rollup-plugin-node-resolve' 4 | 5 | import pck from '../package.json' 6 | import nuxt from '../node_modules/nuxt/package.json' 7 | 8 | const dependencies = [ 9 | 'cluster', 10 | 'fs', 11 | 'os', 12 | 'path', 13 | 'util', 14 | ...Object.keys(pck.dependencies), 15 | ...Object.keys(nuxt.dependencies) 16 | ] 17 | 18 | Object.keys(nuxt.dependencies).forEach((nuxtPkg) => { 19 | const pck = require(`../node_modules/${nuxtPkg}/package.json`) 20 | Array.prototype.push.apply(dependencies, Object.keys(pck.dependencies).filter(p => !dependencies.includes(p))) 21 | }) 22 | 23 | const rootDir = resolve(__dirname, '..') 24 | 25 | export default [{ 26 | input: resolve(rootDir, 'lib', 'index.js'), 27 | output: { 28 | name: 'Nuxt Generate Cluster', 29 | file: resolve(rootDir, 'dist', 'generator.js'), 30 | format: 'cjs', 31 | preferConst: true, 32 | sourcemap: true 33 | }, 34 | external: dependencies, 35 | plugins: [ 36 | nodeResolve({ 37 | only: [/lodash/] 38 | }), 39 | commonJS() 40 | ] 41 | }] 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | if (fs.existsSync(path.resolve(__dirname, '.git'))) { 5 | // Use esm version when using linked repository to prevent builds 6 | const requireModule = require('esm')(module, { 7 | cache: false, 8 | cjs: { 9 | cache: true, 10 | vars: true, 11 | namedExports: true 12 | } 13 | }) 14 | 15 | module.exports = requireModule('./lib/index.js').default 16 | } else { 17 | // Use production bundle by default 18 | module.exports = require('./dist/generator.js') 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | 4 | coverageDirectory: './coverage/', 5 | 6 | collectCoverageFrom: [ 7 | 'lib/**' 8 | ], 9 | 10 | setupFilesAfterEnv: [ 11 | './test/utils/setup' 12 | ], 13 | 14 | testPathIgnorePatterns: [ 15 | 'test/fixtures/.*/.*?/' 16 | ], 17 | 18 | transformIgnorePatterns: [ 19 | 'node_modules/(?!(@nuxt|nuxt|@babel\/runtime))' 20 | ], 21 | 22 | transform: { 23 | '^.+\\.js$': 'babel-jest' 24 | }, 25 | 26 | moduleFileExtensions: [ 27 | 'js', 28 | 'json' 29 | ], 30 | 31 | expand: true, 32 | 33 | forceExit: false 34 | } 35 | -------------------------------------------------------------------------------- /lib/async/index.js: -------------------------------------------------------------------------------- 1 | import Master from './master' 2 | import Worker from './worker' 3 | import * as Mixins from './mixins' 4 | 5 | export { 6 | Master, 7 | Worker, 8 | Mixins 9 | } 10 | -------------------------------------------------------------------------------- /lib/async/master.js: -------------------------------------------------------------------------------- 1 | import pull from 'lodash/pull' 2 | import Debug from 'debug' 3 | import { Master as GenerateMaster, Commands } from '../generate' 4 | import { Messaging } from './mixins' 5 | import Worker from './worker' 6 | 7 | const debug = Debug('nuxt:master') 8 | 9 | export default class Master extends Messaging(GenerateMaster) { 10 | constructor (options, { workerCount, workerConcurrency, setup } = {}) { 11 | super(options, { workerCount, workerConcurrency }) 12 | 13 | this.workers = [] 14 | this.lastWorkerId = 0 15 | 16 | this.hook(Commands.sendRoutes, this.sendRoutes.bind(this)) 17 | this.hook(Commands.sendErrors, this.saveErrors.bind(this)) 18 | 19 | this.watchdog.hook('isWorkerAlive', (worker) => { 20 | /* istanbul ignore next */ 21 | return typeof this.workers[worker.id] !== 'undefined' 22 | }) 23 | } 24 | 25 | async run (args) { 26 | await this.startListeningForMessages() 27 | 28 | await super.run(args) 29 | } 30 | 31 | async getRoutes (params) { 32 | debug('Retrieving routes') 33 | 34 | const success = await super.getRoutes(params) 35 | 36 | if (success) { 37 | debug(`A total of ${this.routes.length} routes will be generated`) 38 | } 39 | } 40 | 41 | sendRoutes (worker) { 42 | const routes = this.getBatchRoutes() 43 | 44 | if (!routes.length) { 45 | debug(`No more routes, exiting worker ${worker.id}`) 46 | 47 | this.onExit(worker) 48 | } else { 49 | debug(`Sending ${routes.length} routes to worker ${worker.id}`) 50 | 51 | this.watchdog.appendInfo(worker.id, 'routes', routes.length) 52 | this.sendCommand(worker, Commands.sendRoutes, routes) 53 | } 54 | } 55 | 56 | saveErrors (worker, args) { 57 | if (typeof args !== 'undefined' && args.length) { 58 | Array.prototype.push.apply(this.errors, args) 59 | this.watchdog.appendInfo(worker.id, 'errors', args.length) 60 | } 61 | } 62 | 63 | async done () { 64 | const Iter = this.watchdog.iterator() 65 | 66 | let worker 67 | while ((worker = Iter.next()) && !worker.done) { 68 | worker = worker.value 69 | 70 | let workerMsg = `Worker ${worker.id} generated ${worker.routes} routes in ${Math.round(worker.duration / 1E8) / 10}s` 71 | if (worker.errors > 0) { 72 | workerMsg += ` with ${worker.errors} error(s)` 73 | } 74 | debug(workerMsg) 75 | } 76 | 77 | await super.done() 78 | } 79 | 80 | async startWorkers (options) { 81 | for (let i = await this.watchdog.countAlive(); i < this.workerCount; i++) { 82 | this.lastWorkerId++ 83 | const worker = new Worker(options, {}, this.lastWorkerId) 84 | this.workers.push(worker) 85 | this.watchdog.addWorker(worker.id) 86 | 87 | worker.run() 88 | debug(`Worker ${worker.id} started`) 89 | } 90 | } 91 | 92 | async onExit (worker) { 93 | const workerId = worker.id 94 | 95 | this.watchdog.exitWorker(workerId) 96 | pull(this.workers, worker) 97 | 98 | const message = `Worker ${workerId} exited` 99 | debug(message) 100 | 101 | const allDead = await this.watchdog.allDead() 102 | if (allDead) { 103 | await this.done() 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/async/mixins/index.js: -------------------------------------------------------------------------------- 1 | import Messaging from './messaging' 2 | 3 | export { 4 | Messaging 5 | } 6 | -------------------------------------------------------------------------------- /lib/async/mixins/messaging.js: -------------------------------------------------------------------------------- 1 | import indexOf from 'lodash/indexOf' 2 | import values from 'lodash/values' 3 | import { Commands } from '../../generate' 4 | import { consola } from '../../utils' 5 | 6 | let master = null 7 | 8 | export default Base => class extends Base { 9 | startListeningForMessages () { 10 | if (typeof this.__isListening === 'undefined') { 11 | this.__isListening = false 12 | } else /* istanbul ignore next */ if (this.__isListening) { 13 | return 14 | } 15 | 16 | if (typeof this.workers !== 'undefined') { 17 | master = this 18 | } 19 | this.__isListening = true 20 | } 21 | 22 | hasCommand (cmd) { 23 | if (typeof this._commandsArray === 'undefined') { 24 | this._commandsArray = values(Commands) 25 | } 26 | return cmd && indexOf(this._commandsArray, cmd) > -1 27 | } 28 | 29 | async receiveCommand (worker, message) { 30 | const cmd = message.cmd 31 | 32 | if (!this.hasCommand(cmd)) { 33 | consola.error(`Received unknown command ${cmd}`) 34 | } else if (!this.hasHooks(cmd)) { 35 | consola.error(`No handler registered for command ${cmd}`) 36 | } else if (worker) { 37 | await this.callHook(cmd, worker, message.args) 38 | } else { 39 | await this.callHook(cmd, message.args) 40 | } 41 | } 42 | 43 | sendCommand (worker, cmd, args) { 44 | if (arguments.length === 1) { 45 | cmd = worker 46 | worker = undefined 47 | } 48 | 49 | if (!this.hasCommand(cmd)) { 50 | consola.error(`Trying to send unknown command ${cmd}`) 51 | return 52 | } 53 | 54 | const message = { cmd, args } 55 | 56 | if (worker) { 57 | worker.receiveCommand(undefined, message) 58 | } else { 59 | master.receiveCommand(this, message) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/async/worker.js: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import { Worker as GenerateWorker, Commands } from '../generate' 3 | import { Messaging } from './mixins' 4 | 5 | const debug = Debug('nuxt:worker') 6 | 7 | export default class Worker extends Messaging(GenerateWorker) { 8 | constructor (options, cliOptions = {}, id) { 9 | super(options) 10 | 11 | this.setId(id) 12 | 13 | this.hook(Commands.sendRoutes, this.generateRoutes.bind(this)) 14 | } 15 | 16 | async init () { 17 | await super.init() 18 | 19 | this.generator.nuxt.hook('generate:routeCreated', ({ route, path }) => { 20 | path = path.replace(this.generator.distPath, '') 21 | debug(`Worker ${this.id} generated file: ${path}`) 22 | }) 23 | } 24 | 25 | async run () { 26 | await super.run() 27 | 28 | this.startListeningForMessages() 29 | this.sendCommand(Commands.sendRoutes) 30 | } 31 | 32 | async generateRoutes (args) { 33 | const routes = args 34 | debug(`Worker ${this.id} received ${routes.length} routes = require(master`) 35 | 36 | let errors 37 | try { 38 | errors = await super.generateRoutes(routes) 39 | } catch (e) { 40 | } 41 | 42 | if (errors && errors.length) { 43 | errors = errors.map((error) => { 44 | error.workerId = this.id 45 | 46 | /* istanbul ignore next */ 47 | if (error.type === 'unhandled') { 48 | // convert error stack to a string already, we cant send a stack object to the master process 49 | error.error = { stack: '' + error.error.stack } 50 | } 51 | return error 52 | }) 53 | 54 | this.sendCommand(undefined, Commands.sendErrors, errors) 55 | } 56 | 57 | this.sendCommand(Commands.sendRoutes) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/cluster/index.js: -------------------------------------------------------------------------------- 1 | import Master from './master' 2 | import Worker from './worker' 3 | 4 | export { 5 | Master, 6 | Worker 7 | } 8 | -------------------------------------------------------------------------------- /lib/cluster/master.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster' 2 | import { Master as GenerateMaster, Commands } from '../generate' 3 | import { consola, messaging } from '../utils' 4 | 5 | export default class Master extends GenerateMaster { 6 | constructor (options, { workerCount, workerConcurrency, failOnPageError, setup, adjustLogLevel } = {}) { 7 | super(options, { adjustLogLevel, workerCount, failOnPageError, workerConcurrency }) 8 | 9 | if (setup) { 10 | cluster.setupMaster(setup) 11 | } 12 | 13 | cluster.on('fork', this.onFork.bind(this)) 14 | cluster.on('exit', this.onExit.bind(this)) 15 | 16 | global._ngc_log_tag = 'master' 17 | 18 | messaging.on(Commands.sendRoutes, (data, senderId, worker) => { 19 | this.sendRoutes(senderId, worker) 20 | }) 21 | messaging.on(Commands.sendErrors, (data, senderId, worker) => { 22 | this.saveErrors(senderId, worker, data) 23 | }) 24 | 25 | this.watchdog.hook('isWorkerAlive', (worker) => { 26 | return typeof cluster.workers[worker.id] !== 'undefined' && cluster.workers[worker.id].isConnected() 27 | }) 28 | } 29 | 30 | async getRoutes (params) { 31 | consola.master('retrieving routes') 32 | 33 | const success = await super.getRoutes(params) 34 | 35 | if (success) { 36 | consola.master(`${this.routes.length} routes will be generated`) 37 | } 38 | } 39 | 40 | sendRoutes (senderId, worker) { 41 | const routes = this.getBatchRoutes() 42 | 43 | if (!routes.length) { 44 | consola.master(`no more routes, exiting worker ${worker.id}`) 45 | 46 | worker.disconnect() 47 | } else { 48 | consola.cluster(`sending ${routes.length} routes to worker ${worker.id}`) 49 | 50 | this.watchdog.appendInfo(worker.id, 'routes', routes.length) 51 | 52 | messaging.send(senderId, Commands.sendRoutes, routes) 53 | } 54 | } 55 | 56 | saveErrors (senderId, worker, args) { 57 | if (typeof args !== 'undefined' && args.length) { 58 | Array.prototype.push.apply(this.errors, args) 59 | this.watchdog.appendInfo(worker.id, 'errors', args.length) 60 | } 61 | } 62 | 63 | async done () { 64 | const Iter = this.watchdog.iterator() 65 | 66 | let worker 67 | while ((worker = Iter.next()) && !worker.done) { 68 | worker = worker.value 69 | 70 | let workerMsg = `worker ${worker.id} generated ${worker.routes} routes in ${Math.round(worker.duration / 1E8) / 10}s` 71 | if (worker.errors > 0) { 72 | workerMsg += ` with ${worker.errors} error(s)` 73 | } 74 | consola.cluster(workerMsg) 75 | } 76 | 77 | await super.done() 78 | } 79 | 80 | async startWorkers (options) { 81 | // Dont start more workers then there are routes 82 | const maxWorkerCount = Math.min(this.workerCount, this.routes.length) 83 | 84 | for (let i = await this.watchdog.countAlive(); i < maxWorkerCount; i++) { 85 | cluster.fork({ 86 | args: JSON.stringify({ 87 | options, 88 | cliOptions: { 89 | failOnPageError: this.failOnPageError 90 | } 91 | }) 92 | }) 93 | } 94 | } 95 | 96 | onFork (worker) { 97 | const pid = worker.process.pid 98 | consola.master(`worker ${worker.id} started with pid ${pid}`) 99 | 100 | this.watchdog.addWorker(worker.id, { pid }) 101 | } 102 | 103 | async onExit (worker, code, signal) { 104 | const workerId = worker.id 105 | 106 | this.watchdog.exitWorker(workerId, { code, signal }) 107 | 108 | let message = `worker ${workerId} exited` 109 | 110 | let fatal = false 111 | if (code) { 112 | message += ` with status code ${code}` 113 | fatal = true 114 | } 115 | 116 | if (signal) { 117 | message += ` by signal ${signal}` 118 | fatal = true 119 | } 120 | 121 | if (fatal) { 122 | consola.fatal(message) 123 | } else { 124 | consola.master(message) 125 | } 126 | 127 | const allDead = await this.watchdog.allDead() 128 | if (allDead) { 129 | await this.done() 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/cluster/worker.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster' 2 | import { consola, messaging } from '../utils' 3 | import { Worker as GenerateWorker, Commands } from '../generate' 4 | 5 | export default class Worker extends GenerateWorker { 6 | constructor (options, cliOptions = {}) { 7 | super(options, cliOptions) 8 | 9 | if (cluster.isWorker) { 10 | this.setId(cluster.worker.id) 11 | } 12 | 13 | global._ngc_log_tag = `worker ${this.id}` 14 | 15 | messaging.alias = `worker ${this.id}` 16 | messaging.on(Commands.sendRoutes, (data) => { 17 | /* istanbul ignore next */ 18 | this.generateRoutes(data) 19 | }) 20 | } 21 | 22 | static start () { 23 | const args = JSON.parse(process.env.args) 24 | 25 | const worker = new Worker(args.options, args.cliOptions) 26 | worker.run() 27 | return worker 28 | } 29 | 30 | async init () { 31 | await super.init() 32 | 33 | let renderingStartTime 34 | /* istanbul ignore next */ 35 | if (consola.level > 3) { 36 | const debug = consola.debug 37 | consola.debug = (msg) => { 38 | if (msg.startsWith('Rendering url')) { 39 | renderingStartTime = process.hrtime() 40 | } 41 | debug(msg) 42 | } 43 | } 44 | 45 | this.generator.nuxt.hook('generate:routeCreated', ({ route, path, errors }) => { 46 | let durationMessage = '' 47 | if (consola.level > 3) { 48 | const taken = process.hrtime(renderingStartTime) 49 | const duration = Math.round((taken[0] * 1e9 + taken[1]) / 1e6) 50 | durationMessage += ` (${duration}ms)` 51 | } 52 | path = path.replace(this.generator.distPath, '') 53 | 54 | if (errors.length) { 55 | consola.error(`error generating: ${path}` + durationMessage) 56 | } else { 57 | consola.success(`generated: ${path}` + durationMessage) 58 | } 59 | }) 60 | } 61 | 62 | async run () { 63 | await super.run() 64 | 65 | messaging.send('master', Commands.sendRoutes) 66 | } 67 | 68 | async generateRoutes (args) { 69 | const routes = args 70 | consola.cluster(`received ${routes.length} routes`) 71 | 72 | let errors 73 | try { 74 | errors = await super.generateRoutes(routes) 75 | } catch (e) { 76 | /* istanbul ignore next */ 77 | if (cluster.isWorker) { 78 | process.exit(1) 79 | } 80 | } 81 | 82 | if (errors && errors.length) { 83 | errors = errors.map((error) => { 84 | error.workerId = this.id 85 | 86 | /* istanbul ignore next */ 87 | if (error.type === 'unhandled') { 88 | // convert error stack to a string already, we cant send a stack object to the master process 89 | error.error = { stack: '' + error.error.stack } 90 | 91 | if (this.failOnPageError) { 92 | consola.fatal(`Unhandled page error occured for route ${error.route}`) 93 | } 94 | } 95 | return error 96 | }) 97 | 98 | messaging.send(null, Commands.sendErrors, errors) 99 | } 100 | 101 | messaging.send(null, Commands.sendRoutes) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/generate/commands.js: -------------------------------------------------------------------------------- 1 | export default { 2 | sendErrors: 'handleErrors', 3 | sendRoutes: 'requestRoutes', 4 | logSuccess: 'logSuccess', 5 | logError: 'logError' 6 | } 7 | -------------------------------------------------------------------------------- /lib/generate/index.js: -------------------------------------------------------------------------------- 1 | import Commands from './commands' 2 | import Watchdog from './watchdog' 3 | import Master from './master' 4 | import Worker from './worker' 5 | 6 | export { 7 | Commands, 8 | Watchdog, 9 | Master, 10 | Worker 11 | } 12 | -------------------------------------------------------------------------------- /lib/generate/master.js: -------------------------------------------------------------------------------- 1 | import uniq from 'lodash/uniq' 2 | import { Hookable } from '../mixins' 3 | import { consola } from '../utils' 4 | import { getNuxt, getGenerator } from '../utils/nuxt' 5 | import Watchdog from './watchdog' 6 | 7 | export default class Master extends Hookable() { 8 | constructor (options, { workerCount, workerConcurrency, failOnPageError, adjustLogLevel }) { 9 | super() 10 | 11 | this.options = options 12 | 13 | this.watchdog = new Watchdog() 14 | this.startTime = process.hrtime() 15 | 16 | this.workerCount = parseInt(workerCount) 17 | this.workerConcurrency = parseInt(workerConcurrency) 18 | this.failOnPageError = failOnPageError 19 | 20 | if (adjustLogLevel) { 21 | consola.level = consola._defaultLevel + adjustLogLevel 22 | this.options.__workerLogLevel = consola.level 23 | } 24 | 25 | this.routes = [] 26 | this.errors = [] 27 | } 28 | 29 | async init () { 30 | if (this.generator) { 31 | return 32 | } 33 | 34 | const level = consola.level 35 | const nuxt = await getNuxt(this.options) 36 | consola.level = level // ignore whatever Nuxt thinks the level should be 37 | 38 | this.generator = await getGenerator(nuxt) 39 | 40 | this.workerCount = this.workerCount || parseInt(nuxt.options.generate.workers) || require('os').cpus().length 41 | this.workerConcurrency = this.workerConcurrency || parseInt(nuxt.options.generate.workerConcurrency) || 500 42 | } 43 | 44 | async run ({ build, params } = {}) { 45 | await this.init() 46 | 47 | if (build) { 48 | await this.build() 49 | await this.callHook('built', params) 50 | } else { 51 | await this.initiate() 52 | } 53 | 54 | await this.getRoutes(params) 55 | 56 | if (this.routes.length < 1) { 57 | consola.warn('No routes so not starting workers') 58 | return 59 | } 60 | 61 | let options = this.options 62 | 63 | if (typeof this.options.generate.beforeWorkers === 'function') { 64 | options = this.options.generate.beforeWorkers(this.options) || this.options 65 | } 66 | 67 | await this.startWorkers(options) 68 | } 69 | 70 | async initiate (build) { 71 | if (!build) { 72 | build = false 73 | } 74 | 75 | await this.generator.initiate({ build, init: build }) 76 | } 77 | 78 | async build () { 79 | await this.initiate(true) 80 | } 81 | 82 | async getRoutes (params) { 83 | try { 84 | const routes = await this.generator.initRoutes(params) 85 | if (routes.length) { 86 | // add routes to any existing routes 87 | Array.prototype.push.apply(this.routes, routes) 88 | this.routes = uniq(this.routes) 89 | } 90 | return true 91 | } catch (e) { 92 | return false 93 | } 94 | } 95 | 96 | calculateBatchSize () { 97 | // Even the load between workers 98 | let workerConcurrency = this.workerConcurrency 99 | if (this.routes.length < this.workerCount * this.workerConcurrency) { 100 | workerConcurrency = Math.ceil(this.routes.length / this.workerCount) 101 | } 102 | 103 | return workerConcurrency 104 | } 105 | 106 | getBatchRoutes () { 107 | const batchSize = this.calculateBatchSize() 108 | const routes = this.routes.splice(0, batchSize) 109 | 110 | return routes 111 | } 112 | 113 | async done (workerInfo) { 114 | await this.generator.afterGenerate() 115 | 116 | let duration = process.hrtime(this.startTime) 117 | duration = Math.round((duration[0] * 1E9 + duration[1]) / 1E8) / 10 118 | 119 | const info = { 120 | duration, 121 | errors: this.errors, 122 | workerInfo: workerInfo || this.watchdog.workers 123 | } 124 | 125 | if (this.options.generate && typeof this.options.generate.done === 'function') { 126 | await this.options.generate.done(info, this.generator.nuxt) 127 | } 128 | 129 | await this.callHook('done', info) 130 | 131 | this.errors = [] 132 | } 133 | 134 | startWorkers (options) { 135 | consola.error('Should be implemented by a derived class') 136 | return false 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/generate/watchdog.js: -------------------------------------------------------------------------------- 1 | import { Hookable } from '../mixins' 2 | import { consola } from '../utils' 3 | 4 | export default class Watchdog extends Hookable() { 5 | constructor () { 6 | super() 7 | 8 | this.workers = {} 9 | } 10 | 11 | *iterator () { 12 | const workerIds = Object.keys(this.workers) 13 | 14 | let i = 0 15 | while (i < workerIds.length) { 16 | yield this.workers[workerIds[i]] 17 | i++ 18 | } 19 | } 20 | 21 | addInfo (workerId, key, extraInfo) { 22 | if (arguments.length === 2) { 23 | extraInfo = key 24 | key = undefined 25 | } 26 | 27 | if (this.workers[workerId]) { 28 | if (key) { 29 | this.workers[workerId][key] = extraInfo 30 | } else { 31 | this.workers[workerId] = Object.assign(this.workers[workerId], extraInfo || {}) 32 | } 33 | } 34 | } 35 | 36 | appendInfo (workerId, key, extraInfo) { 37 | if (this.workers[workerId]) { 38 | const keyType = typeof this.workers[workerId][key] 39 | 40 | if (keyType === 'undefined') { 41 | consola.error(`Key ${key} is undefined for worker ${workerId}`) 42 | } else if (keyType === 'string') { 43 | this.workers[workerId][key] += extraInfo 44 | } else if (keyType === 'number') { 45 | this.workers[workerId][key] += parseInt(extraInfo) 46 | } else if (Array.isArray(this.workers[workerId][key])) { 47 | Array.prototype.push.apply(this.workers[workerId][key], extraInfo) 48 | } else if (keyType === 'object') { 49 | this.workers[workerId][key] = Object.assign(this.workers[workerId][key], extraInfo || {}) 50 | } 51 | } 52 | } 53 | 54 | addWorker (workerId, extraInfo) { 55 | if (typeof this.workers[workerId] !== 'undefined') { 56 | consola.error(`A worker with workerId ${workerId} is already registered to the watchdog`) 57 | } 58 | 59 | this.workers[workerId] = Object.assign({ 60 | id: workerId, 61 | start: process.hrtime(), 62 | duration: 0, 63 | signal: 0, 64 | code: 0, 65 | routes: 0, 66 | errors: 0 67 | }, extraInfo || {}) 68 | } 69 | 70 | exitWorker (workerId, extraInfo) { 71 | if (this.workers[workerId]) { 72 | const duration = process.hrtime(this.workers[workerId].start) 73 | this.workers[workerId].duration = duration[0] * 1E9 + duration[1] 74 | 75 | if (extraInfo) { 76 | this.addInfo(workerId, extraInfo) 77 | } 78 | } 79 | } 80 | 81 | async countAlive () { 82 | const Iter = this.iterator() 83 | 84 | let alive = 0 85 | let worker 86 | while ((worker = Iter.next()) && !worker.done) { 87 | if (typeof worker.value !== 'undefined') { 88 | const workerAlive = await this.callHook('isWorkerAlive', worker.value) 89 | if (workerAlive) { 90 | alive++ 91 | } 92 | } 93 | } 94 | return alive 95 | } 96 | 97 | allDead () { 98 | const Iter = this.iterator() 99 | 100 | let worker 101 | while ((worker = Iter.next()) && !worker.done) { 102 | if (typeof worker.value !== 'undefined') { 103 | // let isDead = await this.callHook('isWorkerDead', worker.value) 104 | const isDead = this.workers[worker.value.id].duration > 0 105 | if (!isDead) { 106 | return false 107 | } 108 | } 109 | } 110 | return true 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/generate/worker.js: -------------------------------------------------------------------------------- 1 | import { consola } from '../utils' 2 | import { Hookable } from '../mixins' 3 | import { getNuxt, getGenerator } from '../utils/nuxt' 4 | 5 | export default class Worker extends Hookable() { 6 | constructor (options, { failOnPageError } = {}) { 7 | super() 8 | this.options = options 9 | this.id = -1 10 | 11 | this.failOnPageError = failOnPageError 12 | 13 | if (this.options.__workerLogLevel) { 14 | consola.level = this.options.__workerLogLevel 15 | } 16 | } 17 | 18 | setId (id) { 19 | this.id = id 20 | } 21 | 22 | async init () { 23 | /* istanbul ignore next */ 24 | if (this.generator) { 25 | return 26 | } 27 | 28 | const level = consola.level 29 | const nuxt = await getNuxt(this.options) 30 | consola.level = level // ignore whatever Nuxt thinks the level should be 31 | 32 | this.generator = await getGenerator(nuxt) 33 | } 34 | 35 | async run () { 36 | await this.init() 37 | 38 | await this.generator.initiate({ build: false, init: false }) 39 | } 40 | 41 | async generateRoutes (routes) { 42 | let errors = [] 43 | 44 | try { 45 | errors = await this.generator.generateRoutes(routes) 46 | } catch (err) { 47 | consola.error(`Worker ${process.pid}: Exception while generating routes, exiting`) 48 | consola.error('' + err) 49 | throw err 50 | } 51 | 52 | return errors 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import * as Cluster from './cluster' 2 | 3 | export default Cluster 4 | -------------------------------------------------------------------------------- /lib/mixins/hookable.js: -------------------------------------------------------------------------------- 1 | import { consola, sequence } from '../utils' 2 | 3 | export default (Base) => { 4 | if (!Base) { 5 | Base = class {} 6 | } 7 | 8 | return class extends Base { 9 | initHooks () { 10 | if (!this._hooks) { 11 | this._hooks = {} 12 | } 13 | } 14 | 15 | hook (name, fn) { 16 | if (!name || typeof fn !== 'function') { 17 | return 18 | } 19 | this.initHooks() 20 | 21 | this._hooks[name] = this._hooks[name] || [] 22 | this._hooks[name].push(fn) 23 | } 24 | 25 | async callHook (name, ...args) { 26 | if (!this.hasHooks(name)) { 27 | return 28 | } 29 | // debug(`Call ${name} hooks (${this._hooks[name].length})`) 30 | const ret = [] 31 | try { 32 | ret.push(await sequence(this._hooks[name], fn => fn(...args))) 33 | } catch (err) { 34 | consola.error(`> Error on hook "${name}":`) 35 | consola.error(err.message) 36 | } 37 | return ret.length === 1 ? ret[0] : ret 38 | } 39 | 40 | hasHooks (name) { 41 | return this._hooks && !!this._hooks[name] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/mixins/index.js: -------------------------------------------------------------------------------- 1 | import Hookable from './hookable' 2 | 3 | export { 4 | Hookable 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils/consola.js: -------------------------------------------------------------------------------- 1 | import { isMaster } from 'cluster' 2 | import env from 'std-env' 3 | import figures from 'figures' 4 | import chalk from 'chalk' 5 | import messaging from './messaging' 6 | import { ClusterReporter } from './reporters' 7 | 8 | let _consola 9 | if (global.__consolaSet === undefined) { 10 | _consola = global.consola 11 | // Delete the global.consola set by consola self 12 | delete global.consola 13 | } 14 | 15 | let consola = global.consola // eslint-disable-line import/no-mutable-exports 16 | 17 | if (!consola) { 18 | consola = _consola.create({ 19 | level: env.debug ? 5 : 3, 20 | types: { 21 | ..._consola._types, 22 | ...{ 23 | cluster: { 24 | level: 4, 25 | color: 'blue', 26 | icon: chalk.magenta(figures.radioOn) 27 | }, 28 | master: { 29 | level: 2, 30 | color: 'blue', 31 | icon: chalk.cyan(figures.info) 32 | }, 33 | debug: { 34 | level: 5, 35 | color: 'grey' 36 | }, 37 | trace: { 38 | level: 6, 39 | color: 'white' 40 | } 41 | } 42 | } 43 | }) 44 | _consola = null 45 | 46 | if (isMaster) { 47 | /* istanbul ignore next */ 48 | messaging.on('consola', ({ logObj, stream }) => { 49 | logObj.date = new Date(logObj.date) 50 | consola[logObj.type](...logObj.args) 51 | }) 52 | } else { 53 | /* istanbul ignore next */ 54 | consola.setReporters(new ClusterReporter()) 55 | } 56 | 57 | global.__consolaSet = true 58 | global.consola = consola 59 | 60 | // Delete the loaded consola module from node's cache 61 | // so new imports use the above global.consola 62 | delete require.cache[require.resolve('consola')] 63 | } 64 | 65 | export default consola 66 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | import consola from './consola' 2 | import MessageBroker from './message-broker' 3 | import messaging from './messaging' 4 | 5 | export { 6 | consola, 7 | messaging, 8 | MessageBroker 9 | } 10 | 11 | // Copied from Nuxt.Utils 12 | export const sequence = function sequence (tasks, fn) { 13 | return tasks.reduce( 14 | (promise, task) => promise.then(() => fn(task)), 15 | Promise.resolve() 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/message-broker.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster' 2 | import uuid from 'uuid' 3 | import consola from 'consola' 4 | 5 | export default class MessageBroker { 6 | constructor ({ isMaster, masterId, alias, autoListen } = {}, masterRef) { 7 | this.id = uuid() 8 | 9 | this.isMaster = isMaster !== undefined ? isMaster : cluster.isMaster 10 | this.masterId = masterId || 'master' 11 | this.masterRef = masterRef 12 | 13 | this.alias = alias || (this.isMaster ? this.masterId : undefined) 14 | this.listening = false 15 | 16 | this.proxies = {} 17 | this.services = {} 18 | 19 | // this._messageHandler 20 | if (autoListen !== false) { 21 | this.listen() 22 | } 23 | } 24 | 25 | registerWithMaster () { 26 | this.send(this.masterId, '_register', { 27 | alias: this.alias 28 | }) 29 | } 30 | 31 | registerProxy ({ alias }, senderId, ref) { 32 | consola.debug(`registering ${senderId} ` + (alias ? `with alias ${alias}` : 'without alias') + (ref && ref.id ? ` and id ${ref.id}` : '')) 33 | 34 | this.proxies[senderId] = ref 35 | 36 | if (alias) { 37 | this.proxies[alias] = ref 38 | } 39 | } 40 | 41 | listen () { 42 | if (!this.isMaster) { 43 | this.registerWithMaster() 44 | } else { 45 | this.on('_register', (...args) => { 46 | this.registerProxy(...args) 47 | }) 48 | } 49 | 50 | if (!this.listening) { 51 | if (this.isMaster) { 52 | this._messageHandler = (worker, message) => { 53 | /* istanbul ignore next */ 54 | this.handleMessage(message, worker) 55 | } 56 | 57 | cluster.on('message', this._messageHandler) 58 | } else { 59 | this._messageHandler = (message) => { 60 | /* istanbul ignore next */ 61 | this.handleMessage(message) 62 | } 63 | 64 | process.on('message', this._messageHandler) 65 | } 66 | 67 | this.listening = true 68 | } 69 | } 70 | 71 | close () { 72 | if (this._messageHandler) { 73 | if (this.isMaster) { 74 | cluster.removeListener('message', this._messageHandler) 75 | } else { 76 | process.removeListener('message', this._messageHandler) 77 | } 78 | } 79 | } 80 | 81 | handleMessage (message, worker) { 82 | consola.trace((this.alias || this.id) + ' received message', message instanceof Object ? JSON.stringify(message) : message) 83 | 84 | const { receiverId } = message 85 | if (receiverId !== undefined) { 86 | if ( 87 | receiverId === this.id || 88 | receiverId === this.alias || 89 | (this.isMaster && receiverId === this.masterId) 90 | ) { 91 | this.callService(message, worker) 92 | } else if (this.isMaster && this.proxies[receiverId]) { 93 | this.proxies[receiverId].send(message) 94 | } else { 95 | consola.warn(`Proxy ${receiverId} not registered`) 96 | } 97 | } 98 | } 99 | 100 | callService ({ senderId, serviceId, data }, worker) { 101 | if (serviceId in this.services) { 102 | this.services[serviceId](data, senderId, worker) 103 | } else { 104 | consola.warn(`Proxy '${this.alias || this.id}': Service ${serviceId} not registered`) 105 | } 106 | } 107 | 108 | on (serviceId, callback, overwrite) { 109 | if (serviceId in this.services && !overwrite) { 110 | consola.warn(`Service ${serviceId} already registered`) 111 | } else { 112 | this.services[serviceId] = callback 113 | } 114 | } 115 | 116 | send (receiverIdOrAlias, serviceId, data) { 117 | const message = { 118 | receiverId: receiverIdOrAlias || this.masterId, 119 | senderId: this.id, 120 | serviceId, 121 | data 122 | } 123 | this.sendMessage(message) 124 | } 125 | 126 | sendMessage (message) { 127 | if (!this.isMaster && !cluster.isMaster) { 128 | consola.trace('sending message through process', JSON.stringify(message)) 129 | 130 | process.send(message) 131 | } else if (!this.isMaster && this.masterRef) { 132 | return new Promise((resolve) => { 133 | consola.trace('sending message through promise', JSON.stringify(message)) 134 | 135 | const ref = {} 136 | if (message.serviceId === '_register') { 137 | ref.send = (message) => { 138 | this.handleMessage(message) 139 | } 140 | } 141 | 142 | this.masterRef.handleMessage(message, ref) 143 | resolve() 144 | }) 145 | } else if (this.proxies[message.receiverId]) { 146 | consola.trace('sending message through proxy', JSON.stringify(message)) 147 | 148 | this.proxies[message.receiverId].send(message) 149 | } else if (message.receiverId === this.id || message.receiverId === this.alias) { 150 | this.handleMessage(message) 151 | } else { 152 | consola.error(`Unable to send message, unknown receiver ${message.receiverId}`) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/utils/messaging.js: -------------------------------------------------------------------------------- 1 | import MessageBroker from './message-broker' 2 | 3 | export default new MessageBroker() 4 | -------------------------------------------------------------------------------- /lib/utils/nuxt/imports.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import consola from 'consola' 3 | 4 | const localNodeModules = path.resolve(process.cwd(), 'node_modules') 5 | 6 | // Prefer importing modules from local node_modules (for NPX and global bin) 7 | async function _import (modulePath) { 8 | let m 9 | for (const mp of [path.resolve(localNodeModules, modulePath), modulePath]) { 10 | try { 11 | m = await import(mp) 12 | } catch (e) { 13 | /* istanbul ignore next */ 14 | if (e.code !== 'MODULE_NOT_FOUND') { 15 | throw e 16 | } else if (mp === modulePath) { 17 | consola.fatal( 18 | `Module ${modulePath} not found.\n\n`, 19 | 'Please install missing dependency:\n\n', 20 | `Using npm: npm i ${modulePath}\n\n`, 21 | `Using yarn: yarn add ${modulePath}` 22 | ) 23 | } 24 | } 25 | } 26 | return m 27 | } 28 | 29 | export const builder = () => _import('@nuxt/builder') 30 | export const webpack = () => _import('@nuxt/webpack') 31 | export const generator = () => _import('@nuxt/generator') 32 | export const core = () => _import('@nuxt/core') 33 | export const importModule = _import 34 | -------------------------------------------------------------------------------- /lib/utils/nuxt/index.js: -------------------------------------------------------------------------------- 1 | import * as imports from './imports' 2 | 3 | export const getNuxt = async function getNuxt (options) { 4 | const { Nuxt } = await imports.core() 5 | const nuxt = new Nuxt(options) 6 | await nuxt.ready() 7 | return nuxt 8 | } 9 | 10 | export const getBuilder = async function getBuilder (nuxt) { 11 | const { Builder } = await imports.builder() 12 | const { BundleBuilder } = await imports.webpack() 13 | return new Builder(nuxt, BundleBuilder) 14 | } 15 | 16 | export const getGenerator = async function getGenerator (nuxt) { 17 | const { Generator } = await imports.generator() 18 | const builder = await getBuilder(nuxt) 19 | return new Generator(nuxt, builder) 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/reporters/cluster.js: -------------------------------------------------------------------------------- 1 | import messaging from '../messaging' 2 | 3 | // Consola Reporter 4 | export default class Reporter { 5 | log (logObj, { async } = {}) { 6 | if (logObj.type === 'success' && logObj.args[0].startsWith('Generated ')) { 7 | // Ignore success messages from Nuxt.Generator::generateRoute 8 | return 9 | } else if (logObj.type === 'error' && logObj.args[0].startsWith('Error generating ')) { 10 | // Ignore error messages from Nuxt.Generator::generateRoute 11 | return 12 | } 13 | 14 | if (global._ngc_log_tag) { 15 | logObj.tag = global._ngc_log_tag 16 | } 17 | 18 | messaging.send(null, 'consola', { logObj, stream: { async } }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/reporters/index.js: -------------------------------------------------------------------------------- 1 | import ClusterReporter from './cluster' 2 | 3 | export { 4 | ClusterReporter 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-generate-cluster", 3 | "version": "2.6.1", 4 | "description": "Multi-threaded generate for nuxt using cluster", 5 | "main": "./index.js", 6 | "scripts": { 7 | "build": "rollup -c build/rollup.config.js", 8 | "coverage": "codecov", 9 | "lint": "eslint --ext .js bin/** lib test", 10 | "release": "yarn lint && yarn test && yarn build && standard-version", 11 | "test": "yarn test:fixtures && yarn test:unit && yarn test:cli", 12 | "test:fixtures": "jest test/fixtures", 13 | "test:unit": "jest test/unit --coverage", 14 | "test:cli": "jest test/cli", 15 | "test:clicov": "nyc jest test/cli --coverage --coverageReporters none" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/nuxt-community/nuxt-generate-cluster.git" 20 | }, 21 | "files": [ 22 | "bin", 23 | "lib", 24 | "dist", 25 | "index.js" 26 | ], 27 | "keywords": [ 28 | "nuxt", 29 | "generate", 30 | "multithread", 31 | "cluster" 32 | ], 33 | "bin": { 34 | "nuxt-generate": "./bin/nuxt-generate.js" 35 | }, 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/nuxt-community/nuxt-generate-cluster/issues" 39 | }, 40 | "homepage": "https://github.com/nuxt-community/nuxt-generate-cluster#readme", 41 | "engines": { 42 | "node": ">=8.0.0" 43 | }, 44 | "dependencies": { 45 | "consola": "^2.11.3", 46 | "data-store": "^4.0.3", 47 | "lodash": "^4.17.15", 48 | "meow": "^6.0.0", 49 | "uuid": "^3.4.0" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.8.3", 53 | "@babel/polyfill": "^7.8.3", 54 | "@babel/preset-env": "^7.8.3", 55 | "@nuxtjs/eslint-config": "^2.0.0", 56 | "babel-core": "^7.0.0-bridge.0", 57 | "babel-eslint": "^10.0.3", 58 | "babel-jest": "^24.9.0", 59 | "babel-loader": "^8.0.6", 60 | "babel-plugin-dynamic-import-node": "^2.3.0", 61 | "codecov": "^3.6.1", 62 | "cross-env": "^6.0.3", 63 | "cross-spawn": "^7.0.1", 64 | "eslint": "^6.8.0", 65 | "eslint-config-standard": "^14.1.0", 66 | "eslint-config-standard-jsx": "^8.1.0", 67 | "eslint-plugin-import": "^2.20.0", 68 | "eslint-plugin-jest": "^23.6.0", 69 | "eslint-plugin-node": "^11.0.0", 70 | "eslint-plugin-promise": "^4.2.1", 71 | "eslint-plugin-react": "^7.18.0", 72 | "eslint-plugin-standard": "^4.0.1", 73 | "eslint-plugin-vue": "^6.1.2", 74 | "express": "^4.17.1", 75 | "finalhandler": "^1.1.2", 76 | "get-port": "^5.1.1", 77 | "jest": "^24.9.0", 78 | "jsdom": "^16.0.0", 79 | "klaw-sync": "^6.0.0", 80 | "nuxt": "^2.5.0", 81 | "nyc": "^15.0.0", 82 | "pug": "^2.0.4", 83 | "pug-plain-loader": "^1.0.0", 84 | "request": "^2.88.0", 85 | "request-promise-native": "^1.0.8", 86 | "rimraf": "^3.0.0", 87 | "rollup": "^1.29.0", 88 | "rollup-plugin-commonjs": "^10.1.0", 89 | "rollup-plugin-node-resolve": "^5.2.0", 90 | "standard-version": "^7.0.1" 91 | }, 92 | "peerDependencies": { 93 | "nuxt": "^2.11.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/cli/cli.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { runCliGenerate } from '../utils' 3 | 4 | describe('cli', () => { 5 | test('bin/nuxt-generate', async () => { 6 | const result = await runCliGenerate('basic') 7 | 8 | expect(result.exitCode).toEqual(0) 9 | expect(result.stdout).toContain('Nuxt files generated') 10 | expect(result.stdout).toContain('worker 1 started') 11 | expect(result.stdout).toContain('worker 2 started') 12 | expect(result.stdout).not.toContain('worker 3 started') 13 | expect(result.stdout).toContain('generated: ') 14 | expect(result.stdout).toContain(`${path.sep}users${path.sep}1${path.sep}index.html`) 15 | expect(result.stdout).toContain('worker 1 exited') 16 | expect(result.stdout).toContain('worker 2 exited') 17 | expect(result.stdout).toContain('HTML Files generated in') 18 | expect(result.stderr).toContain('==== Error report ====') 19 | }) 20 | 21 | test('bin/nuxt-generate: no error', async () => { 22 | const result = await runCliGenerate('error-testing', ['--params=error=no-error']) 23 | 24 | expect(result.exitCode).toEqual(0) 25 | expect(result.stdout).toContain(`generated: ${path.sep}no-error${path.sep}index.html`) 26 | expect(result.stdout).toContain('worker 1 started') 27 | expect(result.stdout).not.toContain('worker 2 started') 28 | expect(result.stdout).toContain('worker 1 exited') 29 | expect(result.stdout).toContain('HTML Files generated in') 30 | }) 31 | 32 | test('bin/nuxt-generate: unhandled error', async () => { 33 | const result = await runCliGenerate('error-testing', ['--params=error=unhandled-error']) 34 | 35 | expect(result.exitCode).toEqual(0) 36 | }) 37 | 38 | test('bin/nuxt-generate: unhandled error with --fail-on-page-error', async () => { 39 | const result = await runCliGenerate('error-testing', ['--params=error=unhandled-error', '--fail-on-page-error']) 40 | 41 | expect(result.exitCode).toEqual(1) 42 | expect(result.stderr).toContain('Unhandled page error occured for route /unhandled-error') 43 | }) 44 | 45 | test('bin/nuxt-generate: killed worker', async () => { 46 | const result = await runCliGenerate('error-testing', ['--params=error=kill-process']) 47 | 48 | expect(result.exitCode).toEqual(1) 49 | 50 | // windows doesnt really understand signals 51 | if (process.platform === 'win32') { 52 | expect(result.stderr).toContain('worker 1 exited with status code 1') 53 | } else { 54 | expect(result.stderr).toContain('worker 1 exited by signal SIGTERM') 55 | } 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/fixtures/basic/basic.test.js: -------------------------------------------------------------------------------- 1 | import { buildFixture } from '../../utils/build' 2 | 3 | buildFixture('basic') 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/middleware/-ignored.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | throw new Error('Should be ignored') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/middleware/error.js: -------------------------------------------------------------------------------- 1 | export default function ({ error }) { 2 | error({ message: 'Middleware Error', statusCode: 505 }) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/middleware/ignored.test.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | throw new Error('Should be ignored') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/middleware/meta.js: -------------------------------------------------------------------------------- 1 | export default ({ store, route, redirect }) => { 2 | store.commit('setMeta', route.meta) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/middleware/redirect.js: -------------------------------------------------------------------------------- 1 | export default function ({ redirect }) { 2 | redirect('/') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | let _nuxt 4 | 5 | export default { 6 | render: { 7 | dist: { 8 | maxAge: ((60 * 60 * 24 * 365) * 2) 9 | } 10 | }, 11 | router: { 12 | extendRoutes (routes, resolve) { 13 | return [{ 14 | path: '/before-enter', 15 | name: 'before-enter', 16 | beforeEnter: (to, from, next) => { next('/') } 17 | }, ...routes] 18 | } 19 | }, 20 | generate: { 21 | routes: [ 22 | // TODO: generate with {build: false} does not scans pages! 23 | '/noloading', 24 | '/stateless', 25 | '/css', 26 | '/stateful', 27 | '/head', 28 | '/async-data', 29 | '/validate', 30 | '/redirect', 31 | '/store-module', 32 | '/users/1', 33 | '/users/2', 34 | '/тест雨', 35 | { route: '/users/3', payload: { id: 3000 } } 36 | ], 37 | interval: 200, 38 | subFolders: true 39 | }, 40 | head () { 41 | return { 42 | titleTemplate: (titleChunk) => { 43 | return titleChunk ? `${titleChunk} - Nuxt.js` : 'Nuxt.js' 44 | } 45 | } 46 | }, 47 | modulesDir: path.join(__dirname, '..', '..', '..', 'node_modules'), 48 | hooks: { 49 | ready (nuxt) { 50 | _nuxt = nuxt 51 | nuxt.__hook_ready_called__ = true 52 | }, 53 | build: { 54 | done (builder) { 55 | builder.__hook_built_called__ = true 56 | } 57 | }, 58 | render: { 59 | routeDone (url) { 60 | _nuxt.__hook_render_routeDone__ = url 61 | } 62 | }, 63 | bad: null, 64 | '': true 65 | }, 66 | transition: false, 67 | plugins: [ 68 | '~/plugins/vuex-module', 69 | '~/plugins/dir-plugin', 70 | '~/plugins/inject' 71 | ], 72 | build: { 73 | scopeHoisting: true, 74 | publicPath: '', 75 | postcss: { 76 | preset: { 77 | features: { 78 | 'custom-selectors': true 79 | } 80 | }, 81 | plugins: { 82 | cssnano: {}, 83 | [path.resolve(__dirname, 'plugins', 'tailwind.js')]: {} 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/-ignored.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/async-data.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/await-async-data.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/callback-async-data.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/config.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/css.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/error-midd.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/error-object.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/error-string.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/error.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/error2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/extractCSS.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/fn-midd.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/head.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/ignored.test.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/js-link.js: -------------------------------------------------------------------------------- 1 | export default { 2 | } 3 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/js-link.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/jsx-link.js: -------------------------------------------------------------------------------- 1 | import renderLink from './jsx-link.jsx' 2 | 3 | export default { 4 | render: renderLink 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/jsx-link.jsx: -------------------------------------------------------------------------------- 1 | export default function (h) { 2 | return (
3 |

JSX Link Page

4 |
) 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/jsx.js: -------------------------------------------------------------------------------- 1 | export default { 2 | render () { 3 | return
4 |

JSX Page

5 |
6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/meta.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/no-ssr.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/noloading.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/pug.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/redirect-external.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/redirect-middleware.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/redirect-name.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/redirect.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/router-guard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/special-state.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/stateful.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/stateless.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/store-module.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/store.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/style.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/users/_id.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/validate-async.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/validate.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /test/fixtures/basic/pages/тест雨.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/plugins/dir-plugin/index.js: -------------------------------------------------------------------------------- 1 | if (process.client) { 2 | window.__test_plugin = true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/plugins/inject.js: -------------------------------------------------------------------------------- 1 | export default ({ route, params }, inject) => { 2 | const { injectValue } = route.query 3 | if (typeof injectValue === 'undefined') { 4 | return 5 | } 6 | const key = 'injectedProperty' 7 | const map = { 8 | undefined, 9 | null: null, 10 | false: false, 11 | 0: 0, 12 | empty: '' 13 | } 14 | const value = map[injectValue] 15 | inject(key, value) 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/basic/plugins/tailwind.js: -------------------------------------------------------------------------------- 1 | 2 | const postcss = require('postcss') 3 | 4 | module.exports = postcss.plugin('nuxt-test', () => { 5 | return function () {} 6 | }) 7 | -------------------------------------------------------------------------------- /test/fixtures/basic/plugins/vuex-module.js: -------------------------------------------------------------------------------- 1 | export default function ({ store }) { 2 | store.registerModule('simpleModule', { 3 | namespaced: true, 4 | state: () => ({ 5 | mutateMe: 'not mutated' 6 | }), 7 | actions: { 8 | mutate ({ commit }) { 9 | commit('mutate') 10 | } 11 | }, 12 | mutations: { 13 | mutate (state) { 14 | state.mutateMe = 'mutated' 15 | } 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/basic/static/body.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | console.log('Body script!') 3 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/-ignored.js: -------------------------------------------------------------------------------- 1 | throw new Error('This file should be ignored!!') 2 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/bab/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | babVal: 10 3 | }) 4 | 5 | export const getters = { 6 | getBabVal (state) { 7 | return state.babVal 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/foo/bar.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | baz: 'Vuex Nested Modules' 3 | }) 4 | 5 | export const getters = { 6 | baz (state) { 7 | return state.baz 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/foo/blarg.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | val: 1 3 | }) 4 | 5 | export const getters = { 6 | getVal (state) { 7 | return 100 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/foo/blarg/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getVal (state) { 3 | return state.val 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/foo/blarg/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | val: 2 3 | }) 4 | 5 | export const getters = { 6 | getVal (state) { 7 | return 99 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/foo/blarg/state.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | val: 4 3 | }) 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/ignored.test.js: -------------------------------------------------------------------------------- 1 | throw new Error('This file should be ignored!!') 2 | -------------------------------------------------------------------------------- /test/fixtures/basic/store/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | counter: 1, 3 | meta: [] 4 | }) 5 | 6 | export const mutations = { 7 | increment (state) { 8 | state.counter++ 9 | }, 10 | setMeta (state, meta) { 11 | state.meta = meta 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/error-testing/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default { 4 | modulesDir: path.join(__dirname, '..', '..', '..', 'node_modules'), 5 | 6 | generate: { 7 | routes (callback, params) { 8 | const routes = [ 9 | `/${params.error}` 10 | ] 11 | callback(null, routes) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/error-testing/pages/_error.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /test/unit/async.generate.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs' 2 | import http from 'http' 3 | import { resolve } from 'path' 4 | import { remove } from 'fs-extra' 5 | import serveStatic from 'serve-static' 6 | import finalhandler from 'finalhandler' 7 | import { Async, getPort, loadFixture, rp, listPaths, equalOrStartsWith, waitUntil } from '../utils' 8 | 9 | let port 10 | const url = route => 'http://localhost:' + port + route 11 | const rootDir = resolve(__dirname, '..', 'fixtures/basic') 12 | const distDir = resolve(rootDir, '.nuxt-async') 13 | 14 | let builder 15 | let server = null 16 | let generator = null 17 | let pathsBefore 18 | let changedFileName 19 | 20 | describe('async generate', () => { 21 | beforeAll(async () => { 22 | const config = await loadFixture('basic', { generate: { dir: distDir } }) 23 | 24 | const master = new Async.Master(config, { 25 | workerCount: 2 26 | }) 27 | 28 | await master.init() 29 | generator = master.generator 30 | builder = generator.builder 31 | 32 | builder.build = jest.fn() 33 | const nuxt = generator.nuxt 34 | 35 | pathsBefore = listPaths(nuxt.options.rootDir) 36 | 37 | let ready = false 38 | // Make sure our check for changed files is really working 39 | changedFileName = resolve(nuxt.options.generate.dir, '..', '.nuxt-async-changed') 40 | master.hook('done', (info) => { 41 | writeFileSync(changedFileName, '') 42 | expect(info.errors.length).toBe(1) 43 | ready = true 44 | }) 45 | 46 | await master.run({ build: true }) 47 | await waitUntil(() => ready) 48 | 49 | const serve = serveStatic(distDir) 50 | server = http.createServer((req, res) => { 51 | serve(req, res, finalhandler(req, res)) 52 | }) 53 | 54 | port = await getPort() 55 | server.listen(port) 56 | }) 57 | 58 | // Close server and ask nuxt to stop listening to file changes 59 | afterAll(async () => { 60 | await server.close() 61 | }) 62 | 63 | test('Check builder', () => { 64 | expect(builder.bundleBuilder.buildContext.isStatic).toBe(true) 65 | expect(builder.build).toHaveBeenCalledTimes(1) 66 | }) 67 | 68 | test('Check ready hook called', () => { 69 | expect(generator.nuxt.__hook_ready_called__).toBe(true) 70 | }) 71 | 72 | test('Check changed files', () => { 73 | // When generating Nuxt we only expect files to change 74 | // within nuxt.options.generate.dir, but also allow other 75 | // .nuxt dirs for when tests are runInBand 76 | const allowChangesDir = resolve(generator.nuxt.options.generate.dir, '..', '.nuxt') 77 | 78 | let changedFileFound = false 79 | const paths = listPaths(generator.nuxt.options.rootDir, pathsBefore) 80 | paths.forEach((item) => { 81 | if (item.path === changedFileName) { 82 | changedFileFound = true 83 | } else { 84 | expect(equalOrStartsWith(allowChangesDir, item.path)).toBe(true) 85 | } 86 | }) 87 | expect(changedFileFound).toBe(true) 88 | }) 89 | 90 | test('Format errors', () => { 91 | const error = generator._formatErrors([ 92 | { type: 'handled', route: '/h1', error: 'page not found' }, 93 | { type: 'unhandled', route: '/h2', error: { stack: 'unhandled error stack' } } 94 | ]) 95 | expect(error).toMatch(' /h1') 96 | expect(error).toMatch(' /h2') 97 | expect(error).toMatch('"page not found"') 98 | expect(error).toMatch('unhandled error stack') 99 | }) 100 | 101 | test('/stateless', async () => { 102 | const window = await generator.nuxt.server.renderAndGetWindow(url('/stateless')) 103 | const html = window.document.body.innerHTML 104 | expect(html).toContain('

My component!

') 105 | }) 106 | 107 | test('/store-module', async () => { 108 | const window = await generator.nuxt.server.renderAndGetWindow(url('/store-module')) 109 | const html = window.document.body.innerHTML 110 | expect(html).toContain('

mutated

') 111 | }) 112 | 113 | test('/css', async () => { 114 | const window = await generator.nuxt.server.renderAndGetWindow(url('/css')) 115 | 116 | const headHtml = window.document.head.innerHTML 117 | expect(headHtml).toContain('.red{color:red') 118 | 119 | const element = window.document.querySelector('.red') 120 | expect(element).not.toBe(null) 121 | expect(element.textContent).toBe('This is red') 122 | expect(element.className).toBe('red') 123 | // t.is(window.getComputedStyle(element), 'red') 124 | }) 125 | 126 | test('/stateful', async () => { 127 | const window = await generator.nuxt.server.renderAndGetWindow(url('/stateful')) 128 | const html = window.document.body.innerHTML 129 | expect(html).toContain('

The answer is 42

') 130 | }) 131 | 132 | test('/head', async () => { 133 | const window = await generator.nuxt.server.renderAndGetWindow(url('/head')) 134 | const html = window.document.body.innerHTML 135 | const metas = window.document.getElementsByTagName('meta') 136 | expect(window.document.title).toBe('My title - Nuxt.js') 137 | expect(metas[0].getAttribute('content')).toBe('my meta') 138 | expect(html).toContain('

I can haz meta tags

') 139 | }) 140 | 141 | test('/async-data', async () => { 142 | const window = await generator.nuxt.server.renderAndGetWindow(url('/async-data')) 143 | const html = window.document.body.innerHTML 144 | expect(html).toContain('

Nuxt.js

') 145 | }) 146 | 147 | test('/тест雨 (test non ascii route)', async () => { 148 | const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨')) 149 | const html = window.document.body.innerHTML 150 | expect(html).toContain('Hello unicode') 151 | }) 152 | 153 | test('/users/1/index.html', async () => { 154 | const html = await rp(url('/users/1/index.html')) 155 | expect(html).toContain('

User: 1

') 156 | expect( 157 | existsSync(resolve(distDir, 'users/1/index.html')) 158 | ).toBe(true) 159 | expect(existsSync(resolve(distDir, 'users/1.html'))).toBe(false) 160 | }) 161 | 162 | test('/users/2', async () => { 163 | const html = await rp(url('/users/2')) 164 | expect(html).toContain('

User: 2

') 165 | }) 166 | 167 | test('/users/3 (payload given)', async () => { 168 | const html = await rp(url('/users/3')) 169 | expect(html).toContain('

User: 3000

') 170 | }) 171 | 172 | test('/users/4 -> Not found', async () => { 173 | await expect(rp(url('/users/4'))).rejects.toMatchObject({ 174 | statusCode: 404, 175 | response: { 176 | body: expect.stringContaining('Cannot GET /users/4') 177 | } 178 | }) 179 | }) 180 | 181 | test('/validate should not be server-rendered', async () => { 182 | const html = await rp(url('/validate')) 183 | expect(html).toContain('
') 184 | expect(html).toContain('serverRendered:!1') 185 | }) 186 | 187 | test('/validate -> should display a 404', async () => { 188 | const window = await generator.nuxt.server.renderAndGetWindow(url('/validate')) 189 | const html = window.document.body.innerHTML 190 | expect(html).toContain('This page could not be found') 191 | }) 192 | 193 | test('/validate?valid=true', async () => { 194 | const window = await generator.nuxt.server.renderAndGetWindow(url('/validate?valid=true')) 195 | const html = window.document.body.innerHTML 196 | expect(html).toContain('I am valid') 197 | }) 198 | 199 | test('/redirect should not be server-rendered', async () => { 200 | const html = await rp(url('/redirect')) 201 | expect(html).toContain('
') 202 | expect(html).toContain('serverRendered:!1') 203 | }) 204 | 205 | test('/redirect -> check redirected source', async () => { 206 | const window = await generator.nuxt.server.renderAndGetWindow(url('/redirect')) 207 | const html = window.document.body.innerHTML 208 | expect(html).toContain('

Index page

') 209 | }) 210 | 211 | test('/users/1 not found', async () => { 212 | await remove(resolve(distDir, 'users')) 213 | await expect(rp(url('/users/1'))).rejects.toMatchObject({ 214 | statusCode: 404, 215 | response: { 216 | body: expect.stringContaining('Cannot GET /users/1') 217 | } 218 | }) 219 | }) 220 | 221 | test('nuxt re-generating with no subfolders', async () => { 222 | generator.nuxt.options.generate.subFolders = false 223 | await expect(generator.generate({ build: false })).resolves.toBeTruthy() 224 | }) 225 | 226 | test('/users/1.html', async () => { 227 | const html = await rp(url('/users/1.html')) 228 | expect(html).toContain('

User: 1

') 229 | expect(existsSync(resolve(distDir, 'users/1.html'))).toBe(true) 230 | expect( 231 | existsSync(resolve(distDir, 'users/1/index.html')) 232 | ).toBe(false) 233 | }) 234 | 235 | test('/-ignored', async () => { 236 | await expect(rp(url('/-ignored'))).rejects.toMatchObject({ 237 | statusCode: 404, 238 | response: { 239 | body: expect.stringContaining('Cannot GET /-ignored') 240 | } 241 | }) 242 | }) 243 | 244 | test('/ignored.test', async () => { 245 | await expect(rp(url('/ignored.test'))).rejects.toMatchObject({ 246 | statusCode: 404, 247 | response: { 248 | body: expect.stringContaining('Cannot GET /ignored.test') 249 | } 250 | }) 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /test/unit/async.messaging.test.js: -------------------------------------------------------------------------------- 1 | import { Async, Generate, Mixins, consola } from '../utils' 2 | 3 | jest.mock('../../lib/utils/consola') 4 | 5 | class Messenger extends Async.Mixins.Messaging(Mixins.Hookable()) {} 6 | 7 | describe('async messaging', () => { 8 | afterEach(() => { 9 | jest.clearAllMocks() 10 | }) 11 | 12 | test('Can send/receive', () => { 13 | const sender = new Messenger() 14 | Messenger.workers = [] 15 | sender.startListeningForMessages() 16 | const receiver = new Messenger() 17 | receiver.startListeningForMessages() 18 | 19 | const payload = { a: 1 } 20 | 21 | receiver.hook(Generate.Commands.sendRoutes, (args) => { 22 | expect(args).toBe(payload) 23 | }) 24 | 25 | sender.sendCommand(receiver, Generate.Commands.sendRoutes, payload) 26 | }) 27 | 28 | test('Send unknown command fails', () => { 29 | const sender = new Messenger() 30 | 31 | sender.sendCommand('unknown-command') 32 | expect(consola.error).toHaveBeenCalledTimes(1) 33 | }) 34 | 35 | test('Receive unknown command fails', () => { 36 | const receiver = new Messenger() 37 | 38 | receiver.receiveCommand(undefined, { cmd: 'unknown-command' }) 39 | expect(consola.error).toHaveBeenCalledTimes(1) 40 | }) 41 | 42 | test('Receive command without plugins fails', () => { 43 | const receiver = new Messenger() 44 | 45 | receiver.receiveCommand(undefined, { cmd: Generate.Commands.sendRoutes }) 46 | expect(consola.error).toHaveBeenCalledTimes(1) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/unit/cluster.generate.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs' 2 | import http from 'http' 3 | import { resolve } from 'path' 4 | import { remove } from 'fs-extra' 5 | import serveStatic from 'serve-static' 6 | import finalhandler from 'finalhandler' 7 | import { Cluster, getPort, loadFixture, rp, listPaths, equalOrStartsWith, waitUntil } from '../utils' 8 | 9 | let port 10 | const url = route => 'http://localhost:' + port + route 11 | const rootDir = resolve(__dirname, '..', 'fixtures/basic') 12 | const distDir = resolve(rootDir, '.nuxt-cluster') 13 | 14 | let builder 15 | let server = null 16 | let generator = null 17 | let pathsBefore 18 | let changedFileName 19 | 20 | describe('cluster generate', () => { 21 | beforeAll(async () => { 22 | const config = await loadFixture('basic', { generate: { dir: distDir } }) 23 | 24 | const master = new Cluster.Master(config, { 25 | adjustLogLevel: -6, 26 | workerCount: 2, 27 | setup: { 28 | exec: resolve(__dirname, '..', 'utils', 'cluster.worker.js') 29 | } 30 | }) 31 | await master.init() 32 | 33 | generator = master.generator 34 | builder = generator.builder 35 | 36 | builder.build = jest.fn() 37 | const nuxt = generator.nuxt 38 | 39 | pathsBefore = listPaths(nuxt.options.rootDir) 40 | 41 | let ready = false 42 | // Make sure our check for changed files is really working 43 | changedFileName = resolve(nuxt.options.generate.dir, '..', '.nuxt-cluster-changed') 44 | master.hook('done', (info) => { 45 | writeFileSync(changedFileName, '') 46 | expect(info.errors.length).toBe(1) 47 | ready = true 48 | }) 49 | 50 | await master.run({ build: true }) 51 | await waitUntil(() => ready) 52 | 53 | const serve = serveStatic(distDir) 54 | server = http.createServer((req, res) => { 55 | serve(req, res, finalhandler(req, res)) 56 | }) 57 | 58 | port = await getPort() 59 | server.listen(port) 60 | }) 61 | 62 | test('Check builder', () => { 63 | expect(builder.bundleBuilder.buildContext.isStatic).toBe(true) 64 | expect(builder.build).toHaveBeenCalledTimes(1) 65 | }) 66 | 67 | test('Check ready hook called', () => { 68 | expect(generator.nuxt.__hook_ready_called__).toBe(true) 69 | }) 70 | 71 | test('Check changed files', () => { 72 | // When generating Nuxt we only expect files to change 73 | // within nuxt.options.generate.dir, but also allow other 74 | // .nuxt dirs for when tests are runInBand 75 | const allowChangesDir = resolve(generator.nuxt.options.generate.dir, '..', '.nuxt') 76 | 77 | let changedFileFound = false 78 | const paths = listPaths(generator.nuxt.options.rootDir, pathsBefore) 79 | paths.forEach((item) => { 80 | if (item.path === changedFileName) { 81 | changedFileFound = true 82 | } else { 83 | expect(equalOrStartsWith(allowChangesDir, item.path)).toBe(true) 84 | } 85 | }) 86 | expect(changedFileFound).toBe(true) 87 | }) 88 | 89 | test('Format errors', () => { 90 | const error = generator._formatErrors([ 91 | { type: 'handled', route: '/h1', error: 'page not found' }, 92 | { type: 'unhandled', route: '/h2', error: { stack: 'unhandled error stack' } } 93 | ]) 94 | expect(error).toMatch(' /h1') 95 | expect(error).toMatch(' /h2') 96 | expect(error).toMatch('"page not found"') 97 | expect(error).toMatch('unhandled error stack') 98 | }) 99 | 100 | test('/stateless', async () => { 101 | const window = await generator.nuxt.server.renderAndGetWindow(url('/stateless')) 102 | const html = window.document.body.innerHTML 103 | expect(html).toContain('

My component!

') 104 | }) 105 | 106 | test('/store-module', async () => { 107 | const window = await generator.nuxt.server.renderAndGetWindow(url('/store-module')) 108 | const html = window.document.body.innerHTML 109 | expect(html).toContain('

mutated

') 110 | }) 111 | 112 | test('/css', async () => { 113 | const window = await generator.nuxt.server.renderAndGetWindow(url('/css')) 114 | 115 | const headHtml = window.document.head.innerHTML 116 | expect(headHtml).toContain('.red{color:red') 117 | 118 | const element = window.document.querySelector('.red') 119 | expect(element).not.toBe(null) 120 | expect(element.textContent).toBe('This is red') 121 | expect(element.className).toBe('red') 122 | // t.is(window.getComputedStyle(element), 'red') 123 | }) 124 | 125 | test('/stateful', async () => { 126 | const window = await generator.nuxt.server.renderAndGetWindow(url('/stateful')) 127 | const html = window.document.body.innerHTML 128 | expect(html).toContain('

The answer is 42

') 129 | }) 130 | 131 | test('/head', async () => { 132 | const window = await generator.nuxt.server.renderAndGetWindow(url('/head')) 133 | const html = window.document.body.innerHTML 134 | const metas = window.document.getElementsByTagName('meta') 135 | expect(window.document.title).toBe('My title - Nuxt.js') 136 | expect(metas[0].getAttribute('content')).toBe('my meta') 137 | expect(html).toContain('

I can haz meta tags

') 138 | }) 139 | 140 | test('/async-data', async () => { 141 | const window = await generator.nuxt.server.renderAndGetWindow(url('/async-data')) 142 | const html = window.document.body.innerHTML 143 | expect(html).toContain('

Nuxt.js

') 144 | }) 145 | 146 | test('/тест雨 (test non ascii route)', async () => { 147 | const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨')) 148 | const html = window.document.body.innerHTML 149 | expect(html).toContain('Hello unicode') 150 | }) 151 | 152 | test('/users/1/index.html', async () => { 153 | const html = await rp(url('/users/1/index.html')) 154 | expect(html).toContain('

User: 1

') 155 | expect( 156 | existsSync(resolve(distDir, 'users/1/index.html')) 157 | ).toBe(true) 158 | expect(existsSync(resolve(distDir, 'users/1.html'))).toBe(false) 159 | }) 160 | 161 | test('/users/2', async () => { 162 | const html = await rp(url('/users/2')) 163 | expect(html).toContain('

User: 2

') 164 | }) 165 | 166 | test('/users/3 (payload given)', async () => { 167 | const html = await rp(url('/users/3')) 168 | expect(html).toContain('

User: 3000

') 169 | }) 170 | 171 | test('/users/4 -> Not found', async () => { 172 | await expect(rp(url('/users/4'))).rejects.toMatchObject({ 173 | statusCode: 404, 174 | response: { 175 | body: expect.stringContaining('Cannot GET /users/4') 176 | } 177 | }) 178 | }) 179 | 180 | test('/validate should not be server-rendered', async () => { 181 | const html = await rp(url('/validate')) 182 | expect(html).toContain('
') 183 | expect(html).toContain('serverRendered:!1') 184 | }) 185 | 186 | test('/validate -> should display a 404', async () => { 187 | const window = await generator.nuxt.server.renderAndGetWindow(url('/validate')) 188 | const html = window.document.body.innerHTML 189 | expect(html).toContain('This page could not be found') 190 | }) 191 | 192 | test('/validate?valid=true', async () => { 193 | const window = await generator.nuxt.server.renderAndGetWindow(url('/validate?valid=true')) 194 | const html = window.document.body.innerHTML 195 | expect(html).toContain('I am valid') 196 | }) 197 | 198 | test('/redirect should not be server-rendered', async () => { 199 | const html = await rp(url('/redirect')) 200 | expect(html).toContain('
') 201 | expect(html).toContain('serverRendered:!1') 202 | }) 203 | 204 | test('/redirect -> check redirected source', async () => { 205 | const window = await generator.nuxt.server.renderAndGetWindow(url('/redirect')) 206 | const html = window.document.body.innerHTML 207 | expect(html).toContain('

Index page

') 208 | }) 209 | 210 | test('/users/1 not found', async () => { 211 | await remove(resolve(distDir, 'users')) 212 | await expect(rp(url('/users/1'))).rejects.toMatchObject({ 213 | statusCode: 404, 214 | response: { 215 | body: expect.stringContaining('Cannot GET /users/1') 216 | } 217 | }) 218 | }) 219 | 220 | test('nuxt re-generating with no subfolders', async () => { 221 | generator.nuxt.options.generate.subFolders = false 222 | await expect(generator.generate({ build: false })).resolves.toBeTruthy() 223 | }) 224 | 225 | test('/users/1.html', async () => { 226 | const html = await rp(url('/users/1.html')) 227 | expect(html).toContain('

User: 1

') 228 | expect(existsSync(resolve(distDir, 'users/1.html'))).toBe(true) 229 | expect( 230 | existsSync(resolve(distDir, 'users/1/index.html')) 231 | ).toBe(false) 232 | }) 233 | 234 | test('/-ignored', async () => { 235 | await expect(rp(url('/-ignored'))).rejects.toMatchObject({ 236 | statusCode: 404, 237 | response: { 238 | body: expect.stringContaining('Cannot GET /-ignored') 239 | } 240 | }) 241 | }) 242 | 243 | test('/ignored.test', async () => { 244 | await expect(rp(url('/ignored.test'))).rejects.toMatchObject({ 245 | statusCode: 404, 246 | response: { 247 | body: expect.stringContaining('Cannot GET /ignored.test') 248 | } 249 | }) 250 | }) 251 | 252 | // Close server and ask nuxt to stop listening to file changes 253 | afterAll(async () => { 254 | await server.close() 255 | }) 256 | }) 257 | -------------------------------------------------------------------------------- /test/unit/cluster.master.test.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import cluster from 'cluster' 3 | import { Cluster, loadFixture, consola } from '../utils' 4 | 5 | jest.mock('cluster') 6 | jest.mock('../../lib/utils/consola') 7 | 8 | describe('cluster master', () => { 9 | let master 10 | const rootDir = resolve(__dirname, '..', 'fixtures/basic') 11 | const distDir = resolve(rootDir, '.nuxt-master') 12 | let config 13 | 14 | beforeAll(async () => { 15 | config = await loadFixture('basic', { generate: { dir: distDir } }) 16 | 17 | master = new Cluster.Master(config) 18 | await master.run() 19 | }) 20 | 21 | test('logs exit code', async () => { 22 | master.done = jest.fn() 23 | await master.onExit({ id: 123 }, 456, 789) 24 | expect(consola.fatal).toHaveBeenCalledWith(expect.stringMatching('worker 123 exited with status code 456 by signal 789')) 25 | }) 26 | 27 | test('counts alive workers', async () => { 28 | cluster.workers = [ 29 | [], // filler 30 | { id: 1, isConnected: () => true } 31 | ] 32 | 33 | expect(cluster.workers.length).toBe(2) 34 | 35 | master.watchdog.addWorker(1, { pid: 123 }) 36 | master.watchdog.addWorker(2, { pid: 456 }) 37 | 38 | const alive = await master.watchdog.countAlive() 39 | expect(alive).toBe(1) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/unit/cluster.worker.test.js: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import cluster from 'cluster' 3 | import { Cluster, loadFixture, consola } from '../utils' 4 | 5 | jest.mock('cluster') 6 | jest.mock('../../lib/utils/consola') 7 | 8 | describe('cluster worker', () => { 9 | let worker 10 | const rootDir = resolve(__dirname, '..', 'fixtures/basic') 11 | const distDir = resolve(rootDir, '.nuxt-worker') 12 | let config 13 | 14 | beforeAll(async () => { 15 | config = await loadFixture('basic', { generate: { dir: distDir } }) 16 | 17 | worker = new Cluster.Worker(config) 18 | await worker.run() 19 | }) 20 | 21 | beforeEach(() => { 22 | jest.resetAllMocks() 23 | }) 24 | 25 | test('static start processes env and runs worker', () => { 26 | const options = { 27 | test: '123' 28 | } 29 | process.env.args = JSON.stringify({ options }) 30 | 31 | const workerRun = Cluster.Worker.prototype.run 32 | 33 | const run = jest.fn() 34 | Cluster.Worker.prototype.run = run 35 | 36 | const worker = Cluster.Worker.start() 37 | 38 | expect(run).toHaveBeenCalledTimes(1) 39 | expect(worker.options.test).toBe(options.test) 40 | 41 | Cluster.Worker.prototype.run = workerRun 42 | }) 43 | 44 | test('can generate routes', async () => { 45 | let routes = worker.generator.nuxt.options.generate.routes 46 | routes = worker.generator.decorateWithPayloads([], routes) 47 | 48 | const routesLength = routes.length 49 | const spy = jest.fn() 50 | worker.generator.nuxt.hook('generate:routeCreated', spy) 51 | await worker.generateRoutes(routes) 52 | expect(consola.cluster).toHaveBeenCalledWith(`received ${routesLength} routes`) 53 | expect(consola.success).toHaveBeenCalledTimes(routesLength - consola.error.mock.calls.length) 54 | }) 55 | 56 | test('calculates duration on level >4', async () => { 57 | consola.level = 4 58 | jest.unmock('consola') 59 | 60 | const ccluster = jest.fn() 61 | const cworker = jest.fn() 62 | consola.cluster = ccluster 63 | consola.success = cworker 64 | 65 | let routes = worker.generator.nuxt.options.generate.routes 66 | routes = worker.generator.decorateWithPayloads([], routes) 67 | 68 | const routesLength = routes.length 69 | const spy = jest.fn() 70 | worker.generator.nuxt.hook('generate:routeCreated', spy) 71 | await worker.generateRoutes(routes) 72 | 73 | expect(ccluster).toHaveBeenCalledWith(`received ${routesLength} routes`) 74 | expect(cworker).toHaveBeenCalledTimes(routesLength - consola.error.mock.calls.length) 75 | const esep = sep.replace('\\', '\\\\') 76 | const reg = 'generated: ' + esep + 'users' + esep + '1' + esep + 'index.html \\([0-9]+ms\\)' 77 | expect(cworker).toHaveBeenCalledWith(expect.stringMatching(new RegExp(reg))) 78 | }) 79 | 80 | test('sets id based on cluster id', () => { 81 | cluster.isWorker = true 82 | cluster.worker = { id: 999 } 83 | 84 | worker = new Cluster.Worker(config) 85 | 86 | expect(worker.id).toBe(999) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/unit/consola.test.js: -------------------------------------------------------------------------------- 1 | import consolaDefault from 'consola' 2 | import { consola } from '../utils' 3 | 4 | jest.mock('std-env', () => { 5 | return { 6 | ci: false, 7 | test: false 8 | } 9 | }) 10 | 11 | describe('consola', () => { 12 | test('extends default', () => { 13 | Object.keys(consolaDefault).forEach((key) => { 14 | // our consola should have all properties 15 | // of default consola except 16 | // the Class exports and _internal props 17 | if (!key.match(/^[A-Z_]/)) { 18 | expect(consola[key]).not.toBeUndefined() 19 | } 20 | }) 21 | }) 22 | 23 | test('custom props should exists', () => { 24 | ['cluster', 'master'].forEach((key) => { 25 | expect(consola[key]).toBeDefined() 26 | expect(consolaDefault[key]).toBeUndefined() 27 | }) 28 | }) 29 | 30 | // doesnt work 31 | test('uses fancy reporter by default', () => { 32 | expect(consola._reporters[0].constructor.name).toBe('BasicReporter') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/unit/generate.watchdog.test.js: -------------------------------------------------------------------------------- 1 | import { waitFor, Generate, consola } from '../utils' 2 | 3 | jest.mock('../../lib/utils/consola') 4 | 5 | describe('watchdog', () => { 6 | afterEach(() => { 7 | jest.clearAllMocks() 8 | }) 9 | 10 | test('Count alive workers', async () => { 11 | const watchdog = new Generate.Watchdog() 12 | watchdog.hook('isWorkerAlive', (worker) => { 13 | return worker.id === 1 14 | }) 15 | 16 | watchdog.addWorker(1) 17 | watchdog.addWorker(2) 18 | 19 | expect(await watchdog.countAlive()).toBe(1) 20 | }) 21 | 22 | test('Count dead workers', async () => { 23 | const watchdog = new Generate.Watchdog() 24 | watchdog.hook('isWorkerAlive', (worker) => { 25 | return worker.id === 1 26 | }) 27 | 28 | watchdog.addWorker(1) 29 | watchdog.addWorker(2) 30 | await waitFor(1) 31 | 32 | watchdog.exitWorker(1) 33 | expect(await watchdog.allDead()).toBe(false) 34 | 35 | watchdog.exitWorker(2) 36 | expect(await watchdog.allDead()).toBe(true) 37 | }) 38 | 39 | test('Error message on adding same id', () => { 40 | const watchdog = new Generate.Watchdog() 41 | watchdog.addWorker(1) 42 | watchdog.addWorker(1) 43 | expect(consola.error).toHaveBeenCalledTimes(1) 44 | }) 45 | 46 | test('Can add info', () => { 47 | const watchdog = new Generate.Watchdog() 48 | watchdog.addWorker(1) 49 | 50 | expect(watchdog.workers[1].routes).toBe(0) 51 | watchdog.addInfo(1, { routes: 2 }) 52 | expect(watchdog.workers[1].routes).toBe(2) 53 | watchdog.addInfo(1, { routes: 3 }) 54 | expect(watchdog.workers[1].routes).toBe(3) 55 | }) 56 | 57 | test('Can add info by key', () => { 58 | const watchdog = new Generate.Watchdog() 59 | watchdog.addWorker(1) 60 | 61 | expect(watchdog.workers[1].routes).toBe(0) 62 | watchdog.addInfo(1, 'routes', 2) 63 | expect(watchdog.workers[1].routes).toBe(2) 64 | watchdog.addInfo(1, 'routes', 3) 65 | expect(watchdog.workers[1].routes).toBe(3) 66 | }) 67 | 68 | test('Cannot append to unknown key', () => { 69 | const watchdog = new Generate.Watchdog() 70 | watchdog.addWorker(1) 71 | watchdog.appendInfo(1, 'unknown-key', true) 72 | expect(consola.error).toHaveBeenCalledTimes(1) 73 | }) 74 | 75 | test('Can append string', () => { 76 | const watchdog = new Generate.Watchdog() 77 | watchdog.addWorker(1, { str: '' }) 78 | 79 | expect(watchdog.workers[1].str).toBe('') 80 | watchdog.appendInfo(1, 'str', 'a') 81 | expect(watchdog.workers[1].str).toBe('a') 82 | watchdog.appendInfo(1, 'str', 'b') 83 | expect(watchdog.workers[1].str).toBe('ab') 84 | }) 85 | 86 | test('Can append number', () => { 87 | const watchdog = new Generate.Watchdog() 88 | watchdog.addWorker(1, { num: 0 }) 89 | 90 | watchdog.appendInfo(1, 'num', 1) 91 | expect(watchdog.workers[1].num).toBe(1) 92 | watchdog.appendInfo(1, 'num', 1) 93 | expect(watchdog.workers[1].num).toBe(2) 94 | }) 95 | 96 | test('Can append array', () => { 97 | const watchdog = new Generate.Watchdog() 98 | watchdog.addWorker(1, { arr: [] }) 99 | 100 | expect(watchdog.workers[1].arr.length).toBe(0) 101 | watchdog.appendInfo(1, 'arr', [1]) 102 | expect(watchdog.workers[1].arr.length).toBe(1) 103 | watchdog.appendInfo(1, 'arr', [2]) 104 | expect(watchdog.workers[1].arr.length).toBe(2) 105 | }) 106 | 107 | test('Can append object', () => { 108 | const watchdog = new Generate.Watchdog() 109 | watchdog.addWorker(1, { obj: {} }) 110 | 111 | expect(watchdog.workers[1].obj).toEqual({}) 112 | watchdog.appendInfo(1, 'obj', { a: 1 }) 113 | expect(watchdog.workers[1].obj.a).toBe(1) 114 | watchdog.appendInfo(1, 'obj', { a: 2, b: 1 }) 115 | expect(watchdog.workers[1].obj.a).toBe(2) 116 | expect(watchdog.workers[1].obj.b).toBe(1) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /test/unit/messaging.test.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster' 2 | import consola from 'consola' 3 | import { MessageBroker } from '../utils' 4 | 5 | const masterConf = { isMaster: true, alias: 'm' } 6 | const childConf = { isMaster: false, alias: 'c' } 7 | 8 | let mb 9 | let cb 10 | 11 | jest.mock('cluster') 12 | jest.mock('consola') 13 | 14 | describe('messaging', () => { 15 | beforeEach(() => { 16 | mb = new MessageBroker(masterConf) 17 | cb = new MessageBroker(childConf, mb) 18 | }) 19 | 20 | afterEach(() => { 21 | mb.close() 22 | cb.close() 23 | jest.clearAllMocks() 24 | }) 25 | 26 | test('child can register with master', () => { 27 | mb.registerProxy = jest.fn() 28 | cb = new MessageBroker(childConf, mb) 29 | 30 | expect(mb.registerProxy).toHaveBeenCalledTimes(1) 31 | }) 32 | 33 | test('error on send to unknown', () => { 34 | mb.send('SOME ID', 'test') 35 | 36 | expect(consola.error).toHaveBeenCalledTimes(1) 37 | expect(consola.error.mock.calls[0][0]).toMatch(/Unable to send message/) 38 | }) 39 | 40 | test('warns on unknown proxy', () => { 41 | cb.send('SOME ID', 'test') 42 | 43 | expect(consola.warn).toHaveBeenCalledTimes(1) 44 | expect(consola.warn.mock.calls[0][0]).toMatch(/Proxy SOME ID not registered/) 45 | expect(consola.error).not.toHaveBeenCalled() 46 | }) 47 | 48 | test('can send to self', () => { 49 | mb.callService = jest.fn() 50 | 51 | mb.send(mb.id, 'test') 52 | expect(consola.error).not.toHaveBeenCalled() 53 | expect(mb.callService).toHaveBeenCalledTimes(1) 54 | 55 | mb.send(mb.alias, 'test') 56 | expect(consola.error).not.toHaveBeenCalled() 57 | expect(mb.callService).toHaveBeenCalledTimes(2) 58 | }) 59 | 60 | test('can send from master to child', () => { 61 | cb.callService = jest.fn() 62 | 63 | mb.send(cb.id, 'test') 64 | 65 | expect(consola.error).not.toHaveBeenCalled() 66 | expect(cb.callService).toHaveBeenCalledTimes(1) 67 | }) 68 | 69 | test('can send from child to master', () => { 70 | mb.callService = jest.fn() 71 | 72 | cb.send(mb.id, 'test') 73 | 74 | expect(consola.error).not.toHaveBeenCalled() 75 | expect(mb.callService).toHaveBeenCalledTimes(1) 76 | }) 77 | 78 | test('can send from child to child', () => { 79 | const cb2 = new MessageBroker(childConf, mb) 80 | cb2.alias = 'c2' 81 | cb2.callService = jest.fn() 82 | 83 | cb.send(cb2.id, 'test') 84 | 85 | expect(consola.error).not.toHaveBeenCalled() 86 | expect(cb2.callService).toHaveBeenCalledTimes(1) 87 | }) 88 | 89 | test('warns on unregistered service', () => { 90 | mb.send(cb.id, 'test') 91 | 92 | expect(consola.error).not.toHaveBeenCalled() 93 | expect(consola.warn).toHaveBeenCalledTimes(1) 94 | expect(consola.warn.mock.calls[0][0]).toMatch(/Service test not registered/) 95 | }) 96 | 97 | test('warns on already registered service', () => { 98 | mb.on('test') 99 | expect(consola.warn).not.toHaveBeenCalled() 100 | 101 | mb.on('test') 102 | expect(consola.warn).toHaveBeenCalledTimes(1) 103 | expect(consola.warn.mock.calls[0][0]).toMatch(/Service test already registered/) 104 | expect(consola.error).not.toHaveBeenCalled() 105 | }) 106 | 107 | test('receives data on service (m -> c)', () => { 108 | expect.assertions(3) 109 | 110 | const data = 'test data' 111 | cb.on('test', (message) => { 112 | expect(message).toBe(data) 113 | }) 114 | mb.send(cb.id, 'test', data) 115 | 116 | expect(consola.warn).not.toHaveBeenCalled() 117 | expect(consola.error).not.toHaveBeenCalled() 118 | }) 119 | 120 | test('receives data on service (c -> m)', () => { 121 | expect.assertions(3) 122 | 123 | const data = 'test data' 124 | mb.on('test', (message) => { 125 | expect(message).toBe(data) 126 | }) 127 | cb.send(mb.id, 'test', data) 128 | 129 | expect(consola.warn).not.toHaveBeenCalled() 130 | expect(consola.error).not.toHaveBeenCalled() 131 | }) 132 | 133 | test('receives data on service (c -> c)', () => { 134 | expect.assertions(3) 135 | const cb2 = new MessageBroker(childConf, mb) 136 | cb2.alias = 'c2' 137 | 138 | const data = 'test data' 139 | cb2.on('test', (message) => { 140 | expect(message).toBe(data) 141 | }) 142 | cb.send(cb2.id, 'test', data) 143 | 144 | expect(consola.warn).not.toHaveBeenCalled() 145 | expect(consola.error).not.toHaveBeenCalled() 146 | }) 147 | 148 | test('sends message through process for cluster worker', () => { 149 | cluster.isMaster = false 150 | 151 | process.send = jest.fn() 152 | 153 | cb.send(mb.id, 'test') 154 | 155 | expect(process.send).toHaveBeenCalledTimes(1) 156 | expect(consola.warn).not.toHaveBeenCalled() 157 | expect(consola.error).not.toHaveBeenCalled() 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/unit/miscellaneous.test.js: -------------------------------------------------------------------------------- 1 | import { Generate, Mixins, consola } from '../utils' 2 | 3 | jest.mock('../../lib/utils/consola') 4 | 5 | describe('miscellaneous', () => { 6 | beforeEach(() => { 7 | jest.clearAllMocks() 8 | }) 9 | 10 | test('generate.master does not call build', async () => { 11 | const master = new Generate.Master({}, {}) 12 | master.getRoutes = () => { return [] } 13 | master.build = jest.fn() 14 | master.initiate = jest.fn() 15 | master.startWorkers = jest.fn() 16 | 17 | await master.run({ build: false }) 18 | 19 | expect(master.build).not.toHaveBeenCalled() 20 | expect(master.initiate).toHaveBeenCalled() 21 | expect(master.startWorkers).not.toHaveBeenCalled() 22 | // expect(consola.warn).toHaveBeenCalled() 23 | }) 24 | 25 | test('generate.master hook:done', async () => { 26 | const done = jest.fn() 27 | const master = new Generate.Master({ 28 | generate: { done } 29 | }, {}) 30 | await master.init() 31 | master.generator.afterGenerate = jest.fn() 32 | master.initiate = jest.fn() 33 | 34 | await master.done() 35 | 36 | expect(done).toHaveBeenCalled() 37 | }) 38 | 39 | test('generate.master beforeWorkers', async () => { 40 | const beforeWorkers = jest.fn() 41 | const master = new Generate.Master({ 42 | generate: { beforeWorkers } 43 | }, {}) 44 | master.routes = ['/route'] 45 | master.build = jest.fn() 46 | master.initiate = jest.fn() 47 | master.startWorkers = jest.fn() 48 | 49 | await master.run() 50 | 51 | expect(beforeWorkers).toHaveBeenCalled() 52 | }) 53 | 54 | test('generate.master.getRoutes fails on exception in generator', async () => { 55 | const master = new Generate.Master({}, {}) 56 | await master.init() 57 | master.generator.initRoutes = () => { 58 | throw new Error('Error') 59 | } 60 | const success = await master.getRoutes() 61 | expect(success).toBe(false) 62 | }) 63 | 64 | test('generate.master.startWorkers shows error message', () => { 65 | const master = new Generate.Master({}, {}) 66 | master.startWorkers() 67 | expect(consola.error).toHaveBeenCalledTimes(1) 68 | }) 69 | 70 | test('generate.worker.generateRoutes fails on exception in generator', async () => { 71 | const worker = new Generate.Worker({}, {}) 72 | await worker.init() 73 | worker.generator.generateRoutes = () => { 74 | throw new Error('Oopsy') 75 | } 76 | 77 | await expect(worker.generateRoutes([])).rejects.toThrow('Oopsy') 78 | expect(consola.error).toHaveBeenCalledTimes(2) 79 | }) 80 | 81 | test('can pass consola.level to worker', () => { 82 | consola.defaultLevel = 0 83 | const worker = new Generate.Worker({ __workerLogLevel: 3 }, {}) 84 | expect(consola._level).toBe(3) 85 | expect(worker.id).toBe(-1) 86 | }) 87 | 88 | test('error in hooks are logged', async () => { 89 | class HookTestClass extends Mixins.Hookable() {} 90 | 91 | const msg = 'Oopsy' 92 | const hookName = 'throw-error' 93 | const hookTest = new HookTestClass() 94 | hookTest.hook(hookName, (msg) => { 95 | throw new Error(msg) 96 | }) 97 | await hookTest.callHook(hookName, msg) 98 | 99 | expect(consola.error).toHaveBeenCalledTimes(2) 100 | expect(consola.error.mock.calls[0][0]).toMatch(hookName) 101 | expect(consola.error.mock.calls[1][0]).toBe(msg) 102 | 103 | expect(Object.keys(hookTest._hooks).length).toBe(1) 104 | hookTest.hook() 105 | expect(Object.keys(hookTest._hooks).length).toBe(1) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/unit/reporter.test.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import messaging from '../../lib/utils/messaging' 3 | import { Reporters } from '../utils' 4 | 5 | let reporter 6 | 7 | jest.mock('consola') 8 | jest.mock('../../lib/utils/messaging') 9 | 10 | describe('cluster reporter', () => { 11 | beforeEach(() => { 12 | jest.clearAllMocks() 13 | reporter = new Reporters.ClusterReporter() 14 | }) 15 | 16 | // this test is not really a test 17 | test('nuxt success generated msg is ignored', () => { 18 | reporter.log({ 19 | type: 'success', 20 | args: ['Generated TEST'] 21 | }) 22 | 23 | expect(consola.success).not.toHaveBeenCalled() 24 | }) 25 | 26 | test('log is received by messaging', () => { 27 | reporter.log({ 28 | type: 'debug', 29 | args: ['Something'] 30 | }) 31 | 32 | expect(messaging.send).toHaveBeenCalledTimes(1) 33 | }) 34 | 35 | test('uses global ngc_log_tag', () => { 36 | global._ngc_log_tag = 'test tag' 37 | 38 | reporter.log({ 39 | type: 'debug', 40 | args: ['Something'] 41 | }) 42 | 43 | expect(messaging.send).toHaveBeenCalledTimes(1) 44 | expect(messaging.send.mock.calls[0][2].logObj.tag).toBe('test tag') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/utils/build.js: -------------------------------------------------------------------------------- 1 | import { loadFixture, Nuxt, Builder, BundleBuilder, listPaths, equalOrStartsWith } from './index' 2 | 3 | export const buildFixture = function (fixture, callback, hooks = []) { 4 | const pathsBefore = {} 5 | let nuxt 6 | 7 | test(`Build ${fixture}`, async () => { 8 | const config = await loadFixture(fixture) 9 | nuxt = new Nuxt(config) 10 | await nuxt.ready() 11 | 12 | pathsBefore.root = listPaths(nuxt.options.rootDir) 13 | if (nuxt.options.rootDir !== nuxt.options.srcDir) { 14 | pathsBefore.src = listPaths(nuxt.options.srcDir) 15 | } 16 | 17 | const buildDone = jest.fn() 18 | hooks.forEach(([hook, fn]) => nuxt.hook(hook, fn)) 19 | nuxt.hook('build:done', buildDone) 20 | const builder = await new Builder(nuxt, BundleBuilder).build() 21 | // 2: BUILD_DONE 22 | expect(builder._buildStatus).toBe(2) 23 | expect(buildDone).toHaveBeenCalledTimes(1) 24 | if (typeof callback === 'function') { 25 | callback(builder) 26 | } 27 | }, 120000) 28 | 29 | test('Check changed files', () => { 30 | expect.hasAssertions() 31 | 32 | // When building Nuxt we only expect files to changed 33 | // within the nuxt.options.buildDir 34 | Object.keys(pathsBefore).forEach((key) => { 35 | const paths = listPaths(nuxt.options[`${key}Dir`], pathsBefore[key]) 36 | paths.forEach((item) => { 37 | expect(equalOrStartsWith(nuxt.options.buildDir, item.path)).toBe(true) 38 | }) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /test/utils/cluster.worker.js: -------------------------------------------------------------------------------- 1 | const { Worker } = require('../../') 2 | Worker.start() 3 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import klawSync from 'klaw-sync' 4 | import spawn from 'cross-spawn' 5 | 6 | import _getPort from 'get-port' 7 | import { defaultsDeep, find } from 'lodash' 8 | import _rp from 'request-promise-native' 9 | 10 | import pkg from '../../package.json' 11 | import Cluster from '../../lib/index.js' 12 | import * as Async from '../../lib/async' 13 | import * as Generate from '../../lib/generate' 14 | import * as Mixins from '../../lib/mixins' 15 | import * as Reporters from '../../lib/utils/reporters' 16 | import { consola } from '../../lib/utils' 17 | 18 | export { Nuxt, Builder, Generator } from 'nuxt' 19 | 20 | export { BundleBuilder } from '@nuxt/webpack' 21 | 22 | export * from '../../lib/utils' 23 | 24 | // mostly just to silence them 25 | consola.mockTypes(() => jest.fn()) 26 | 27 | export { 28 | Cluster, 29 | Async, 30 | Generate, 31 | Mixins, 32 | Reporters 33 | } 34 | 35 | export const rp = _rp 36 | export const getPort = _getPort 37 | export const version = pkg.version 38 | 39 | export const loadFixture = async function (fixture, overrides) { 40 | const rootDir = path.resolve(__dirname, '..', 'fixtures', fixture) 41 | const configFile = path.resolve(rootDir, `nuxt.config${process.env.NUXT_TS === 'true' ? '.ts' : '.js'}`) 42 | 43 | let config = fs.existsSync(configFile) ? (await import(`../fixtures/${fixture}/nuxt.config`)).default : {} 44 | if (typeof config === 'function') { 45 | config = await config() 46 | } 47 | 48 | config.rootDir = rootDir 49 | config.dev = false 50 | config.test = true 51 | 52 | return defaultsDeep({}, overrides, config) 53 | } 54 | 55 | export const waitFor = function waitFor (ms) { 56 | return new Promise(resolve => setTimeout(resolve, ms || 0)) 57 | } 58 | 59 | /** 60 | * Prepare an object to pass to the createSportsSelectionView function 61 | * @param {Function} condition return true to stop the waiting 62 | * @param {Number} duration seconds totally wait 63 | * @param {Number} interval milliseconds interval to check the condition 64 | * 65 | * @returns {Boolean} true: timeout, false: condition becomes true within total time 66 | */ 67 | export const waitUntil = async function waitUntil (condition, duration = 20, interval = 250) { 68 | let iterator = 0 69 | const steps = Math.floor(duration * 1000 / interval) 70 | 71 | while (!condition() && iterator < steps) { 72 | await waitFor(interval) 73 | iterator++ 74 | } 75 | 76 | if (iterator === steps) { 77 | return true 78 | } 79 | return false 80 | } 81 | 82 | export const listPaths = function listPaths (dir, pathsBefore = [], options = {}) { 83 | if (Array.isArray(pathsBefore) && pathsBefore.length) { 84 | // only return files that didn't exist before building 85 | // and files that have been changed 86 | options.filter = (item) => { 87 | const foundItem = find(pathsBefore, (itemBefore) => { 88 | return item.path === itemBefore.path 89 | }) 90 | return typeof foundItem === 'undefined' || 91 | item.stats.mtimeMs !== foundItem.stats.mtimeMs 92 | } 93 | } 94 | 95 | return klawSync(dir, options) 96 | } 97 | 98 | export const equalOrStartsWith = function equalOrStartsWith (string1, string2) { 99 | return string1 === string2 || string2.startsWith(string1) 100 | } 101 | 102 | /** 103 | * Run the CLI script to generate a given fixture, and return the CLI's output. 104 | * 105 | * @param {string} fixtureName 106 | * @param {string[]} [extraArg] 107 | * @returns Promise {{stdout: string, stderr: string, exitCode: number}} 108 | */ 109 | export function runCliGenerate (fixtureName, extraArg) { 110 | const rootDir = path.resolve(__dirname, '..', 'fixtures', fixtureName) 111 | // Nuxt sets log level to 0 for CI and env=TEST 112 | // -v offsets from default log level, not current level 113 | // hence one -v is enough 114 | const args = [ 115 | rootDir, 116 | '--build', 117 | '--workers=2', 118 | '--config-file=nuxt.config.js', 119 | '-v' 120 | ] 121 | if (extraArg) { 122 | args.push(...extraArg) 123 | } 124 | const env = Object.assign(process.env, { 125 | NODE_ENV: 'production' 126 | }) 127 | 128 | const binGenerate = path.resolve(__dirname, '..', '..', 'bin', 'nuxt-generate.js') 129 | 130 | return new Promise((resolve) => { 131 | let stdout = '' 132 | let stderr = '' 133 | let error 134 | 135 | const nuxtGenerate = spawn(binGenerate, args, { env }) 136 | 137 | nuxtGenerate.stdout.on('data', (data) => { stdout += data.toString() }) 138 | nuxtGenerate.stderr.on('data', (data) => { stderr += data.toString() }) 139 | nuxtGenerate.on('error', (err) => { error = err }) 140 | nuxtGenerate.on('close', (exitCode, signal) => { 141 | resolve({ stdout, stderr, error, exitCode, signal }) 142 | }) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /test/utils/setup.js: -------------------------------------------------------------------------------- 1 | const isAppveyor = !!process.env.APPVEYOR 2 | describe.skip.appveyor = isAppveyor ? describe.skip : describe 3 | test.skip.appveyor = isAppveyor ? test.skip : test 4 | 5 | const isWin = process.platform === 'win32' 6 | describe.skip.win = isWin ? describe.skip : describe 7 | test.skip.win = isWin ? test.skip : test 8 | 9 | jest.setTimeout(60000) 10 | --------------------------------------------------------------------------------