Hooks ready to be tested

This commit is contained in:
Sébastien Chopin 2017-10-30 22:39:08 +01:00
parent f72e620d7d
commit 94ad5955e6
11 changed files with 84 additions and 87 deletions

View File

@ -6,7 +6,6 @@ import pify from 'pify'
import webpack from 'webpack' import webpack from 'webpack'
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
import { join, resolve, basename, extname, dirname } from 'path' import { join, resolve, basename, extname, dirname } from 'path'
import Tapable from 'tappable'
import MFS from 'memory-fs' import MFS from 'memory-fs'
import webpackDevMiddleware from 'webpack-dev-middleware' import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware' import webpackHotMiddleware from 'webpack-hot-middleware'
@ -24,9 +23,8 @@ debug.color = 2 // Force green color
const glob = pify(Glob) const glob = pify(Glob)
export default class Builder extends Tapable { export default class Builder {
constructor(nuxt) { constructor(nuxt) {
super()
this.nuxt = nuxt this.nuxt = nuxt
this.isStatic = false // Flag to know if the build is for a generated app this.isStatic = false // Flag to know if the build is for a generated app
this.options = nuxt.options this.options = nuxt.options
@ -57,9 +55,6 @@ export default class Builder extends Tapable {
this.vueLoader = vueLoaderConfig.bind(this) this.vueLoader = vueLoaderConfig.bind(this)
this._buildStatus = STATUS.INITIAL this._buildStatus = STATUS.INITIAL
// Call class hook
this.nuxt.applyPlugins('builder', this)
} }
get plugins() { get plugins() {
@ -121,8 +116,8 @@ export default class Builder extends Tapable {
// Wait for nuxt ready // Wait for nuxt ready
await this.nuxt.ready() await this.nuxt.ready()
// Wait for build plugins // Call before hook
await this.applyPluginsAsync('build', this.options.build) await this.nuxt.callHook('build:before', this, this.options.build)
// Babel options // Babel options
this.babelOptions = _.defaults(this.options.build.babel, { this.babelOptions = _.defaults(this.options.build.babel, {
@ -183,7 +178,8 @@ export default class Builder extends Tapable {
// Flag to set that building is done // Flag to set that building is done
this._buildStatus = STATUS.BUILD_DONE this._buildStatus = STATUS.BUILD_DONE
await this.applyPluginsAsync('built') // Call done hook
await this.nuxt.callHook('build:done', this)
return this return this
} }
@ -267,7 +263,7 @@ export default class Builder extends Tapable {
templateVars.router.routes = this.options.build.createRoutes(this.options.srcDir) templateVars.router.routes = this.options.build.createRoutes(this.options.srcDir)
} }
await this.applyPluginsAsync('extendRoutes', templateVars.router.routes) await this.nuxt.callHook('build:extendRoutes', templateVars.router.routes, r)
// router.extendRoutes method // router.extendRoutes method
if (typeof this.options.router.extendRoutes === 'function') { if (typeof this.options.router.extendRoutes === 'function') {
@ -335,7 +331,7 @@ export default class Builder extends Tapable {
} }
} }
await this.applyPluginsAsync('generate', { templatesFiles, templateVars, resolve: r }) await this.nuxt.callHook('build:templates', { templatesFiles, templateVars, resolve: r })
// Interpret and move template files to .nuxt/ // Interpret and move template files to .nuxt/
await Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => { await Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => {
@ -374,8 +370,6 @@ export default class Builder extends Tapable {
const dateFS = Date.now() / 1000 - 1000 const dateFS = Date.now() / 1000 - 1000
return utimes(path, dateFS, dateFS) return utimes(path, dateFS, dateFS)
})) }))
await this.applyPluginsAsync('generated')
} }
async webpackBuild() { async webpackBuild() {
@ -436,11 +430,11 @@ export default class Builder extends Tapable {
// Start Builds // Start Builds
await sequence(this.compilers, (compiler) => new Promise(async (resolve, reject) => { await sequence(this.compilers, (compiler) => new Promise(async (resolve, reject) => {
const name = compiler.options.name const name = compiler.options.name
await this.applyPluginsAsync('compile', { name, compiler }) await this.nuxt.callHook('build:compile', { name, compiler })
// Resolve only when compiler emit done event // Resolve only when compiler emit done event
compiler.plugin('done', async (stats) => { compiler.plugin('done', async (stats) => {
await this.applyPluginsAsync('compiled', { name, compiler, stats }) await this.nuxt.callHook('build:compiled', { name, compiler, stats })
process.nextTick(resolve) process.nextTick(resolve)
}) })
// --- Dev Build --- // --- Dev Build ---
@ -511,7 +505,7 @@ export default class Builder extends Tapable {
} }
// Stop webpack middleware on nuxt.close() // Stop webpack middleware on nuxt.close()
this.nuxt.plugin('close', () => new Promise(resolve => { this.nuxt.hook('close', () => new Promise(resolve => {
this.webpackDevMiddleware.close(() => resolve()) this.webpackDevMiddleware.close(() => resolve())
})) }))
@ -548,7 +542,7 @@ export default class Builder extends Tapable {
.on('change', refreshFiles) .on('change', refreshFiles)
// Stop watching on nuxt.close() // Stop watching on nuxt.close()
this.nuxt.plugin('close', () => { this.nuxt.hook('close', () => {
filesWatcher.close() filesWatcher.close()
customFilesWatcher.close() customFilesWatcher.close()
}) })

View File

@ -2,15 +2,13 @@ import { copy, remove, writeFile, mkdirp, removeSync, existsSync } from 'fs-extr
import _ from 'lodash' import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path' import { resolve, join, dirname, sep } from 'path'
import { minify } from 'html-minifier' import { minify } from 'html-minifier'
import Tapable from 'tappable'
import { isUrl, promisifyRoute, waitFor, flatRoutes } from 'utils' import { isUrl, promisifyRoute, waitFor, flatRoutes } from 'utils'
import Debug from 'debug' import Debug from 'debug'
const debug = Debug('nuxt:generate') const debug = Debug('nuxt:generate')
export default class Generator extends Tapable { export default class Generator {
constructor(nuxt, builder) { constructor(nuxt, builder) {
super()
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.builder = builder this.builder = builder
@ -20,28 +18,26 @@ export default class Generator extends Tapable {
this.srcBuiltPath = resolve(this.options.buildDir, 'dist') this.srcBuiltPath = resolve(this.options.buildDir, 'dist')
this.distPath = resolve(this.options.rootDir, this.options.generate.dir) this.distPath = resolve(this.options.rootDir, this.options.generate.dir)
this.distNuxtPath = join(this.distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath)) this.distNuxtPath = join(this.distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
// Call class hook
this.nuxt.applyPlugins('generator', this)
} }
async generate({ build = true, init = true } = {}) { async generate({ build = true, init = true } = {}) {
// Wait for nuxt be ready
await this.nuxt.ready()
// Call before hook
await this.nuxt.callHook('generate:before', this, this.options.generate)
const s = Date.now() const s = Date.now()
let errors = [] let errors = []
// Add flag to set process.static // Add flag to set process.static
this.builder.forGenerate() this.builder.forGenerate()
// Wait for nuxt be ready
await this.nuxt.ready()
// Start build process // Start build process
if (build) { if (build) {
await this.builder.build() await this.builder.build()
} }
await this.nuxt.applyPluginsAsync('generator', this)
// Initialize dist directory // Initialize dist directory
if (init) { if (init) {
await this.initDist() await this.initDist()
@ -63,7 +59,8 @@ export default class Generator extends Tapable {
let routes = (this.options.router.mode === 'hash') ? ['/'] : flatRoutes(this.options.router.routes) let routes = (this.options.router.mode === 'hash') ? ['/'] : flatRoutes(this.options.router.routes)
routes = this.decorateWithPayloads(routes, generateRoutes) routes = this.decorateWithPayloads(routes, generateRoutes)
await this.applyPluginsAsync('generate', routes) // extendRoutes hook
await this.nuxt.callHook('generate:extendRoutes', routes)
// Start generate process // Start generate process
while (routes.length) { while (routes.length) {
@ -87,7 +84,8 @@ export default class Generator extends Tapable {
const duration = Math.round((Date.now() - s) / 100) / 10 const duration = Math.round((Date.now() - s) / 100) / 10
debug(`HTML Files generated in ${duration}s`) debug(`HTML Files generated in ${duration}s`)
await this.applyPluginsAsync('generated', errors) // done hook
await this.nuxt.callHook('generate:done', this, errors)
if (errors.length) { if (errors.length) {
const report = errors.map(({ type, route, error }) => { const report = errors.map(({ type, route, error }) => {

View File

@ -291,6 +291,7 @@ Options.defaults = {
editor: { editor: {
editor: 'code' editor: 'code'
}, },
hooks: () => {},
messages: { messages: {
error_404: 'This page could not be found', error_404: 'This page could not be found',
server_error: 'Server error', server_error: 'Server error',

View File

@ -2,27 +2,24 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { uniq } from 'lodash' import { uniq } from 'lodash'
import hash from 'hash-sum' import hash from 'hash-sum'
import Tapable from 'tappable'
import { chainFn, sequence } from 'utils' import { chainFn, sequence } from 'utils'
import Debug from 'debug' import Debug from 'debug'
const debug = Debug('nuxt:module') const debug = Debug('nuxt:module')
export default class ModuleContainer extends Tapable { export default class ModuleContainer {
constructor(nuxt) { constructor(nuxt) {
super()
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.requiredModules = [] this.requiredModules = []
// Call class hook
this.nuxt.applyPlugins('moduleContainer', this)
} }
async ready() { async ready() {
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))
await this.applyPluginsAsync('ready') // Call done hook
await this.nuxt.callHook('modules:done', this)
} }
addVendor(vendor) { addVendor(vendor) {
@ -102,42 +99,43 @@ export default class ModuleContainer extends Tapable {
// Allows passing runtime options to each module // Allows passing runtime options to each module
const options = moduleOpts.options || (typeof moduleOpts === 'object' ? moduleOpts : {}) const options = moduleOpts.options || (typeof moduleOpts === 'object' ? moduleOpts : {})
const originalSrc = moduleOpts.src || moduleOpts const src = moduleOpts.src || moduleOpts
// Resolve module // Resolve module
let module = originalSrc let module
if (typeof module === 'string') { if (typeof src === 'string') {
module = require(this.nuxt.resolvePath(module)) module = require(this.nuxt.resolvePath(src))
} }
// Validate module // Validate module
/* istanbul ignore if */ /* istanbul ignore if */
if (typeof module !== 'function') { if (typeof module !== 'function') {
throw new Error(`[nuxt] Module ${JSON.stringify(originalSrc)} should export a function`) throw new Error(`[nuxt] Module ${JSON.stringify(src)} should export a function`)
} }
// Module meta // Module meta
if (!module.meta) { module.meta = module.meta || {}
module.meta = {} let name = module.meta.name || module.name
}
if (module.meta.name) { // If requireOnce specified & module from NPM or with specified name
const alreadyRequired = this.requiredModules.indexOf(module.meta.name) !== -1 if (requireOnce && name) {
const alreadyRequired = this.requiredModules.indexOf(name) !== -1
if (requireOnce && alreadyRequired) { if (requireOnce && alreadyRequired) {
return return
} }
if (!alreadyRequired) { if (!alreadyRequired) {
this.requiredModules.push(module.meta.name) this.requiredModules.push(name)
} }
} }
// Call module with `this` context and pass options // Call module with `this` context and pass options
const m = await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const result = module.call(this, options, (err, m) => { const result = module.call(this, options, (err) => {
/* istanbul ignore if */ /* istanbul ignore if */
if (err) { if (err) {
return reject(err) return reject(err)
} }
resolve(m) resolve()
}) })
// If module send back a promise // If module send back a promise
if (result && result.then instanceof Function) { if (result && result.then instanceof Function) {
@ -145,9 +143,8 @@ export default class ModuleContainer extends Tapable {
} }
// 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 (module.length < 2) {
return resolve(module) return resolve()
} }
}) })
await this.applyPluginsAsync('module', { meta: module.meta, module: m })
} }
} }

View File

@ -1,6 +1,6 @@
import Tapable from 'tappable'
import chalk from 'chalk' import chalk from 'chalk'
import { Options } from 'common' import { Options } from 'common'
import { sequence } from 'utils'
import ModuleContainer from './module' import ModuleContainer from './module'
import Renderer from './renderer' import Renderer from './renderer'
import Debug from 'debug' import Debug from 'debug'
@ -11,10 +11,8 @@ import { join, resolve } from 'path'
const debug = Debug('nuxt:') const debug = Debug('nuxt:')
debug.color = 5 debug.color = 5
export default class Nuxt extends Tapable { export default class Nuxt {
constructor(options = {}) { constructor(options = {}) {
super()
this.options = Options.from(options) this.options = Options.from(options)
// Paths for resolving requires from `rootDir` // Paths for resolving requires from `rootDir`
@ -22,6 +20,9 @@ export default class Nuxt extends Tapable {
this.initialized = false this.initialized = false
this.errorHandler = this.errorHandler.bind(this) this.errorHandler = this.errorHandler.bind(this)
// Hooks
this._hooks = {}
this.hook = this.hook.bind(this)
// Create instance of core components // Create instance of core components
this.moduleContainer = new ModuleContainer(this) this.moduleContainer = new ModuleContainer(this)
@ -40,17 +41,35 @@ export default class Nuxt extends Tapable {
return this._ready return this._ready
} }
// Call hooks
if (typeof this.options.hooks === 'function') {
this.options.hooks(this.hook)
}
// Add nuxt modules
await this.moduleContainer.ready() await this.moduleContainer.ready()
await this.applyPluginsAsync('ready')
await this.renderer.ready() await this.renderer.ready()
this.initialized = true this.initialized = true
await this.callHook('ready', this)
return this return this
} }
hook(name, fn) {
this._hooks[name] = this._hooks[name] || []
this._hooks[name].push(fn)
}
async callHook(name, ...args) {
if (!this._hooks[name]) {
return
}
await sequence(this._hooks[name], (fn) => fn(...args))
}
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)
@ -61,7 +80,7 @@ export default class Nuxt extends Tapable {
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.plugin('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')
@ -73,7 +92,7 @@ export default class Nuxt extends Tapable {
}) })
})) }))
resolve(this.applyPluginsAsync('listen', { server, port, host })) this.callHook('listen', server, { port, host }).then(resolve)
}) })
// Add server.destroy(cb) method // Add server.destroy(cb) method
@ -84,7 +103,7 @@ export default class Nuxt extends Tapable {
errorHandler/* istanbul ignore next */() { errorHandler/* istanbul ignore next */() {
// Apply plugins // Apply plugins
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
this.applyPluginsAsync('error', ...arguments).catch(console.error) this.callHook('error', ...arguments).catch(console.error)
// Silent // Silent
if (this.options.errorHandler === false) { if (this.options.errorHandler === false) {
@ -119,7 +138,7 @@ export default class Nuxt extends Tapable {
} }
async close(callback) { async close(callback) {
await this.applyPluginsAsync('close') await this.callHook('close', this)
/* istanbul ignore if */ /* istanbul ignore if */
if (typeof callback === 'function') { if (typeof callback === 'function') {

View File

@ -2,7 +2,6 @@ import ansiHTML from 'ansi-html'
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
import generateETag from 'etag' import generateETag from 'etag'
import fresh from 'fresh' import fresh from 'fresh'
import Tapable from 'tappable'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import compression from 'compression' import compression from 'compression'
import _ from 'lodash' import _ from 'lodash'
@ -24,9 +23,8 @@ setAnsiColors(ansiHTML)
let jsdom = null let jsdom = null
export default class Renderer extends Tapable { export default class Renderer {
constructor(nuxt) { constructor(nuxt) {
super()
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
@ -49,12 +47,10 @@ export default class Renderer extends Tapable {
spaTemplate: null, spaTemplate: null,
errorTemplate: parseTemplate('Nuxt.js Internal Server Error') errorTemplate: parseTemplate('Nuxt.js Internal Server Error')
} }
// Call class hook
this.nuxt.applyPlugins('renderer', this)
} }
async ready() { async ready() {
await this.nuxt.callHook('render:before', this, this.options.render)
// Setup nuxt middleware // Setup nuxt middleware
await this.setupMiddleware() await this.setupMiddleware()
@ -63,8 +59,8 @@ export default class Renderer extends Tapable {
await this.loadResources() await this.loadResources()
} }
// Call ready plugin // Call done hook
await this.applyPluginsAsync('ready') await this.nuxt.callHook('render:done', this)
} }
async loadResources(_fs = fs) { async loadResources(_fs = fs) {
@ -109,7 +105,7 @@ export default class Renderer extends Tapable {
} }
// Call resourcesLoaded plugin // Call resourcesLoaded plugin
await this.applyPluginsAsync('resourcesLoaded', this.resources) await this.nuxt.callHook('render:resourcesLoaded', this.resources)
if (updated.length > 0) { if (updated.length > 0) {
this.createRenderer() this.createRenderer()
@ -195,7 +191,7 @@ export default class Renderer extends Tapable {
async setupMiddleware() { async setupMiddleware() {
// Apply setupMiddleware from modules first // Apply setupMiddleware from modules first
await this.applyPluginsAsync('setupMiddleware', this.app) await this.nuxt.callHook('render:setupMiddleware', this.app)
// Gzip middleware for production // Gzip middleware for production
if (!this.options.dev && this.options.render.gzip) { if (!this.options.dev && this.options.render.gzip) {

View File

@ -107,7 +107,6 @@
"server-destroy": "^1.0.1", "server-destroy": "^1.0.1",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"source-map-support": "^0.5.0", "source-map-support": "^0.5.0",
"tappable": "^1.1.0",
"uglifyjs-webpack-plugin": "^1.0.1", "uglifyjs-webpack-plugin": "^1.0.1",
"url-loader": "^0.6.2", "url-loader": "^0.6.2",
"vue": "^2.5.2", "vue": "^2.5.2",

View File

@ -2,17 +2,17 @@ module.exports = function () {
let ctr = 1 let ctr = 1
// Add hook for module // Add hook for module
this.plugin('ready', moduleContainer => { this.plugin('modules:done', (moduleContainer) => {
this.nuxt.__module_hook = moduleContainer && ctr++ this.nuxt.__module_hook = moduleContainer && ctr++
}) })
// Add hook for renderer // Add hook for renderer
this.nuxt.plugin('renderer', renderer => { this.nuxt.hook('render:done', (renderer) => {
this.nuxt.__renderer_hook = renderer && ctr++ this.nuxt.__renderer_hook = renderer && ctr++
}) })
// Add hook for build // Add hook for build
this.nuxt.plugin('build', builder => { this.nuxt.plugin('build:done', (builder) => {
this.nuxt.__builder_hook = builder && ctr++ this.nuxt.__builder_hook = builder && ctr++
}) })
} }

View File

@ -2,7 +2,7 @@ module.exports = {
loading: true, loading: true,
modules: [ modules: [
'~/modules/basic', '~/modules/basic',
'~/modules/tapable', '~/modules/hooks',
{ {
src: '~/modules/middleware', src: '~/modules/middleware',
options: { options: {

View File

@ -36,7 +36,7 @@ test('Middleware', async t => {
t.is(response, 'It works!', '/api response is correct') t.is(response, 'It works!', '/api response is correct')
}) })
test('Tapable', async t => { test('Hooks', async t => {
t.is(nuxt.__module_hook, 1) t.is(nuxt.__module_hook, 1)
t.is(nuxt.__renderer_hook, 2) t.is(nuxt.__renderer_hook, 2)
t.is(nuxt.__builder_hook, 3) t.is(nuxt.__builder_hook, 3)

View File

@ -6488,17 +6488,10 @@ table@^4.0.1:
slice-ansi "1.0.0" slice-ansi "1.0.0"
string-width "^2.1.1" string-width "^2.1.1"
tapable@^0.2.6, tapable@^0.2.7: tapable@^0.2.7:
version "0.2.8" version "0.2.8"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
tappable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tappable/-/tappable-1.1.0.tgz#521770dea7dc4715d48ddb4c471071afee012025"
dependencies:
pify "^3.0.0"
tapable "^0.2.6"
tar-pack@^3.4.0: tar-pack@^3.4.0:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"