mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(plugin): ✨ Add dynamic plugin and manifest loader
This commit is contained in:
parent
f623f2c1a0
commit
d224d7b9b8
41
app.ts
41
app.ts
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { join } from "node:path";
|
||||||
import { handleZodError } from "@/api";
|
import { handleZodError } from "@/api";
|
||||||
|
import { configureLoggers } from "@/loggers";
|
||||||
import { sentry } from "@/sentry";
|
import { sentry } from "@/sentry";
|
||||||
import { cors } from "@hono/hono/cors";
|
import { cors } from "@hono/hono/cors";
|
||||||
import { createMiddleware } from "@hono/hono/factory";
|
import { createMiddleware } from "@hono/hono/factory";
|
||||||
|
|
@ -8,9 +10,11 @@ import { swaggerUI } from "@hono/swagger-ui";
|
||||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||||
/* import { prometheus } from "@hono/prometheus";
|
/* import { prometheus } from "@hono/prometheus";
|
||||||
*/ import { getLogger } from "@logtape/logtape";
|
*/ import { getLogger } from "@logtape/logtape";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import type { ValidationError } from "zod-validation-error";
|
||||||
import pkg from "~/package.json" with { type: "application/json" };
|
import pkg from "~/package.json" with { type: "application/json" };
|
||||||
import { config } from "~/packages/config-manager/index";
|
import { config } from "~/packages/config-manager/index";
|
||||||
import plugin from "~/plugins/openid";
|
import { PluginLoader } from "./classes/plugin/loader";
|
||||||
import { agentBans } from "./middlewares/agent-bans";
|
import { agentBans } from "./middlewares/agent-bans";
|
||||||
import { bait } from "./middlewares/bait";
|
import { bait } from "./middlewares/bait";
|
||||||
import { boundaryCheck } from "./middlewares/boundary-check";
|
import { boundaryCheck } from "./middlewares/boundary-check";
|
||||||
|
|
@ -20,6 +24,7 @@ import { routes } from "./routes";
|
||||||
import type { ApiRouteExports, HonoEnv } from "./types/api";
|
import type { ApiRouteExports, HonoEnv } from "./types/api";
|
||||||
|
|
||||||
export const appFactory = async () => {
|
export const appFactory = async () => {
|
||||||
|
await configureLoggers();
|
||||||
const serverLogger = getLogger("server");
|
const serverLogger = getLogger("server");
|
||||||
|
|
||||||
const app = new OpenAPIHono<HonoEnv>({
|
const app = new OpenAPIHono<HonoEnv>({
|
||||||
|
|
@ -110,11 +115,35 @@ export const appFactory = async () => {
|
||||||
route.default(app);
|
route.default(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error We check if the keys are valid before this is called
|
serverLogger.info`Loading plugins`;
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: loadConfig is a private method
|
|
||||||
plugin["_loadConfig"](config.oidc);
|
const loader = new PluginLoader();
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: AddToApp is a private method
|
|
||||||
plugin["_addToApp"](app);
|
const plugins = await loader.loadPlugins(join(process.cwd(), "plugins"));
|
||||||
|
|
||||||
|
for (const data of plugins) {
|
||||||
|
serverLogger.info`Loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} ${chalk.gray(`[${plugins.indexOf(data) + 1}/${plugins.length}]`)}`;
|
||||||
|
try {
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: loadConfig is a private method
|
||||||
|
await data.plugin["_loadConfig"](
|
||||||
|
config.plugins?.[data.manifest.name],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
serverLogger.fatal`Plugin configuration is invalid: ${chalk.redBright(e as ValidationError)}`;
|
||||||
|
serverLogger.fatal`Put your configuration at ${chalk.blueBright(
|
||||||
|
"plugins.<plugin-name>",
|
||||||
|
)}`;
|
||||||
|
serverLogger.fatal`Press Ctrl+C to exit`;
|
||||||
|
|
||||||
|
// Hang until Ctrl+C is pressed
|
||||||
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: AddToApp is a private method
|
||||||
|
await data.plugin["_addToApp"](app);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverLogger.info`Plugins loaded`;
|
||||||
|
|
||||||
app.doc31("/openapi.json", {
|
app.doc31("/openapi.json", {
|
||||||
openapi: "3.1.0",
|
openapi: "3.1.0",
|
||||||
|
|
|
||||||
226
classes/plugin/loader.test.ts
Normal file
226
classes/plugin/loader.test.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
jest,
|
||||||
|
mock,
|
||||||
|
test,
|
||||||
|
} from "bun:test";
|
||||||
|
import { ZodError, type ZodTypeAny, z } from "zod";
|
||||||
|
import { Plugin, PluginConfigManager } from "~/packages/plugin-kit";
|
||||||
|
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
||||||
|
import { PluginLoader } from "./loader";
|
||||||
|
|
||||||
|
const mockReaddir = jest.fn();
|
||||||
|
const mockGetLogger = jest.fn(() => ({
|
||||||
|
fatal: jest.fn(),
|
||||||
|
}));
|
||||||
|
const mockParseJSON5 = jest.fn();
|
||||||
|
const mockParseJSONC = jest.fn();
|
||||||
|
const mockFromZodError = jest.fn();
|
||||||
|
|
||||||
|
mock.module("node:fs/promises", () => ({
|
||||||
|
readdir: mockReaddir,
|
||||||
|
}));
|
||||||
|
|
||||||
|
mock.module("@logtape/logtape", () => ({
|
||||||
|
getLogger: mockGetLogger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
mock.module("confbox", () => ({
|
||||||
|
parseJSON5: mockParseJSON5,
|
||||||
|
parseJSONC: mockParseJSONC,
|
||||||
|
}));
|
||||||
|
|
||||||
|
mock.module("zod-validation-error", () => ({
|
||||||
|
fromZodError: mockFromZodError,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("PluginLoader", () => {
|
||||||
|
let pluginLoader: PluginLoader;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pluginLoader = new PluginLoader();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getDirectories should return directories", async () => {
|
||||||
|
mockReaddir.mockResolvedValue([
|
||||||
|
{ name: "dir1", isDirectory: () => true },
|
||||||
|
{ name: "file1", isDirectory: () => false },
|
||||||
|
{ name: "dir2", isDirectory: () => true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
const directories = await pluginLoader["getDirectories"]("/some/path");
|
||||||
|
expect(directories).toEqual(["dir1", "dir2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findManifestFile should return manifest file if found", async () => {
|
||||||
|
mockReaddir.mockResolvedValue(["manifest.json", "otherfile.txt"]);
|
||||||
|
|
||||||
|
const manifestFile =
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
await pluginLoader["findManifestFile"]("/some/path");
|
||||||
|
expect(manifestFile).toBe("manifest.json");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hasEntrypoint should return true if entrypoint file is found", async () => {
|
||||||
|
mockReaddir.mockResolvedValue(["index.ts", "otherfile.txt"]);
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
const hasEntrypoint = await pluginLoader["hasEntrypoint"]("/some/path");
|
||||||
|
expect(hasEntrypoint).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseManifestFile should parse JSON manifest", async () => {
|
||||||
|
const manifestContent = { name: "test-plugin" };
|
||||||
|
Bun.file = jest.fn().mockReturnValue({
|
||||||
|
text: async () => JSON.stringify(manifestContent),
|
||||||
|
});
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
const manifest = await pluginLoader["parseManifestFile"](
|
||||||
|
"/some/path/manifest.json",
|
||||||
|
"manifest.json",
|
||||||
|
);
|
||||||
|
expect(manifest).toEqual(manifestContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findPlugins should return plugin directories with valid manifest and entrypoint", async () => {
|
||||||
|
mockReaddir
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "plugin1", isDirectory: () => true },
|
||||||
|
{ name: "plugin2", isDirectory: () => true },
|
||||||
|
])
|
||||||
|
.mockResolvedValue(["manifest.json", "index.ts"]);
|
||||||
|
|
||||||
|
const plugins = await pluginLoader.findPlugins("/some/path");
|
||||||
|
expect(plugins).toEqual(["plugin1", "plugin2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseManifest should parse and validate manifest", async () => {
|
||||||
|
const manifestContent: Manifest = {
|
||||||
|
name: "test-plugin",
|
||||||
|
version: "1.1.0",
|
||||||
|
description: "Doobaee",
|
||||||
|
};
|
||||||
|
mockReaddir.mockResolvedValue(["manifest.json"]);
|
||||||
|
Bun.file = jest.fn().mockReturnValue({
|
||||||
|
text: async () => JSON.stringify(manifestContent),
|
||||||
|
});
|
||||||
|
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: manifestContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifest = await pluginLoader.parseManifest(
|
||||||
|
"/some/path",
|
||||||
|
"plugin1",
|
||||||
|
);
|
||||||
|
expect(manifest).toEqual(manifestContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseManifest should throw error if manifest is missing", async () => {
|
||||||
|
mockReaddir.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
pluginLoader.parseManifest("/some/path", "plugin1"),
|
||||||
|
).rejects.toThrow("Plugin plugin1 is missing a manifest file");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseManifest should throw error if manifest is invalid", async () => {
|
||||||
|
// @ts-expect-error trying to cause a type error here
|
||||||
|
const manifestContent: Manifest = {
|
||||||
|
name: "test-plugin",
|
||||||
|
version: "1.1.0",
|
||||||
|
};
|
||||||
|
mockReaddir.mockResolvedValue(["manifest.json"]);
|
||||||
|
Bun.file = jest.fn().mockReturnValue({
|
||||||
|
text: async () => JSON.stringify(manifestContent),
|
||||||
|
});
|
||||||
|
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
pluginLoader.parseManifest("/some/path", "plugin1"),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadPlugin should load and return a Plugin instance", async () => {
|
||||||
|
const mockPlugin = new Plugin(
|
||||||
|
{
|
||||||
|
name: "test-plugin",
|
||||||
|
version: "1.1.0",
|
||||||
|
description: "Doobaee",
|
||||||
|
},
|
||||||
|
new PluginConfigManager(z.object({})),
|
||||||
|
);
|
||||||
|
mock.module("/some/path/index.ts", () => ({
|
||||||
|
default: mockPlugin,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const plugin = await pluginLoader.loadPlugin("/some/path", "index.ts");
|
||||||
|
expect(plugin).toBeInstanceOf(Plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadPlugin should throw error if default export is not a Plugin", async () => {
|
||||||
|
mock.module("/some/path/index.ts", () => ({
|
||||||
|
default: "cheese",
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
pluginLoader.loadPlugin("/some/path", "index.ts"),
|
||||||
|
).rejects.toThrow("Entrypoint is not a Plugin");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadPlugins should load all plugins in a directory", async () => {
|
||||||
|
const manifestContent: Manifest = {
|
||||||
|
name: "test-plugin",
|
||||||
|
version: "1.1.0",
|
||||||
|
description: "Doobaee",
|
||||||
|
};
|
||||||
|
const mockPlugin = new Plugin(
|
||||||
|
manifestContent,
|
||||||
|
new PluginConfigManager(z.object({})),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockReaddir
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ name: "plugin1", isDirectory: () => true },
|
||||||
|
{ name: "plugin2", isDirectory: () => true },
|
||||||
|
])
|
||||||
|
.mockResolvedValue(["manifest.json", "index.ts"]);
|
||||||
|
Bun.file = jest.fn().mockReturnValue({
|
||||||
|
text: async () => JSON.stringify(manifestContent),
|
||||||
|
});
|
||||||
|
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: manifestContent,
|
||||||
|
});
|
||||||
|
mock.module("/some/path/plugin1/index.ts", () => ({
|
||||||
|
default: mockPlugin,
|
||||||
|
}));
|
||||||
|
mock.module("/some/path/plugin2/index.ts", () => ({
|
||||||
|
default: mockPlugin,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const plugins = await pluginLoader.loadPlugins("/some/path");
|
||||||
|
expect(plugins).toEqual([
|
||||||
|
{
|
||||||
|
manifest: manifestContent,
|
||||||
|
plugin: mockPlugin as unknown as Plugin<ZodTypeAny>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manifest: manifestContent,
|
||||||
|
plugin: mockPlugin as unknown as Plugin<ZodTypeAny>,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
176
classes/plugin/loader.ts
Normal file
176
classes/plugin/loader.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { readdir } from "node:fs/promises";
|
||||||
|
import { getLogger } from "@logtape/logtape";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { parseJSON5, parseJSONC } from "confbox";
|
||||||
|
import type { ZodTypeAny } from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { Plugin } from "~/packages/plugin-kit/plugin";
|
||||||
|
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage plugins.
|
||||||
|
*/
|
||||||
|
export class PluginLoader {
|
||||||
|
private logger = getLogger("plugin");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all directories in a given directory.
|
||||||
|
* @param {string} dir - The directory to search.
|
||||||
|
* @returns {Promise<string[]>} - An array of directory names.
|
||||||
|
*/
|
||||||
|
private async getDirectories(dir: string): Promise<string[]> {
|
||||||
|
const files = await readdir(dir, { withFileTypes: true });
|
||||||
|
return files.filter((f) => f.isDirectory()).map((f) => f.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the manifest file in a given directory.
|
||||||
|
* @param {string} dir - The directory to search.
|
||||||
|
* @returns {Promise<string | undefined>} - The manifest file name if found, otherwise undefined.
|
||||||
|
*/
|
||||||
|
private async findManifestFile(dir: string): Promise<string | undefined> {
|
||||||
|
const files = await readdir(dir);
|
||||||
|
return files.find((f) => f.match(/^manifest\.(json|json5|jsonc)$/));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory has an entrypoint file (index.ts).
|
||||||
|
* @param {string} dir - The directory to search.
|
||||||
|
* @returns {Promise<boolean>} - True if the entrypoint file is found, otherwise false.
|
||||||
|
*/
|
||||||
|
private async hasEntrypoint(dir: string): Promise<boolean> {
|
||||||
|
const files = await readdir(dir);
|
||||||
|
return files.includes("index.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the manifest file based on its type.
|
||||||
|
* @param {string} manifestPath - The path to the manifest file.
|
||||||
|
* @param {string} manifestFile - The manifest file name.
|
||||||
|
* @returns {Promise<unknown>} - The parsed manifest content.
|
||||||
|
* @throws Will throw an error if the manifest file cannot be parsed.
|
||||||
|
*/
|
||||||
|
private async parseManifestFile(
|
||||||
|
manifestPath: string,
|
||||||
|
manifestFile: string,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const manifestText = await Bun.file(manifestPath).text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (manifestFile.endsWith(".json")) {
|
||||||
|
return JSON.parse(manifestText);
|
||||||
|
}
|
||||||
|
if (manifestFile.endsWith(".json5")) {
|
||||||
|
return parseJSON5(manifestText);
|
||||||
|
}
|
||||||
|
if (manifestFile.endsWith(".jsonc")) {
|
||||||
|
return parseJSONC(manifestText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger
|
||||||
|
.fatal`Could not parse plugin manifest ${chalk.blue(manifestPath)} as ${manifestFile.split(".").pop()?.toUpperCase()}.`;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all direct subdirectories with a valid manifest file and entrypoint (index.ts).
|
||||||
|
* @param {string} dir - The directory to search.
|
||||||
|
* @returns {Promise<string[]>} - An array of plugin directories.
|
||||||
|
*/
|
||||||
|
public async findPlugins(dir: string): Promise<string[]> {
|
||||||
|
const directories = await this.getDirectories(dir);
|
||||||
|
const plugins: string[] = [];
|
||||||
|
|
||||||
|
for (const directory of directories) {
|
||||||
|
const manifestFile = await this.findManifestFile(
|
||||||
|
`${dir}/${directory}`,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
manifestFile &&
|
||||||
|
(await this.hasEntrypoint(`${dir}/${directory}`))
|
||||||
|
) {
|
||||||
|
plugins.push(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the manifest file of a plugin.
|
||||||
|
* @param {string} dir - The directory containing the plugin.
|
||||||
|
* @param {string} plugin - The plugin directory name.
|
||||||
|
* @returns {Promise<Manifest>} - The parsed manifest object.
|
||||||
|
* @throws Will throw an error if the manifest file is missing or invalid.
|
||||||
|
*/
|
||||||
|
public async parseManifest(dir: string, plugin: string): Promise<Manifest> {
|
||||||
|
const manifestFile = await this.findManifestFile(`${dir}/${plugin}`);
|
||||||
|
|
||||||
|
if (!manifestFile) {
|
||||||
|
throw new Error(`Plugin ${plugin} is missing a manifest file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestPath = `${dir}/${plugin}/${manifestFile}`;
|
||||||
|
const manifest = await this.parseManifestFile(
|
||||||
|
manifestPath,
|
||||||
|
manifestFile,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await manifestSchema.safeParseAsync(manifest);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
this.logger
|
||||||
|
.fatal`Plugin manifest ${chalk.blue(manifestPath)} is invalid.`;
|
||||||
|
throw fromZodError(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an entrypoint's default export and check if it's a Plugin.
|
||||||
|
* @param {string} dir - The directory containing the entrypoint.
|
||||||
|
* @param {string} entrypoint - The entrypoint file name.
|
||||||
|
* @returns {Promise<Plugin<ZodTypeAny>>} - The loaded Plugin instance.
|
||||||
|
* @throws Will throw an error if the entrypoint's default export is not a Plugin.
|
||||||
|
*/
|
||||||
|
public async loadPlugin(
|
||||||
|
dir: string,
|
||||||
|
entrypoint: string,
|
||||||
|
): Promise<Plugin<ZodTypeAny>> {
|
||||||
|
const plugin = (await import(`${dir}/${entrypoint}`)).default;
|
||||||
|
|
||||||
|
if (plugin instanceof Plugin) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger
|
||||||
|
.fatal`Default export of entrypoint ${chalk.blue(entrypoint)} at ${chalk.blue(dir)} is not a Plugin.`;
|
||||||
|
|
||||||
|
throw new Error("Entrypoint is not a Plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all plugins in a given directory.
|
||||||
|
* @param {string} dir - The directory to search.
|
||||||
|
* @returns An array of objects containing the manifest and plugin instance.
|
||||||
|
*/
|
||||||
|
public async loadPlugins(
|
||||||
|
dir: string,
|
||||||
|
): Promise<{ manifest: Manifest; plugin: Plugin<ZodTypeAny> }[]> {
|
||||||
|
const plugins = await this.findPlugins(dir);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
plugins.map(async (plugin) => {
|
||||||
|
const manifest = await this.parseManifest(dir, plugin);
|
||||||
|
const pluginInstance = await this.loadPlugin(
|
||||||
|
dir,
|
||||||
|
`${plugin}/index.ts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { manifest, plugin: pluginInstance };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4006,6 +4006,10 @@
|
||||||
"default": {
|
"default": {
|
||||||
"federation": false
|
"federation": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"cli-table": "^0.3.11",
|
"cli-table": "^0.3.11",
|
||||||
|
"confbox": "^0.1.7",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"hono": "npm:@jsr/hono__hono@4.6.2",
|
"hono": "npm:@jsr/hono__hono@4.6.2",
|
||||||
|
|
|
||||||
|
|
@ -642,6 +642,7 @@ export const configValidator = z.object({
|
||||||
.default({
|
.default({
|
||||||
federation: false,
|
federation: false,
|
||||||
}),
|
}),
|
||||||
|
plugins: z.record(z.string(), z.any()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Config = z.infer<typeof configValidator>;
|
export type Config = z.infer<typeof configValidator>;
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@ export class Plugin<ConfigSchema extends z.ZodTypeAny> {
|
||||||
* This will be called when the plugin is loaded.
|
* This will be called when the plugin is loaded.
|
||||||
* @param config Values the user has set in the configuration file.
|
* @param config Values the user has set in the configuration file.
|
||||||
*/
|
*/
|
||||||
protected _loadConfig(config: z.input<ConfigSchema>) {
|
protected _loadConfig(config: z.input<ConfigSchema>): Promise<void> {
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
this.configManager["_load"](config);
|
return this.configManager["_load"](config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _addToApp(app: OpenAPIHono<HonoEnv>) {
|
protected _addToApp(app: OpenAPIHono<HonoEnv>) {
|
||||||
|
|
@ -117,7 +117,7 @@ export class PluginConfigManager<Schema extends z.ZodTypeAny> {
|
||||||
try {
|
try {
|
||||||
this.store = await this.schema.parseAsync(config);
|
this.store = await this.schema.parseAsync(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw fromZodError(error as ZodError);
|
throw fromZodError(error as ZodError).message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,5 +201,10 @@ export const configureLoggers = (silent = false) =>
|
||||||
category: ["logtape", "meta"],
|
category: ["logtape", "meta"],
|
||||||
level: "error",
|
level: "error",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
category: "plugin",
|
||||||
|
sinks: ["console", "file"],
|
||||||
|
filters: ["configFilter"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue