mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +00:00
feat: HMR support for serverMiddleware (#6881)
This commit is contained in:
parent
6891c31d81
commit
8907e1553f
@ -7,6 +7,7 @@ import hash from 'hash-sum'
|
|||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
import upath from 'upath'
|
import upath from 'upath'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
|
||||||
import debounce from 'lodash/debounce'
|
import debounce from 'lodash/debounce'
|
||||||
import omit from 'lodash/omit'
|
import omit from 'lodash/omit'
|
||||||
@ -21,10 +22,8 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
determineGlobals,
|
determineGlobals,
|
||||||
stripWhitespace,
|
stripWhitespace,
|
||||||
isString,
|
|
||||||
isIndexFileAndFolder,
|
isIndexFileAndFolder,
|
||||||
isPureObject,
|
scanRequireTree
|
||||||
clearRequireCache
|
|
||||||
} from '@nuxt/utils'
|
} from '@nuxt/utils'
|
||||||
|
|
||||||
import Ignore from './ignore'
|
import Ignore from './ignore'
|
||||||
@ -60,6 +59,9 @@ export default class Builder {
|
|||||||
this.watchRestart()
|
this.watchRestart()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Enable HMR for serverMiddleware
|
||||||
|
this.serverMiddlewareHMR()
|
||||||
|
|
||||||
// Close hook
|
// Close hook
|
||||||
this.nuxt.hook('close', () => this.close())
|
this.nuxt.hook('close', () => this.close())
|
||||||
}
|
}
|
||||||
@ -649,6 +651,9 @@ export default class Builder {
|
|||||||
|
|
||||||
assignWatcher (key) {
|
assignWatcher (key) {
|
||||||
return (watcher) => {
|
return (watcher) => {
|
||||||
|
if (this.watchers[key]) {
|
||||||
|
this.watchers[key].close()
|
||||||
|
}
|
||||||
this.watchers[key] = watcher
|
this.watchers[key] = watcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -690,25 +695,61 @@ export default class Builder {
|
|||||||
this.createFileWatcher([r(this.options.srcDir, this.options.dir.app)], ['add', 'change', 'unlink'], refreshFiles, this.assignWatcher('app'))
|
this.createFileWatcher([r(this.options.srcDir, this.options.dir.app)], ['add', 'change', 'unlink'], refreshFiles, this.assignWatcher('app'))
|
||||||
}
|
}
|
||||||
|
|
||||||
getServerMiddlewarePaths () {
|
serverMiddlewareHMR () {
|
||||||
return this.options.serverMiddleware
|
// Check nuxt.server dependency
|
||||||
.map((serverMiddleware) => {
|
if (!this.nuxt.server) {
|
||||||
if (isString(serverMiddleware)) {
|
return
|
||||||
return serverMiddleware
|
|
||||||
}
|
}
|
||||||
if (isPureObject(serverMiddleware) && isString(serverMiddleware.handler)) {
|
|
||||||
return serverMiddleware.handler
|
// Get registered server middleware with path
|
||||||
|
const entries = this.nuxt.server.serverMiddlewarePaths()
|
||||||
|
|
||||||
|
// Resolve dependency tree
|
||||||
|
const deps = new Set()
|
||||||
|
const dep2Entry = {}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
for (const dep of scanRequireTree(entry)) {
|
||||||
|
deps.add(dep)
|
||||||
|
if (!dep2Entry[dep]) {
|
||||||
|
dep2Entry[dep] = new Set()
|
||||||
}
|
}
|
||||||
})
|
dep2Entry[dep].add(entry)
|
||||||
.filter(Boolean)
|
}
|
||||||
.map(p => path.extname(p) ? p : this.nuxt.resolver.resolvePath(p))
|
}
|
||||||
|
|
||||||
|
// Create watcher
|
||||||
|
this.createFileWatcher(
|
||||||
|
Array.from(deps),
|
||||||
|
['all'],
|
||||||
|
debounce((event, fileName) => {
|
||||||
|
for (const entry of dep2Entry[fileName]) {
|
||||||
|
// Reload entry
|
||||||
|
let newItem
|
||||||
|
try {
|
||||||
|
newItem = this.nuxt.server.replaceMiddleware(entry, entry)
|
||||||
|
} catch (error) {
|
||||||
|
consola.error(error)
|
||||||
|
consola.error(`[HMR Error]: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newItem) {
|
||||||
|
// Full reload if HMR failed
|
||||||
|
return this.nuxt.callHook('watch:restart', { event, path: fileName })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log
|
||||||
|
consola.info(`[HMR] ${chalk.cyan(newItem.route)} (${chalk.grey(fileName)})`)
|
||||||
|
}
|
||||||
|
// Tree may be changed so recreate watcher
|
||||||
|
this.serverMiddlewareHMR()
|
||||||
|
}, 200),
|
||||||
|
this.assignWatcher('serverMiddleware')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watchRestart () {
|
watchRestart () {
|
||||||
const serverMiddlewarePaths = this.getServerMiddlewarePaths()
|
|
||||||
const nuxtRestartWatch = [
|
const nuxtRestartWatch = [
|
||||||
// Server middleware
|
|
||||||
...serverMiddlewarePaths,
|
|
||||||
// Custom watchers
|
// Custom watchers
|
||||||
...this.options.watch
|
...this.options.watch
|
||||||
].map(this.nuxt.resolver.resolveAlias)
|
].map(this.nuxt.resolver.resolveAlias)
|
||||||
@ -732,11 +773,6 @@ export default class Builder {
|
|||||||
if (['add', 'change', 'unlink'].includes(event) === false) {
|
if (['add', 'change', 'unlink'].includes(event) === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/* istanbul ignore if */
|
|
||||||
if (serverMiddlewarePaths.includes(fileName)) {
|
|
||||||
consola.debug(`Clear cache for ${fileName}`)
|
|
||||||
clearRequireCache(fileName)
|
|
||||||
}
|
|
||||||
await this.nuxt.callHook('watch:fileChanged', this, fileName) // Legacy
|
await this.nuxt.callHook('watch:fileChanged', this, fileName) // Legacy
|
||||||
await this.nuxt.callHook('watch:restart', { event, path: fileName })
|
await this.nuxt.callHook('watch:restart', { event, path: fileName })
|
||||||
},
|
},
|
||||||
|
@ -290,8 +290,6 @@ describe('builder: builder watch', () => {
|
|||||||
expect(chokidar.watch).toBeCalledTimes(1)
|
expect(chokidar.watch).toBeCalledTimes(1)
|
||||||
expect(chokidar.watch).toBeCalledWith(
|
expect(chokidar.watch).toBeCalledWith(
|
||||||
[
|
[
|
||||||
'resolveAlias(resolvePath(/var/nuxt/src/serverMiddleware/test))',
|
|
||||||
'resolveAlias(resolvePath(/var/nuxt/src/serverMiddleware/test-handler))',
|
|
||||||
'resolveAlias(/var/nuxt/src/watch/test)',
|
'resolveAlias(/var/nuxt/src/watch/test)',
|
||||||
'/var/nuxt/src/.nuxtignore',
|
'/var/nuxt/src/.nuxtignore',
|
||||||
path.join('/var/nuxt/src/var/nuxt/src/store') // because store == false + using path.join()
|
path.join('/var/nuxt/src/var/nuxt/src/store') // because store == false + using path.join()
|
||||||
|
@ -171,40 +171,128 @@ export default class Server {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
useMiddleware (middleware) {
|
_normalizeMiddleware (middleware) {
|
||||||
let handler = middleware.handler || middleware
|
// Normalize plain function
|
||||||
|
if (typeof middleware === 'function') {
|
||||||
|
middleware = { handle: middleware }
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve handler setup as string (path)
|
// If a plain string provided as path to middleware
|
||||||
if (typeof handler === 'string') {
|
if (typeof middleware === 'string') {
|
||||||
|
middleware = this._requireMiddleware(middleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize handler to handle (backward compatiblity)
|
||||||
|
if (middleware.handler && !middleware.handle) {
|
||||||
|
middleware.handle = middleware.handler
|
||||||
|
delete middleware.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path to route (backward compatiblity)
|
||||||
|
if (middleware.path && !middleware.route) {
|
||||||
|
middleware.route = middleware.path
|
||||||
|
delete middleware.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// If handle is a string pointing to path
|
||||||
|
if (typeof middleware.handle === 'string') {
|
||||||
|
Object.assign(middleware, this._requireMiddleware(middleware.handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle
|
||||||
|
if (!middleware.handle) {
|
||||||
|
middleware.handle = (req, res, next) => {
|
||||||
|
next(new Error('ServerMiddleware should expose a handle: ' + middleware.entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
_requireMiddleware (entry) {
|
||||||
|
// Resolve entry
|
||||||
|
entry = this.nuxt.resolver.resolvePath(entry)
|
||||||
|
|
||||||
|
// Require middleware
|
||||||
|
let middleware
|
||||||
try {
|
try {
|
||||||
const requiredModuleFromHandlerPath = this.nuxt.resolver.requireModule(handler)
|
middleware = this.nuxt.resolver.requireModule(entry)
|
||||||
|
} catch (error) {
|
||||||
|
// Show full error
|
||||||
|
consola.error('ServerMiddleware Error:', error)
|
||||||
|
|
||||||
// In case the "handler" is not derived from an object but is a normal string, another object with
|
// Placeholder for error
|
||||||
// path and handler could be the result
|
middleware = {
|
||||||
|
route: '#error',
|
||||||
// If the required module has handler, treat the module as new "middleware" object
|
handle: (req, res, next) => { next(error) }
|
||||||
if (requiredModuleFromHandlerPath.handler) {
|
|
||||||
middleware = requiredModuleFromHandlerPath
|
|
||||||
}
|
|
||||||
|
|
||||||
handler = requiredModuleFromHandlerPath.handler || requiredModuleFromHandlerPath
|
|
||||||
} catch (err) {
|
|
||||||
consola.error(err)
|
|
||||||
// Throw error in production mode
|
|
||||||
if (!this.options.dev) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve path
|
// Normalize
|
||||||
const path = (
|
middleware = this._normalizeMiddleware(middleware)
|
||||||
|
|
||||||
|
// Set entry
|
||||||
|
middleware.entry = entry
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveMiddleware (middleware) {
|
||||||
|
// Ensure middleware is normalized
|
||||||
|
middleware = this._normalizeMiddleware(middleware)
|
||||||
|
|
||||||
|
// Resolve final route
|
||||||
|
middleware.route = (
|
||||||
(middleware.prefix !== false ? this.options.router.base : '') +
|
(middleware.prefix !== false ? this.options.router.base : '') +
|
||||||
(typeof middleware.path === 'string' ? middleware.path : '')
|
(typeof middleware.route === 'string' ? middleware.route : '')
|
||||||
).replace(/\/\//g, '/')
|
).replace(/\/\//g, '/')
|
||||||
|
|
||||||
// Use middleware
|
// Assign _middleware to handle to make accessable from app.stack
|
||||||
this.app.use(path, handler)
|
middleware.handle._middleware = middleware
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
useMiddleware (middleware) {
|
||||||
|
const { route, handle } = this.resolveMiddleware(middleware)
|
||||||
|
this.app.use(route.includes('#error') ? '/' : route, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceMiddleware (query, middleware) {
|
||||||
|
let serverStackItem
|
||||||
|
|
||||||
|
if (typeof query === 'string') {
|
||||||
|
// Search by entry
|
||||||
|
serverStackItem = this.app.stack.find(({ handle }) => handle._middleware && handle._middleware.entry === query)
|
||||||
|
} else {
|
||||||
|
// Search by reference
|
||||||
|
serverStackItem = this.app.stack.find(({ handle }) => handle === query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if item not found
|
||||||
|
if (!serverStackItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve middleware
|
||||||
|
const { route, handle } = this.resolveMiddleware(middleware)
|
||||||
|
|
||||||
|
// Update serverStackItem
|
||||||
|
serverStackItem.handle = handle
|
||||||
|
|
||||||
|
// Error State
|
||||||
|
if (route.includes('#error')) {
|
||||||
|
serverStackItem.route = serverStackItem.route || '/'
|
||||||
|
} else {
|
||||||
|
serverStackItem.route = route
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated item
|
||||||
|
return serverStackItem
|
||||||
|
}
|
||||||
|
|
||||||
|
serverMiddlewarePaths () {
|
||||||
|
return this.app.stack.map(({ handle }) => handle._middleware && handle._middleware.entry).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRoute () {
|
renderRoute () {
|
||||||
|
@ -61,7 +61,8 @@ describe('server: server', () => {
|
|||||||
ready: jest.fn(),
|
ready: jest.fn(),
|
||||||
callHook: jest.fn(),
|
callHook: jest.fn(),
|
||||||
resolver: {
|
resolver: {
|
||||||
requireModule: jest.fn()
|
requireModule: jest.fn(),
|
||||||
|
resolvePath: jest.fn().mockImplementation(p => p)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -379,7 +380,7 @@ describe('server: server', () => {
|
|||||||
expect(server.app.use).toBeCalledWith('/middleware', handler)
|
expect(server.app.use).toBeCalledWith('/middleware', handler)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should throw error when module resolves failed', () => {
|
test('should show error when module require failed', () => {
|
||||||
const nuxt = createNuxt()
|
const nuxt = createNuxt()
|
||||||
nuxt.options.router = { base: '/' }
|
nuxt.options.router = { base: '/' }
|
||||||
const server = new Server(nuxt)
|
const server = new Server(nuxt)
|
||||||
@ -388,9 +389,10 @@ describe('server: server', () => {
|
|||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => server.useMiddleware('test-middleware')).toThrow(error)
|
server.useMiddleware('test-middleware')
|
||||||
|
|
||||||
expect(consola.error).toBeCalledTimes(1)
|
expect(consola.error).toBeCalledTimes(1)
|
||||||
expect(consola.error).toBeCalledWith(error)
|
expect(consola.error).toBeCalledWith('ServerMiddleware Error:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should only log error when module resolves failed in dev mode', () => {
|
test('should only log error when module resolves failed in dev mode', () => {
|
||||||
@ -406,7 +408,7 @@ describe('server: server', () => {
|
|||||||
server.useMiddleware('test-middleware')
|
server.useMiddleware('test-middleware')
|
||||||
|
|
||||||
expect(consola.error).toBeCalledTimes(1)
|
expect(consola.error).toBeCalledTimes(1)
|
||||||
expect(consola.error).toBeCalledWith(error)
|
expect(consola.error).toBeCalledWith('ServerMiddleware Error:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should render route via renderer', () => {
|
test('should render route via renderer', () => {
|
||||||
|
@ -3,7 +3,14 @@ export function isExternalDependency (id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearRequireCache (id) {
|
export function clearRequireCache (id) {
|
||||||
const entry = require.cache[id]
|
let entry
|
||||||
|
try {
|
||||||
|
entry = require.cache[id]
|
||||||
|
} catch (e) {
|
||||||
|
delete require.cache[id]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!entry || isExternalDependency(id)) {
|
if (!entry || isExternalDependency(id)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -20,7 +27,14 @@ export function clearRequireCache (id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function scanRequireTree (id, files = new Set()) {
|
export function scanRequireTree (id, files = new Set()) {
|
||||||
const entry = require.cache[id]
|
let entry
|
||||||
|
try {
|
||||||
|
entry = require.cache[id]
|
||||||
|
} catch (e) {
|
||||||
|
files.add(id)
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
if (!entry || isExternalDependency(id) || files.has(id)) {
|
if (!entry || isExternalDependency(id) || files.has(id)) {
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
2
test/fixtures/cli/nuxt.config.js
vendored
2
test/fixtures/cli/nuxt.config.js
vendored
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
serverMiddleware: [
|
serverMiddleware: [
|
||||||
'~/middleware.js',
|
{ route: '/empty', handle: '~/middleware.js' },
|
||||||
(req, res, next) => next()
|
(req, res, next) => next()
|
||||||
],
|
],
|
||||||
watch: ['~/custom.file'],
|
watch: ['~/custom.file'],
|
||||||
|
Loading…
Reference in New Issue
Block a user