├── .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 |
3 |
4 |
5 | [](https://www.npmjs.com/package/nuxt-generate-cluster)
6 | [](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 |
2 |
3 | Should be ignored
4 |
5 |
6 |
7 |
11 |
12 |
14 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/async-data.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ name }}
3 |
4 |
5 |
14 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/await-async-data.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ name }}
3 |
4 |
5 |
19 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/callback-async-data.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ name }}
3 |
4 |
5 |
14 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/config.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ vueConfig[key] | toStr }}
4 |
5 |
6 |
7 |
28 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/css.vue:
--------------------------------------------------------------------------------
1 |
2 | This is red
3 |
4 |
5 |
14 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/error-midd.vue:
--------------------------------------------------------------------------------
1 |
2 | Error page midd
3 |
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 |
2 | Error page
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/error2.vue:
--------------------------------------------------------------------------------
1 |
2 | Error page
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/extractCSS.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/fn-midd.vue:
--------------------------------------------------------------------------------
1 |
2 | Date: {{ date }}
3 |
4 |
5 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/head.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
I can haz meta tags
4 |
5 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/ignored.test.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Should be ignored
4 |
5 |
6 |
7 |
11 |
12 |
14 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 | Index page
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/js-link.js:
--------------------------------------------------------------------------------
1 | export default {
2 | }
3 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/js-link.vue:
--------------------------------------------------------------------------------
1 |
2 | vue file is first-class
3 |
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 |
2 | {{ $store.state.meta }}
3 |
4 |
5 |
13 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/no-ssr.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Displayed only on client-side
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/noloading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ name }}
4 |
{{ loaded }}
5 |
6 |
7 |
8 |
28 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/pug.vue:
--------------------------------------------------------------------------------
1 |
2 | h1 Pug page
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/redirect-external.vue:
--------------------------------------------------------------------------------
1 |
2 | Redirecting...
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/redirect-middleware.vue:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/redirect-name.vue:
--------------------------------------------------------------------------------
1 |
2 | Redirecting...
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/redirect.vue:
--------------------------------------------------------------------------------
1 |
2 | Redirecting...
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/router-guard.vue:
--------------------------------------------------------------------------------
1 |
2 | Router Guard
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/special-state.vue:
--------------------------------------------------------------------------------
1 |
2 | Special state in `window.__NUXT__`
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/stateful.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
The answer is {{ answer }}
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/stateless.vue:
--------------------------------------------------------------------------------
1 |
2 | My component!
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/store-module.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ $store.state.simpleModule.mutateMe }}
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/store.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ baz }}
4 |
5 |
{{ $store.state.counter }}
6 |
7 |
{{ getVal }}
8 |
9 |
{{ getBabVal }}
10 |
11 |
12 |
13 |
24 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/style.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
H1 is rebeccapurple
4 | {{ $style }}
5 |
6 |
7 |
8 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/users/_id.vue:
--------------------------------------------------------------------------------
1 |
2 | User: {{ id }}
3 |
4 |
5 |
13 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/validate-async.vue:
--------------------------------------------------------------------------------
1 |
2 | I am valid
3 |
4 |
5 |
22 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/validate.vue:
--------------------------------------------------------------------------------
1 |
2 | I am valid
3 |
4 |
5 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/basic/pages/тест雨.vue:
--------------------------------------------------------------------------------
1 |
2 | Hello unicode!
3 |
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 |
2 | Hello World
3 |
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('')
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('')
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 |
--------------------------------------------------------------------------------