Skip to content

Commit ea4ee17

Browse files
feat: add plugin hook ordering
1 parent 4483fdc commit ea4ee17

File tree

4 files changed

+110
-12
lines changed

4 files changed

+110
-12
lines changed

lib/app.js

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path="../typedef.js" />
22

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

55

66
/** @type {AppEvent[]} */
@@ -34,35 +34,36 @@ const App = class {
3434
merge(obj) { deepAssign(this, obj) }
3535
async initiate() {
3636
this.config.roxi = await require('./roxi').run()
37+
this.config.roxi.plugins.unshift(
38+
{ name: 'mapper', params: {}, hooks: [{ event: 'bundle', order: { first: true }, action: app => keysAsFunctionsRecursive(app) }] },
39+
)
3740
process.env.ROXI_LOG_LEVEL = this.config.roxi.logLevel
3841
}
3942
async run(events = this.events) {
4043
try {
4144
for (const event of events) {
42-
console.log('event', event)
4345
this.state.event = event
4446
await runPlugins(event, this)
4547
}
4648
} catch (err) { this.errorHandler(err) }
4749
}
4850
}
4951

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

5553
/**
5654
* @param {AppEvent} event
5755
* @param {RoxiApp} app
5856
*/
5957
async function runPlugins(event, app) {
60-
const plugins = [objToArray, ...app.config.roxi.plugins]
61-
for (const plugin of plugins) {
62-
const hooks = plugin.hooks.filter(hookCondition(app, plugin.params, { event }))
63-
for (const hook of hooks)
64-
await app.hookHandler(app, hook, plugin, { event })
65-
}
58+
let hooks = []
59+
60+
for (const plugin of app.config.roxi.plugins)
61+
plugin.hooks
62+
.filter(hookCondition(app, plugin.params, { event }))
63+
.forEach(hook => hooks.push({ plugin, hook }))
64+
65+
for (const hook of sortHooks(hooks))
66+
await app.hookHandler(app, hook.hook, hook.plugin, { event })
6667
}
6768

6869
/**

lib/utils/index.js

+55
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,62 @@ async function keysAsFunctions(obj, map, name) {
100100
return Promise.all(promises)
101101
}
102102

103+
104+
/** @param {{plugin: RoxiPlugin, hook: RoxiPluginHook}[]} hooks */
105+
function sortHooks(hooks) {
106+
const sortedHooks = []
107+
let lastLength = null
108+
let obstacles = []
109+
110+
while (hooks.length) {
111+
if (hooks.length === lastLength)
112+
throw new Error('infinite loop in hook orderings ' + JSON.stringify(obstacles, null, 2))
113+
lastLength = hooks.length
114+
115+
for (const index in hooks) {
116+
const hook = hooks[index]
117+
const pluginName = hook.plugin.name
118+
const orderings = [].concat(hook.hook.order || {})
119+
const isFirst = orderings.find(order => order.first)
120+
const isLast = orderings.find(order => order.last)
121+
const runAfter = orderings.map(order => order.after).filter(Boolean)
122+
const befores = orderings.map(order => order.before).filter(Boolean)
123+
124+
const obstacle = hooks.find((obstacleHook, _index) => {
125+
// don't check against self (typeof index is string)
126+
if (index == _index) return false
127+
128+
const obstacleOrderings = [].concat(obstacleHook.hook.order || {})
129+
const obstacleIsLast = obstacleOrderings.find(order => order.last)
130+
const obstacleIsFirst = obstacleOrderings.find(order => order.first)
131+
const obstaclerunsAfter = obstacleOrderings.map(order => order.after).filter(Boolean)
132+
133+
if (
134+
runAfter.includes(obstacleHook.plugin.name)
135+
|| (isLast && !obstacleIsLast && !obstaclerunsAfter.includes(pluginName))
136+
|| (obstacleIsFirst && !isFirst && !befores.includes(obstacleHook.plugin.name))
137+
|| obstacleOrderings.find(order => order.before === pluginName)
138+
)
139+
return true
140+
})
141+
142+
if (obstacle) {
143+
obstacles.push({
144+
plugin: hook.plugin.name,
145+
obstacle: obstacle.plugin.name
146+
})
147+
} else {
148+
sortedHooks.push(hooks.splice(index, 1)[0])
149+
break;
150+
}
151+
}
152+
}
153+
return sortedHooks
154+
}
155+
156+
103157
module.exports = {
158+
sortHooks,
104159
normalizePluginConfig,
105160
isObject,
106161
deepAssign,

lib/utils/tests/sortHooks.spec.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { sortHooks } = require("..");
2+
3+
const hooks = [
4+
{ hook: { order: [] }, plugin: { name: 'pluginB' } },
5+
{ hook: { order: [] }, plugin: { name: 'pluginD' } },
6+
{ hook: { order: { before: 'pluginD' } }, plugin: { name: 'pluginC' } },
7+
{ hook: { order: [] }, plugin: { name: 'pluginE' } },
8+
{ hook: { order: [{ after: 'pluginG' }] }, plugin: { name: 'pluginH' } },
9+
{ hook: { order: [] }, plugin: { name: 'pluginF' } },
10+
{ hook: { order: [{ last: true }] }, plugin: { name: 'last1' } },
11+
{ hook: { order: [{ last: true }] }, plugin: { name: 'last2' } },
12+
{ hook: { order: [{ after: 'last1' }] }, plugin: { name: 'afterLast1' } },
13+
{ hook: { order: [] }, plugin: { name: 'pluginG' } },
14+
{ hook: { order: { first: true } }, plugin: { name: 'first' } },
15+
{ hook: { order: { before: 'first' } }, plugin: { name: 'beforeFirst' } },
16+
]
17+
18+
it('sorts hooks', () => {
19+
const sortedHooks = sortHooks(hooks)
20+
21+
expect(sortedHooks.map(hook => hook.plugin.name)).toEqual([
22+
'beforeFirst', 'first', 'pluginB', 'pluginC', 'pluginD', 'pluginE', 'pluginF', 'pluginG', 'pluginH',
23+
'last1',
24+
'afterLast1',
25+
'last2',
26+
])
27+
})
28+
29+
it('throws error for infinite loops', () => {
30+
const breaker1 = { hook: { order: [{ before: 'beforeBreaker2' }] }, plugin: { name: 'beforeBreaker1' } }
31+
const breaker2 = { hook: { order: [{ before: 'beforeBreaker1' }] }, plugin: { name: 'beforeBreaker2' } }
32+
expect(()=>{
33+
sortHooks([...hooks, breaker1, breaker2])
34+
}).toThrow('infinite loop')
35+
})

typedef.js

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
* |'after:config'|'before:bundle'|'bundle'
44
* |'after:bundle'|'router'|'end'} AppEvent
55
*
6+
* @typedef {object} HookOrder
7+
* @prop {string=} before
8+
* @prop {string=} after
9+
* @prop {boolean=} first
10+
* @prop {boolean=} last
11+
*
612
* @typedef {object} RoxiPlugin
713
* @prop {string=} name
814
* @prop {RoxiPluginHook[]} hooks
@@ -12,6 +18,7 @@
1218
* @prop {AppEvent} event
1319
* @prop {string=} name
1420
* @prop {RoxiPluginHookFunction|string=} condition
21+
* @prop {HookOrder|HookOrder[]=} order
1522
* @prop {RoxiPluginHookFunction} action
1623
*
1724
* @callback RoxiPluginHookFunction

0 commit comments

Comments
 (0)