diff --git a/lib/core/module.js b/lib/core/module.js index 2ce68de4e8..27d4f5ba53 100755 --- a/lib/core/module.js +++ b/lib/core/module.js @@ -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() } }) diff --git a/lib/core/nuxt.js b/lib/core/nuxt.js index 8a4dccc511..e7930001fb 100644 --- a/lib/core/nuxt.js +++ b/lib/core/nuxt.js @@ -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) }