-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
285 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { expect, test } from 'vitest'; | ||
import { PluginHookType, PluginManager } from './plugin_manager'; | ||
import type { Plugin } from './types'; | ||
|
||
test('PluginManager should execute plugins in correct order (pre -> normal -> post)', async () => { | ||
const order: string[] = []; | ||
const plugins: Plugin[] = [ | ||
{ enforce: 'post', buildStart: () => order.push('post') }, | ||
{ enforce: 'pre', buildStart: () => order.push('pre') }, | ||
{ buildStart: () => order.push('normal') }, | ||
]; | ||
|
||
const manager = new PluginManager(plugins); | ||
await manager.apply({ | ||
hook: 'buildStart', | ||
args: [], | ||
type: PluginHookType.Series, | ||
pluginContext: {}, | ||
}); | ||
|
||
expect(order).toEqual(['pre', 'normal', 'post']); | ||
}); | ||
|
||
test('First hook type should return first non-null result', async () => { | ||
const plugins: Plugin[] = [ | ||
{ buildStart: () => null }, | ||
{ buildStart: () => 'second' }, | ||
{ buildStart: () => 'third' }, | ||
]; | ||
|
||
const manager = new PluginManager(plugins); | ||
const result = await manager.apply({ | ||
hook: 'buildStart', | ||
args: [], | ||
type: PluginHookType.First, | ||
pluginContext: {}, | ||
}); | ||
|
||
expect(result).toBe('second'); | ||
}); | ||
|
||
test('Series hook type should pass result through plugins', async () => { | ||
const plugins: (Plugin & { transform: (n: number) => number })[] = [ | ||
{ transform: (n: number) => n + 1 }, | ||
{ transform: (n: number) => n * 2 }, | ||
{ transform: (n: number) => n - 3 }, | ||
]; | ||
|
||
const manager = new PluginManager(plugins); | ||
const result = await manager.apply({ | ||
hook: 'transform', | ||
args: [], | ||
memo: 5, | ||
type: PluginHookType.Series, | ||
pluginContext: {}, | ||
}); | ||
|
||
// 5 -> 6 -> 12 -> 9 | ||
expect(result).toBe(9); | ||
}); | ||
|
||
test('Parallel hook type should execute plugins concurrently', async () => { | ||
const delays = [30, 10, 20]; | ||
const order: number[] = []; | ||
|
||
const plugins: (Plugin & { test: () => Promise<string> })[] = delays.map( | ||
(delay, index) => ({ | ||
test: async () => { | ||
await new Promise((resolve) => setTimeout(resolve, delay)); | ||
order.push(index); | ||
return `result${index}`; | ||
}, | ||
}), | ||
); | ||
|
||
const manager = new PluginManager(plugins); | ||
const results = await manager.apply({ | ||
hook: 'test', | ||
args: [], | ||
type: PluginHookType.Parallel, | ||
pluginContext: {}, | ||
}); | ||
|
||
// Results should be in original plugin order | ||
expect(results).toEqual(['result0', 'result1', 'result2']); | ||
// Execution order should be based on delays (10ms, 20ms, 30ms) | ||
expect(order).toEqual([1, 2, 0]); | ||
}); | ||
|
||
test('Plugin context should be correctly passed to hooks', async () => { | ||
const context = { value: 42 }; | ||
const plugin: Plugin & { test: () => number } = { | ||
test() { | ||
expect(this).toBe(context); | ||
// @ts-expect-error | ||
return this.value; | ||
}, | ||
}; | ||
|
||
const manager = new PluginManager([plugin]); | ||
const result = await manager.apply({ | ||
hook: 'test', | ||
args: [], | ||
type: PluginHookType.First, | ||
pluginContext: context, | ||
}); | ||
|
||
expect(result).toBe(42); | ||
}); | ||
|
||
test('Invalid hook type should throw error', async () => { | ||
const manager = new PluginManager([]); | ||
|
||
await expect( | ||
manager.apply({ | ||
hook: 'test', | ||
args: [], | ||
type: 'invalid' as any, | ||
pluginContext: {}, | ||
}), | ||
).rejects.toThrow('Invalid hook type: invalid'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { Plugin } from './types'; | ||
|
||
export enum PluginHookType { | ||
First = 'first', | ||
Series = 'series', | ||
Parallel = 'parallel', | ||
} | ||
|
||
export class PluginManager<T extends Plugin> { | ||
#plugins: T[] = []; | ||
constructor(rawPlugins: T[]) { | ||
this.#plugins = [ | ||
...rawPlugins.filter((p) => p.enforce === 'pre'), | ||
...rawPlugins.filter((p) => !p.enforce), | ||
...rawPlugins.filter((p) => p.enforce === 'post'), | ||
]; | ||
} | ||
|
||
async apply({ | ||
hook, | ||
args, | ||
memo, | ||
type = PluginHookType.Series, | ||
pluginContext, | ||
}: { | ||
hook: keyof T; | ||
args: any[]; | ||
memo?: any; | ||
type: PluginHookType; | ||
pluginContext: any; | ||
}) { | ||
const plugins = this.#plugins.filter((p) => !!p[hook]); | ||
if (type === PluginHookType.First) { | ||
for (const plugin of plugins) { | ||
const hookFn = plugin[hook] as Function; | ||
if (typeof hookFn === 'function') { | ||
const result = await hookFn.apply(pluginContext, args); | ||
if (result != null) { | ||
return result; | ||
} | ||
} | ||
} | ||
return null; | ||
} else if (type === PluginHookType.Parallel) { | ||
const results = await Promise.all( | ||
plugins.map((p) => { | ||
const hookFn = p[hook] as Function; | ||
if (typeof hookFn === 'function') { | ||
return hookFn.apply(pluginContext, args); | ||
} | ||
return null; | ||
}), | ||
); | ||
return results.filter((r) => r != null); | ||
} else if (type === PluginHookType.Series) { | ||
let result = memo; | ||
for (const plugin of plugins) { | ||
const hookFn = plugin[hook] as Function; | ||
if (typeof hookFn === 'function') { | ||
result = await hookFn.apply(pluginContext, [result, ...args]); | ||
} | ||
} | ||
return result; | ||
} else { | ||
throw new Error(`Invalid hook type: ${type}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { z } from 'zod'; | ||
|
||
export const PluginSchema = z.object({ | ||
enforce: z.enum(['pre', 'post']).optional(), | ||
name: z.string().optional(), | ||
buildStart: z | ||
.function( | ||
z.tuple([z.object({ command: z.string() })]), | ||
z.union([z.any(), z.promise(z.any())]), | ||
) | ||
.optional(), | ||
buildEnd: z | ||
.function( | ||
z.tuple([z.object({ command: z.string(), err: z.any().optional() })]), | ||
z.union([z.any(), z.promise(z.any())]), | ||
) | ||
.optional(), | ||
config: z | ||
.function( | ||
// TODO: fix z.any() | ||
z.tuple([z.any(), z.object({ command: z.string() })]), | ||
z.union([z.any(), z.promise(z.any()), z.null()]), | ||
) | ||
.optional(), | ||
}); | ||
|
||
export type Plugin = z.infer<typeof PluginSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type yargsParser from 'yargs-parser'; | ||
import type { Config } from '../config'; | ||
import type { PluginManager } from '../plugin/plugin_manager'; | ||
import type { Plugin } from '../plugin/types'; | ||
|
||
interface ContextPaths { | ||
tmpPath: string; | ||
} | ||
|
||
// TODO: fix any | ||
interface PluginContext { | ||
command: string | undefined; | ||
config: Config; | ||
cwd: string; | ||
userConfig: Config; | ||
} | ||
|
||
export enum Mode { | ||
Development = 'development', | ||
Production = 'production', | ||
} | ||
|
||
export interface Context { | ||
argv: yargsParser.Arguments; | ||
config: Config; | ||
pluginManager: PluginManager<Plugin>; | ||
pluginContext: PluginContext; | ||
cwd: string; | ||
mode: Mode; | ||
paths: ContextPaths; | ||
} |