1/**
2 * Plugin runner: executes lifecycle hooks and transform hooks in order.
3 */
4
5import type { Block, Collection, Entry, Site } from "../core/model";
6import type { LoadedPlugin, PluginContext } from "./types";
7import Handlebars from "handlebars";
8
9const logError = (logger: PluginContext["logger"], pluginId: string, hook: string, error: unknown) => {
10 logger.error("plugin.hookError", { pluginId, hook, error: String(error) });
11};
12
13// Lifecycle hooks onInit/onScan/onResolved
14export async function runOnInit(plugins: LoadedPlugin[], ctx: PluginContext) {
15 for (const plugin of plugins) {
16 if (!plugin.onInit) continue;
17 try {
18 await plugin.onInit({ ...ctx, pluginId: plugin.pluginId });
19 } catch (err) {
20 logError(ctx.logger, plugin.pluginId, "onInit", err);
21 }
22 }
23}
24
25export async function runOnScan(plugins: LoadedPlugin[], site: Site, ctx: PluginContext): Promise<Site> {
26 let current = site;
27 for (const plugin of plugins) {
28 if (!plugin.onScan) continue;
29 try {
30 const result = await plugin.onScan(current, { ...ctx, pluginId: plugin.pluginId });
31 if (result && typeof result === "object") {
32 current = result as Site;
33 }
34 } catch (err) {
35 logError(ctx.logger, plugin.pluginId, "onScan", err);
36 }
37 }
38 return current;
39}
40
41export async function runOnResolved(plugins: LoadedPlugin[], site: Site, ctx: PluginContext): Promise<Site> {
42 let current = site;
43 for (const plugin of plugins) {
44 if (!plugin.onResolved) continue;
45 try {
46 const result = await plugin.onResolved(current, { ...ctx, pluginId: plugin.pluginId });
47 if (result && typeof result === "object") {
48 current = result as Site;
49 }
50 } catch (err) {
51 logError(ctx.logger, plugin.pluginId, "onResolved", err);
52 }
53 }
54 return current;
55}
56
57export async function runTransformBlocks(
58 plugins: LoadedPlugin[],
59 collection: Collection,
60 entry: Entry,
61 ctx: PluginContext
62): Promise<Block[]> {
63 // Depth-first transform with recursion on subBlocks
64 const transformBlock = async (blk: Block): Promise<Block> => {
65 let current = blk;
66 for (const plugin of plugins) {
67 if (!plugin.transformBlock) continue;
68 try {
69 const res = await plugin.transformBlock(current, {
70 ...ctx,
71 entry,
72 collection,
73 pluginId: plugin.pluginId,
74 });
75 if (res && typeof res === "object") {
76 current = res as Block;
77 }
78 } catch (err) {
79 logError(ctx.logger, plugin.pluginId, "transformBlock", err);
80 }
81 }
82
83 if (current.subBlocks && current.subBlocks.length) {
84 const updatedSubs = await Promise.all(current.subBlocks.map(transformBlock));
85 current = { ...current, subBlocks: updatedSubs };
86 }
87
88 return current;
89 };
90
91 return Promise.all(entry.blocks.map(transformBlock));
92}
93
94export async function runTransformEntry(
95 plugins: LoadedPlugin[],
96 collection: Collection,
97 entry: Entry,
98 ctx: PluginContext
99): Promise<Entry> {
100 let current = entry;
101 for (const plugin of plugins) {
102 if (!plugin.transformEntry) continue;
103 try {
104 const res = await plugin.transformEntry(current, {
105 ...ctx,
106 collection,
107 pluginId: plugin.pluginId,
108 });
109 if (res && typeof res === "object") {
110 current = res as Entry;
111 }
112 } catch (err) {
113 logError(ctx.logger, plugin.pluginId, "transformEntry", err);
114 }
115 }
116 return current;
117}
118
119export async function runExtendTemplates(plugins: LoadedPlugin[], ctx: PluginContext) {
120 for (const plugin of plugins) {
121 if (!plugin.extendTemplates) continue;
122 try {
123 await plugin.extendTemplates(Handlebars, { ...ctx, pluginId: plugin.pluginId });
124 } catch (err) {
125 logError(ctx.logger, plugin.pluginId, "extendTemplates", err);
126 }
127 }
128}