diff --git a/.eslintrc.js b/.eslintrc.js index 9fa6477017..428eefc3b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,9 @@ module.exports = { extends: [ '@nuxtjs' ], + "globals": { + "BigInt": true + }, overrides: [{ files: [ 'test/fixtures/*/.nuxt*/**' ], rules: { diff --git a/packages/config/src/config/server.js b/packages/config/src/config/server.js index 8f4045ee4b..8d45fee77f 100644 --- a/packages/config/src/config/server.js +++ b/packages/config/src/config/server.js @@ -9,5 +9,6 @@ export default ({ env = {} } = {}) => ({ env.npm_package_config_nuxt_host || 'localhost', socket: env.UNIX_SOCKET || - env.npm_package_config_unix_socket + env.npm_package_config_unix_socket, + timing: false }) diff --git a/packages/config/test/__snapshots__/options.test.js.snap b/packages/config/test/__snapshots__/options.test.js.snap index 320453b49d..f5803e59b4 100644 --- a/packages/config/test/__snapshots__/options.test.js.snap +++ b/packages/config/test/__snapshots__/options.test.js.snap @@ -306,6 +306,7 @@ Object { "https": false, "port": 3000, "socket": undefined, + "timing": false, }, "serverMiddleware": Array [], "srcDir": "/var/nuxt/test", diff --git a/packages/config/test/config/__snapshots__/index.test.js.snap b/packages/config/test/config/__snapshots__/index.test.js.snap index 5a87acd139..23837bdcc4 100644 --- a/packages/config/test/config/__snapshots__/index.test.js.snap +++ b/packages/config/test/config/__snapshots__/index.test.js.snap @@ -282,6 +282,7 @@ Object { "https": false, "port": 3000, "socket": undefined, + "timing": false, }, "serverMiddleware": Array [], "srcDir": undefined, @@ -605,6 +606,7 @@ Object { "https": false, "port": "3001", "socket": "/var/run/nuxt.sock", + "timing": false, }, "serverMiddleware": Array [], "srcDir": undefined, diff --git a/packages/config/test/config/server.test.js b/packages/config/test/config/server.test.js index 69a3a1b93f..ee59034a82 100644 --- a/packages/config/test/config/server.test.js +++ b/packages/config/test/config/server.test.js @@ -1,25 +1,17 @@ import serverConfig from '../../src/config/server' -describe('config: server', () => { - test('should return default server configurations', () => { - expect(serverConfig()).toEqual({ - https: false, - port: 3000, - host: 'localhost', - socket: undefined - }) - }) +const serverDefaults = serverConfig() +describe('config: server', () => { test('should return server configurations with NUXT_* env', () => { const env = { NUXT_PORT: 3001, NUXT_HOST: '127.0.0.1' } expect(serverConfig({ env })).toEqual({ - https: false, + ...serverDefaults, port: env.NUXT_PORT, - host: env.NUXT_HOST, - socket: undefined + host: env.NUXT_HOST }) }) @@ -30,7 +22,7 @@ describe('config: server', () => { UNIX_SOCKET: '/var/run/env.sock' } expect(serverConfig({ env })).toEqual({ - https: false, + ...serverDefaults, port: env.PORT, host: env.HOST, socket: env.UNIX_SOCKET @@ -44,7 +36,7 @@ describe('config: server', () => { npm_package_config_unix_socket: '/var/run/env.npm.sock' } expect(serverConfig({ env })).toEqual({ - https: false, + ...serverDefaults, port: env.npm_package_config_nuxt_port, host: env.npm_package_config_nuxt_host, socket: env.npm_package_config_unix_socket diff --git a/packages/server/package.json b/packages/server/package.json index e17db971d0..e0b255193f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -20,6 +20,7 @@ "fs-extra": "^7.0.1", "ip": "^1.1.5", "launch-editor-middleware": "^2.2.1", + "on-headers": "^1.0.1", "pify": "^4.0.1", "semver": "^5.6.0", "serve-placeholder": "^1.1.0", diff --git a/packages/server/src/middleware/timing.js b/packages/server/src/middleware/timing.js new file mode 100644 index 0000000000..b2b27fd8a7 --- /dev/null +++ b/packages/server/src/middleware/timing.js @@ -0,0 +1,51 @@ +import consola from 'consola' +import onHeaders from 'on-headers' +import { Timer } from '@nuxt/utils' + +export default options => (req, res, next) => { + if (res.timing) { + consola.warn('server-timing is already registered.') + } + res.timing = new ServerTiming() + + if (options && options.total) { + res.timing.start('total', 'Nuxt Server Time') + } + + onHeaders(res, () => { + res.timing.end('total') + + res.setHeader( + 'Server-Timing', + [] + .concat(res.getHeader('Server-Timing') || []) + .concat(res.timing.headers) + .join(', ') + ) + }) + + next() +} + +class ServerTiming extends Timer { + constructor(...args) { + super(...args) + this.headers = [] + } + + end(...args) { + const time = super.end(...args) + this.headers.push(this.formatHeader(time)) + return time + } + + clear() { + super.clear() + this.headers.length = 0 + } + + formatHeader(time) { + const desc = time.description ? `;desc="${time.description}"` : '' + return `${time.name};dur=${time.duration}${desc}` + } +} diff --git a/packages/server/src/server.js b/packages/server/src/server.js index e0de042691..c2f92c37d0 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -12,6 +12,7 @@ import nuxtMiddleware from './middleware/nuxt' import errorMiddleware from './middleware/error' import Listener from './listener' import createModernMiddleware from './middleware/modern' +import createTimingMiddleware from './middleware/timing' export default class Server { constructor(nuxt) { @@ -75,6 +76,10 @@ export default class Server { } } + if (this.options.server.timing) { + this.useMiddleware(createTimingMiddleware(this.options.server.timing)) + } + const modernMiddleware = createModernMiddleware({ context: this.renderer.context }) diff --git a/packages/utils/src/timer.js b/packages/utils/src/timer.js index e4ebabadf3..bb7152751c 100644 --- a/packages/utils/src/timer.js +++ b/packages/utils/src/timer.js @@ -24,3 +24,42 @@ export const timeout = function timeout(fn, ms, msg) { export const waitFor = function waitFor(ms) { return new Promise(resolve => setTimeout(resolve, ms || 0)) } +export class Timer { + constructor() { + this._times = new Map() + } + + start(name, description) { + const time = { + name, + description, + start: this.hrtime() + } + this._times.set(name, time) + return time + } + + end(name) { + if (this._times.has(name)) { + const time = this._times.get(name) + time.duration = this.hrtime(time.start) + this._times.delete(name) + return time + } + } + + hrtime(start) { + const useBigInt = typeof process.hrtime.bigint === 'function' + if (start) { + const end = useBigInt ? process.hrtime.bigint() : process.hrtime(start) + return useBigInt + ? (end - start) / BigInt(1000000) + : (end[0] * 1e3) + (end[1] * 1e-6) + } + return useBigInt ? process.hrtime.bigint() : process.hrtime() + } + + clear() { + this._times.clear() + } +} diff --git a/test/fixtures/with-config/nuxt.config.js b/test/fixtures/with-config/nuxt.config.js index d27dab4503..404ddee974 100644 --- a/test/fixtures/with-config/nuxt.config.js +++ b/test/fixtures/with-config/nuxt.config.js @@ -6,7 +6,10 @@ export default { srcDir: __dirname, server: { port: 8000, - host: '0.0.0.0' + host: '0.0.0.0', + timing: { + total: true + } }, router: { base: '/test/', diff --git a/test/unit/with-config.test.js b/test/unit/with-config.test.js index 4bb47d7fec..374f44b791 100644 --- a/test/unit/with-config.test.js +++ b/test/unit/with-config.test.js @@ -197,6 +197,13 @@ describe('with-config', () => { expect(fakeErrorLog).toHaveBeenCalled() }) + test('/ with Server-Timing header', async () => { + const { headers } = await rp(url('/test'), { + resolveWithFullResponse: true + }) + expect(headers['server-timing']).toMatch(/total;dur=\d+;desc="Nuxt Server Time"/) + }) + // Close server and ask nuxt to stop listening to file changes afterAll(async () => { await nuxt.close() diff --git a/yarn.lock b/yarn.lock index aabe002f7a..abccf1c596 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7771,7 +7771,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -on-headers@~1.0.1: +on-headers@^1.0.1, on-headers@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=