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 hash = require('hash-sum')
const { chainFn, sequence } = require('../common/utils')
const Debug = require('debug')
const debug = Debug('nuxt:module')
module.exports = class ModuleContainer {
constructor(nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = []
this.requiredModules = {}
}
async ready() {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules)
// Load every module in sequence
await sequence(this.options.modules, this.addModule.bind(this))
// Call done hook
await this.nuxt.callHook('modules:done', this)
}
addVendor(vendor) {
/* istanbul ignore if */
if (!vendor) {
return
if (!vendor || typeof vendor !== 'string') {
throw new Error('Invalid vendor:' + vendor)
}
this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor))
}
addTemplate(template) {
/* istanbul ignore if */
if (!template) {
return
if (!template || typeof template !== 'string') {
throw new Error('Invalid template:' + template)
}
// Validate & parse source
const src = template.src || template
const srcPath = path.parse(src)
/* istanbul ignore if */
if (!src || typeof src !== 'string' || !fs.existsSync(src)) {
/* istanbul ignore next */
debug('[nuxt] invalid template', template)
return
throw new Error('Template not found:' + template)
}
// Generate unique and human readable dst filename
const dst = template.fileName ||
(path.basename(srcPath.dir) + '.' + srcPath.name + '.' + hash(src) + srcPath.ext)
const dst =
template.fileName ||
`${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}.${
srcPath.ext
}`
// Add to templates list
const templateObj = {
src,
dst,
options: template.options
}
this.options.build.templates.push(templateObj)
return templateObj
}
addPlugin(template) {
const { dst } = this.addTemplate(template)
// Add to nuxt plugins
this.options.plugins.unshift({
src: path.join(this.options.buildDir, dst),
@ -75,72 +82,93 @@ module.exports = class ModuleContainer {
}
extendRoutes(fn) {
this.options.router.extendRoutes = chainFn(this.options.router.extendRoutes, fn)
this.options.router.extendRoutes = chainFn(
this.options.router.extendRoutes,
fn
)
}
requireModule(moduleOpts) {
// Require once
return this.addModule(moduleOpts, true)
return this.addModule(moduleOpts, true /* require once */)
}
async addModule(moduleOpts, requireOnce) {
/* istanbul ignore if */
if (!moduleOpts) {
return
let src
let options
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)) {
moduleOpts = {
src: moduleOpts[0],
options: moduleOpts[1]
src = moduleOpts[0]
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
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
// Validate handler
/* istanbul ignore if */
if (typeof module !== 'function') {
throw new Error(`[nuxt] Module ${JSON.stringify(src)} should export a function`)
if (typeof handler !== 'function') {
throw new Error('Module should export a function: ' + src)
}
// Module meta
module.meta = module.meta || {}
let name = module.meta.name || module.name
// Resolve module meta
const key = (handler.meta && handler.meta.name) || handler.name || src
// If requireOnce specified & module from NPM or with specified name
if (requireOnce && name) {
const alreadyRequired = this.requiredModules.indexOf(name) !== -1
// Check requireOnce if possbile
if (requireOnce && typeof key === 'string') {
const alreadyRequired = this.requiredModules[key]
if (alreadyRequired) {
return
}
this.requiredModules.push(name)
this.requiredModules[key] = { src, options, handler }
}
// Call module with `this` context and pass options
await new Promise((resolve, reject) => {
const result = module.call(this, options, (err) => {
return new Promise((resolve, reject) => {
// Prepare callback
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 */
if (err) {
return reject(err)
}
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 (module.length < 2) {
if (handler.length < 2) {
return resolve()
}
})

View File

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