feat(module): improve error handling

This commit is contained in:
Pooya Parsa 2018-01-11 16:58:45 +03:30
parent 5dd7591e3f
commit e5ffbdfcd2
2 changed files with 141 additions and 89 deletions

View File

@ -3,62 +3,69 @@ const fs = require('fs')
const { uniq } = require('lodash') const { uniq } = require('lodash')
const hash = require('hash-sum') const hash = require('hash-sum')
const { chainFn, sequence } = require('../common/utils') const { chainFn, sequence } = require('../common/utils')
const Debug = require('debug')
const debug = Debug('nuxt:module')
module.exports = class ModuleContainer { module.exports = class ModuleContainer {
constructor(nuxt) { constructor(nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.requiredModules = [] this.requiredModules = {}
} }
async ready() { async ready() {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules) await this.nuxt.callHook('modules:before', this, this.options.modules)
// Load every module in sequence // Load every module in sequence
await sequence(this.options.modules, this.addModule.bind(this)) await sequence(this.options.modules, this.addModule.bind(this))
// Call done hook // Call done hook
await this.nuxt.callHook('modules:done', this) await this.nuxt.callHook('modules:done', this)
} }
addVendor(vendor) { addVendor(vendor) {
/* istanbul ignore if */ /* istanbul ignore if */
if (!vendor) { if (!vendor || typeof vendor !== 'string') {
return throw new Error('Invalid vendor:' + vendor)
} }
this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor)) this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor))
} }
addTemplate(template) { addTemplate(template) {
/* istanbul ignore if */ /* istanbul ignore if */
if (!template) { if (!template || typeof template !== 'string') {
return throw new Error('Invalid template:' + template)
} }
// Validate & parse source // Validate & parse source
const src = template.src || template const src = template.src || template
const srcPath = path.parse(src) const srcPath = path.parse(src)
/* istanbul ignore if */ /* istanbul ignore if */
if (!src || typeof src !== 'string' || !fs.existsSync(src)) { if (!src || typeof src !== 'string' || !fs.existsSync(src)) {
/* istanbul ignore next */ throw new Error('Template not found:' + template)
debug('[nuxt] invalid template', template)
return
} }
// Generate unique and human readable dst filename // Generate unique and human readable dst filename
const dst = template.fileName || const dst =
(path.basename(srcPath.dir) + '.' + srcPath.name + '.' + hash(src) + srcPath.ext) template.fileName ||
`${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}.${
srcPath.ext
}`
// Add to templates list // Add to templates list
const templateObj = { const templateObj = {
src, src,
dst, dst,
options: template.options options: template.options
} }
this.options.build.templates.push(templateObj) this.options.build.templates.push(templateObj)
return templateObj return templateObj
} }
addPlugin(template) { addPlugin(template) {
const { dst } = this.addTemplate(template) const { dst } = this.addTemplate(template)
// Add to nuxt plugins // Add to nuxt plugins
this.options.plugins.unshift({ this.options.plugins.unshift({
src: path.join(this.options.buildDir, dst), src: path.join(this.options.buildDir, dst),
@ -75,72 +82,93 @@ module.exports = class ModuleContainer {
} }
extendRoutes(fn) { extendRoutes(fn) {
this.options.router.extendRoutes = chainFn(this.options.router.extendRoutes, fn) this.options.router.extendRoutes = chainFn(
this.options.router.extendRoutes,
fn
)
} }
requireModule(moduleOpts) { requireModule(moduleOpts) {
// Require once return this.addModule(moduleOpts, true /* require once */)
return this.addModule(moduleOpts, true)
} }
async addModule(moduleOpts, requireOnce) { async addModule(moduleOpts, requireOnce) {
/* istanbul ignore if */ let src
if (!moduleOpts) { let options
return let handler
// Type 1: String
if (typeof moduleOpts === 'string') {
src = moduleOpts
} }
// Allow using babel style array options // Type 2: Babel style array
if (Array.isArray(moduleOpts)) { if (Array.isArray(moduleOpts)) {
moduleOpts = { src = moduleOpts[0]
src: moduleOpts[0], options = moduleOpts[1]
options: moduleOpts[1] }
// Type 3: Pure object
if (typeof moduleOpts === 'object') {
src = moduleOpts.src
options = moduleOpts.options
handler = moduleOpts.handler
}
// Resolve handler
if (!handler) {
try {
handler = require(this.nuxt.resolvePath(src))
} catch (e) {
console.error(e) // eslint-disable-line no-console
throw new Error('Error while resolving module: ' + src)
} }
} }
// Allows passing runtime options to each module // Validate handler
const options = moduleOpts.options || (typeof moduleOpts === 'object' ? moduleOpts : {})
const src = moduleOpts.src || moduleOpts
// Resolve module
let module
if (typeof src === 'string') {
module = require(this.nuxt.resolvePath(src))
}
// Validate module
/* istanbul ignore if */ /* istanbul ignore if */
if (typeof module !== 'function') { if (typeof handler !== 'function') {
throw new Error(`[nuxt] Module ${JSON.stringify(src)} should export a function`) throw new Error('Module should export a function: ' + src)
} }
// Module meta // Resolve module meta
module.meta = module.meta || {} const key = (handler.meta && handler.meta.name) || handler.name || src
let name = module.meta.name || module.name
// If requireOnce specified & module from NPM or with specified name // Check requireOnce if possbile
if (requireOnce && name) { if (requireOnce && typeof key === 'string') {
const alreadyRequired = this.requiredModules.indexOf(name) !== -1 const alreadyRequired = this.requiredModules[key]
if (alreadyRequired) { if (alreadyRequired) {
return return
} }
this.requiredModules.push(name) this.requiredModules[key] = { src, options, handler }
} }
// Call module with `this` context and pass options return new Promise((resolve, reject) => {
await new Promise((resolve, reject) => { // Prepare callback
const result = module.call(this, options, (err) => { const cb = err => {
// eslint-disable-next-line no-console
console.warn(
'[Depricated] Consider using async/await for module handlers.' +
'Supporting callbacks will be removed in next releases: ' +
src
)
/* istanbul ignore if */ /* istanbul ignore if */
if (err) { if (err) {
return reject(err) return reject(err)
} }
resolve() resolve()
})
// If module send back a promise
if (result && result.then instanceof Function) {
return result.then(resolve)
} }
// Call module with `this` context and pass options
const result = handler.call(this, options, cb)
// If module send back a promise
if (result && result.then) {
return result
}
// If not expecting a callback but returns no promise (=synchronous) // If not expecting a callback but returns no promise (=synchronous)
if (module.length < 2) { if (handler.length < 2) {
return resolve() return resolve()
} }
}) })

View File

@ -31,7 +31,9 @@ module.exports = class Nuxt {
// Backward compatibility // Backward compatibility
this.render = this.renderer.app this.render = this.renderer.app
this.renderRoute = this.renderer.renderRoute.bind(this.renderer) this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
this.renderAndGetWindow = this.renderer.renderAndGetWindow.bind(this.renderer) this.renderAndGetWindow = this.renderer.renderAndGetWindow.bind(
this.renderer
)
this._ready = this.ready().catch(this.errorHandler) this._ready = this.ready().catch(this.errorHandler)
} }
@ -51,11 +53,16 @@ module.exports = class Nuxt {
} else if (typeof this.options.hooks === 'function') { } else if (typeof this.options.hooks === 'function') {
this.options.hooks(this.hook) this.options.hooks(this.hook)
} }
// Add nuxt modules
// Await for modules
await this.moduleContainer.ready() await this.moduleContainer.ready()
// Await for renderer to be ready
await this.renderer.ready() await this.renderer.ready()
this.initialized = true this.initialized = true
// Call ready hook
await this.callHook('ready', this) await this.callHook('ready', this)
return this return this
@ -63,14 +70,16 @@ module.exports = class Nuxt {
plugin(name, fn) { plugin(name, fn) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`[warn] nuxt.plugin('${name}',..) is deprecated. Please use new hooks system.`) console.error(
`[Deprecated] nuxt.plugin('${name}',..) is deprecated. Please use new hooks system.`
)
// A tiny backward compatibility util // A tiny backward compatibility util
const hookMap = { const hookMap = {
'ready': 'ready', ready: 'ready',
'close': 'close', close: 'close',
'listen': 'listen', listen: 'listen',
'built': 'build:done' built: 'build:done'
} }
if (hookMap[name]) { if (hookMap[name]) {
@ -95,7 +104,7 @@ module.exports = class Nuxt {
} }
debug(`Call ${name} hooks (${this._hooks[name].length})`) debug(`Call ${name} hooks (${this._hooks[name].length})`)
try { try {
await sequence(this._hooks[name], (fn) => fn(...args)) await sequence(this._hooks[name], fn => fn(...args))
} catch (err) { } catch (err) {
console.error(`> Error on hook "${name}":`) // eslint-disable-line no-console console.error(`> Error on hook "${name}":`) // eslint-disable-line no-console
console.error(err) // eslint-disable-line no-console console.error(err) // eslint-disable-line no-console
@ -103,11 +112,11 @@ module.exports = class Nuxt {
} }
addObjectHooks(hooksObj) { addObjectHooks(hooksObj) {
Object.keys(hooksObj).forEach((name) => { Object.keys(hooksObj).forEach(name => {
let hooks = hooksObj[name] let hooks = hooksObj[name]
hooks = (Array.isArray(hooks) ? hooks : [hooks]) hooks = Array.isArray(hooks) ? hooks : [hooks]
hooks.forEach((hook) => { hooks.forEach(hook => {
this.hook(name, hook) this.hook(name, hook)
}) })
}) })
@ -115,7 +124,9 @@ module.exports = class Nuxt {
listen(port = 3000, host = 'localhost') { listen(port = 3000, host = 'localhost') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = this.renderer.app.listen({ port, host, exclusive: false }, (err) => { const server = this.renderer.app.listen(
{ port, host, exclusive: false },
err => {
/* istanbul ignore if */ /* istanbul ignore if */
if (err) { if (err) {
return reject(err) return reject(err)
@ -123,10 +134,17 @@ module.exports = class Nuxt {
const _host = host === '0.0.0.0' ? 'localhost' : host const _host = host === '0.0.0.0' ? 'localhost' : host
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('\n' + chalk.bgGreen.black(' OPEN ') + chalk.green(` http://${_host}:${port}\n`)) console.log(
'\n' +
chalk.bgGreen.black(' OPEN ') +
chalk.green(` http://${_host}:${port}\n`)
)
// Close server on nuxt close // Close server on nuxt close
this.hook('close', () => new Promise((resolve, reject) => { this.hook(
'close',
() =>
new Promise((resolve, reject) => {
// Destroy server by forcing every connection to be closed // Destroy server by forcing every connection to be closed
server.destroy(err => { server.destroy(err => {
debug('server closed') debug('server closed')
@ -136,17 +154,19 @@ module.exports = class Nuxt {
} }
resolve() resolve()
}) })
})) })
)
this.callHook('listen', server, { port, host }).then(resolve) this.callHook('listen', server, { port, host }).then(resolve)
}) }
)
// Add server.destroy(cb) method // Add server.destroy(cb) method
enableDestroy(server) enableDestroy(server)
}) })
} }
errorHandler/* istanbul ignore next */() { errorHandler() {
// Apply plugins // Apply plugins
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
this.callHook('error', ...arguments).catch(console.error) this.callHook('error', ...arguments).catch(console.error)
@ -169,17 +189,21 @@ module.exports = class Nuxt {
resolvePath(path) { resolvePath(path) {
// Try to resolve using NPM resolve path first // Try to resolve using NPM resolve path first
try { try {
let resolvedPath = Module._resolveFilename(path, { paths: this.options.modulesDir }) let resolvedPath = Module._resolveFilename(path, {
paths: this.options.modulesDir
})
return resolvedPath return resolvedPath
} catch (e) { } catch (e) {
// Just continue // Continue to try other methods
} }
// Shorthand to resolve from project dirs // Shorthand to resolve from project dirs
if (path.indexOf('@@') === 0 || path.indexOf('~~') === 0) { if (path.indexOf('@@') === 0 || path.indexOf('~~') === 0) {
return join(this.options.rootDir, path.substr(2)) return join(this.options.rootDir, path.substr(2))
} else if (path.indexOf('@') === 0 || path.indexOf('~') === 0) { } else if (path.indexOf('@') === 0 || path.indexOf('~') === 0) {
return join(this.options.srcDir, path.substr(1)) return join(this.options.srcDir, path.substr(1))
} }
return resolve(this.options.srcDir, path) return resolve(this.options.srcDir, path)
} }