Skip to content

Commit

Permalink
feat: add plugin hook ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobrosenberg committed Oct 18, 2020
1 parent 4483fdc commit ea4ee17
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 12 deletions.
25 changes: 13 additions & 12 deletions lib/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference path="../typedef.js" />

const { deepAssign, log, keysAsFunctionsRecursive } = require('./utils')
const { deepAssign, log, keysAsFunctionsRecursive, sortHooks } = require('./utils')


/** @type {AppEvent[]} */
Expand Down Expand Up @@ -34,35 +34,36 @@ const App = class {
merge(obj) { deepAssign(this, obj) }
async initiate() {
this.config.roxi = await require('./roxi').run()
this.config.roxi.plugins.unshift(
{ name: 'mapper', params: {}, hooks: [{ event: 'bundle', order: { first: true }, action: app => keysAsFunctionsRecursive(app) }] },
)
process.env.ROXI_LOG_LEVEL = this.config.roxi.logLevel
}
async run(events = this.events) {
try {
for (const event of events) {
console.log('event', event)
this.state.event = event
await runPlugins(event, this)
}
} catch (err) { this.errorHandler(err) }
}
}

//todo move this somewhere else?
const objToArray = {
hooks: [{ event: 'bundle', action: app => keysAsFunctionsRecursive(app) }]
}

/**
* @param {AppEvent} event
* @param {RoxiApp} app
*/
async function runPlugins(event, app) {
const plugins = [objToArray, ...app.config.roxi.plugins]
for (const plugin of plugins) {
const hooks = plugin.hooks.filter(hookCondition(app, plugin.params, { event }))
for (const hook of hooks)
await app.hookHandler(app, hook, plugin, { event })
}
let hooks = []

for (const plugin of app.config.roxi.plugins)
plugin.hooks
.filter(hookCondition(app, plugin.params, { event }))
.forEach(hook => hooks.push({ plugin, hook }))

for (const hook of sortHooks(hooks))
await app.hookHandler(app, hook.hook, hook.plugin, { event })
}

/**
Expand Down
55 changes: 55 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,62 @@ async function keysAsFunctions(obj, map, name) {
return Promise.all(promises)
}


/** @param {{plugin: RoxiPlugin, hook: RoxiPluginHook}[]} hooks */
function sortHooks(hooks) {
const sortedHooks = []
let lastLength = null
let obstacles = []

while (hooks.length) {
if (hooks.length === lastLength)
throw new Error('infinite loop in hook orderings ' + JSON.stringify(obstacles, null, 2))
lastLength = hooks.length

for (const index in hooks) {
const hook = hooks[index]
const pluginName = hook.plugin.name
const orderings = [].concat(hook.hook.order || {})
const isFirst = orderings.find(order => order.first)
const isLast = orderings.find(order => order.last)
const runAfter = orderings.map(order => order.after).filter(Boolean)
const befores = orderings.map(order => order.before).filter(Boolean)

const obstacle = hooks.find((obstacleHook, _index) => {
// don't check against self (typeof index is string)
if (index == _index) return false

const obstacleOrderings = [].concat(obstacleHook.hook.order || {})
const obstacleIsLast = obstacleOrderings.find(order => order.last)
const obstacleIsFirst = obstacleOrderings.find(order => order.first)
const obstaclerunsAfter = obstacleOrderings.map(order => order.after).filter(Boolean)

if (
runAfter.includes(obstacleHook.plugin.name)
|| (isLast && !obstacleIsLast && !obstaclerunsAfter.includes(pluginName))
|| (obstacleIsFirst && !isFirst && !befores.includes(obstacleHook.plugin.name))
|| obstacleOrderings.find(order => order.before === pluginName)
)
return true
})

if (obstacle) {
obstacles.push({
plugin: hook.plugin.name,
obstacle: obstacle.plugin.name
})
} else {
sortedHooks.push(hooks.splice(index, 1)[0])
break;
}
}
}
return sortedHooks
}


module.exports = {
sortHooks,
normalizePluginConfig,
isObject,
deepAssign,
Expand Down
35 changes: 35 additions & 0 deletions lib/utils/tests/sortHooks.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { sortHooks } = require("..");

const hooks = [
{ hook: { order: [] }, plugin: { name: 'pluginB' } },
{ hook: { order: [] }, plugin: { name: 'pluginD' } },
{ hook: { order: { before: 'pluginD' } }, plugin: { name: 'pluginC' } },
{ hook: { order: [] }, plugin: { name: 'pluginE' } },
{ hook: { order: [{ after: 'pluginG' }] }, plugin: { name: 'pluginH' } },
{ hook: { order: [] }, plugin: { name: 'pluginF' } },
{ hook: { order: [{ last: true }] }, plugin: { name: 'last1' } },
{ hook: { order: [{ last: true }] }, plugin: { name: 'last2' } },
{ hook: { order: [{ after: 'last1' }] }, plugin: { name: 'afterLast1' } },
{ hook: { order: [] }, plugin: { name: 'pluginG' } },
{ hook: { order: { first: true } }, plugin: { name: 'first' } },
{ hook: { order: { before: 'first' } }, plugin: { name: 'beforeFirst' } },
]

it('sorts hooks', () => {
const sortedHooks = sortHooks(hooks)

expect(sortedHooks.map(hook => hook.plugin.name)).toEqual([
'beforeFirst', 'first', 'pluginB', 'pluginC', 'pluginD', 'pluginE', 'pluginF', 'pluginG', 'pluginH',
'last1',
'afterLast1',
'last2',
])
})

it('throws error for infinite loops', () => {
const breaker1 = { hook: { order: [{ before: 'beforeBreaker2' }] }, plugin: { name: 'beforeBreaker1' } }
const breaker2 = { hook: { order: [{ before: 'beforeBreaker1' }] }, plugin: { name: 'beforeBreaker2' } }
expect(()=>{
sortHooks([...hooks, breaker1, breaker2])
}).toThrow('infinite loop')
})
7 changes: 7 additions & 0 deletions typedef.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
* |'after:config'|'before:bundle'|'bundle'
* |'after:bundle'|'router'|'end'} AppEvent
*
* @typedef {object} HookOrder
* @prop {string=} before
* @prop {string=} after
* @prop {boolean=} first
* @prop {boolean=} last
*
* @typedef {object} RoxiPlugin
* @prop {string=} name
* @prop {RoxiPluginHook[]} hooks
Expand All @@ -12,6 +18,7 @@
* @prop {AppEvent} event
* @prop {string=} name
* @prop {RoxiPluginHookFunction|string=} condition
* @prop {HookOrder|HookOrder[]=} order
* @prop {RoxiPluginHookFunction} action
*
* @callback RoxiPluginHookFunction
Expand Down

0 comments on commit ea4ee17

Please sign in to comment.