Skip to content

Commit 5f5694f

Browse files
authored
feat: add internal plugin priority and slug api for cross-plugin discovery (#16244)
Adds a temporary, internal API for plugin execution ordering and cross-plugin discovery. Plugins can now attach three optional properties to their function: - `priority` - controls execution order (ascending, default 0), so a high-priority plugin is guaranteed to see config changes from all lower-priority plugins - `slug` - unique identifier so plugins can find each other in `config.plugins` without importing each other. Useful for checking within plugin A whether plugin B is installed - `options` - exposes the plugin's closure args, allowing other plugins to mutate them before the plugin executes (e.g. injecting tools into plugin-mcp's options) Marked as `@internal` so we can start using this across our own plugins without committing to a public API yet. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1214027832433527
1 parent 0516803 commit 5f5694f

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

packages/payload/src/config/build.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import { sanitizeConfig } from './sanitize.js'
99
*/
1010
export async function buildConfig(config: Config): Promise<SanitizedConfig> {
1111
if (Array.isArray(config.plugins)) {
12-
let configAfterPlugins = config
13-
for (const plugin of config.plugins) {
14-
configAfterPlugins = await plugin(configAfterPlugins)
12+
const sorted = [...config.plugins].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
13+
14+
for (const plugin of sorted) {
15+
config = await plugin(config)
1516
}
16-
return await sanitizeConfig(configAfterPlugins)
1717
}
1818

1919
return await sanitizeConfig(config)

packages/payload/src/config/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,14 @@ type Prettify<T> = {
143143
[K in keyof T]: T[K]
144144
} & NonNullable<unknown>
145145

146-
export type Plugin = (config: Config) => Config | Promise<Config>
146+
export type Plugin = ((config: Config) => Config | Promise<Config>) & {
147+
/** @internal Plugin options exposed for cross-plugin mutation. */
148+
options?: Record<string, unknown>
149+
/** @internal Execution order - lower values run first. Defaults to 0. */
150+
priority?: number
151+
/** @internal Unique identifier for cross-plugin discovery via `config.plugins`. */
152+
slug?: string
153+
}
147154

148155
export type LivePreviewURLType = null | string | undefined
149156

test/plugins/config.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Config, Plugin } from 'payload'
2+
13
import { fileURLToPath } from 'node:url'
24
import path from 'path'
35

@@ -8,6 +10,58 @@ const dirname = path.dirname(filename)
810

911
export const pagesSlug = 'pages'
1012

13+
type ReaderPluginOptions = {
14+
items: Array<{ name: string }>
15+
}
16+
17+
/**
18+
* High-priority plugin that reads both its own options and config.custom.
19+
* Other plugins can inject additional items into its options via slug discovery.
20+
*/
21+
const readerPlugin = (pluginOptions: ReaderPluginOptions): Plugin => {
22+
const plugin: Plugin = (config: Config): Config => ({
23+
...config,
24+
custom: {
25+
...(config.custom || {}),
26+
readerSawValue: (config.custom?.writerValue as string) ?? null,
27+
readerItems: pluginOptions.items.map((i) => i.name),
28+
},
29+
})
30+
31+
plugin.slug = 'priority-reader'
32+
plugin.priority = 10
33+
plugin.options = pluginOptions
34+
35+
return plugin
36+
}
37+
38+
/**
39+
* Low-priority plugin that writes to config.custom and injects items
40+
* into the reader plugin's options via slug discovery.
41+
*/
42+
const writerPlugin = (): Plugin => {
43+
const plugin: Plugin = (config: Config): Config => {
44+
const reader = config.plugins?.find((p) => p.slug === 'priority-reader')
45+
if (reader?.options) {
46+
const opts = reader.options as unknown as ReaderPluginOptions
47+
opts.items.push({ name: 'injected-by-writer' })
48+
}
49+
50+
return {
51+
...config,
52+
custom: {
53+
...(config.custom || {}),
54+
writerValue: 'written-by-low-priority',
55+
},
56+
}
57+
}
58+
59+
plugin.priority = 1
60+
plugin.slug = 'priority-writer'
61+
62+
return plugin
63+
}
64+
1165
export default buildConfigWithDefaults({
1266
admin: {
1367
importMap: {
@@ -37,6 +91,9 @@ export default buildConfigWithDefaults({
3791
},
3892
],
3993
}),
94+
// Intentionally listed BEFORE the writer to verify priority sorting works
95+
readerPlugin({ items: [{ name: 'user-provided' }] }),
96+
writerPlugin(),
4097
],
4198
onInit: async (payload) => {
4299
await payload.create({

test/plugins/int.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,29 @@ describe('Collections - Plugins', () => {
3131

3232
expect(id).toBeDefined()
3333
})
34+
35+
describe('plugin priority, slug, and options', () => {
36+
it('should execute plugins sorted by priority regardless of array order', () => {
37+
// The reader (priority 10) is listed BEFORE the writer (priority 1) in the array,
38+
// but priority sorting ensures the writer runs first.
39+
expect(payload.config.custom?.readerSawValue).toBe('written-by-low-priority')
40+
})
41+
42+
it('should allow plugins to find each other by slug', () => {
43+
const reader = payload.config.plugins?.find((p) => p.slug === 'priority-reader')
44+
const writer = payload.config.plugins?.find((p) => p.slug === 'priority-writer')
45+
46+
expect(reader).toBeDefined()
47+
expect(writer).toBeDefined()
48+
})
49+
50+
it('should allow a plugin to mutate another plugin options via slug', () => {
51+
// The writer (runs first) finds the reader by slug and pushes into its options.items.
52+
// The reader (runs second) sees both the user-provided and injected items.
53+
const items = payload.config.custom?.readerItems as string[]
54+
55+
expect(items).toContain('user-provided')
56+
expect(items).toContain('injected-by-writer')
57+
})
58+
})
3459
})

0 commit comments

Comments
 (0)