diff --git a/.changeset/olive-rabbits-beg.md b/.changeset/olive-rabbits-beg.md new file mode 100644 index 00000000..ed914d6d --- /dev/null +++ b/.changeset/olive-rabbits-beg.md @@ -0,0 +1,5 @@ +--- +"astro-integration-kit": minor +--- + +Adds a new `defineAllHooksPlugin` helper for plugins that provide APIs for any hook, including third-party hooks. diff --git a/docs/src/content/docs/core/define-plugin.mdx b/docs/src/content/docs/core/define-plugin.mdx index 49859a33..c9aa9a63 100644 --- a/docs/src/content/docs/core/define-plugin.mdx +++ b/docs/src/content/docs/core/define-plugin.mdx @@ -113,6 +113,27 @@ export default defineIntegration({ }) ``` +## `defineAllHooksPlugin` + +`defineAllHooksPlugin` is a variation of `definePlugin` for when you want to provide the same API for every hook defined in the consumer integration. + +Using this function has an advantange because it allows your plugin to provide an API even for hooks it doesn't know about, like any Astro hook added after the plugin release or hooks added by integrations like `@astrojs/db`. + +Instead of providing a `setup` function returning a map of your API factory for each hook, you provide a function that receives a hook name and returns the API factory for that hook. + +```ts +defineAllHooksPlugin({ + name: "my-plugin", + setup({ name }) { + return (hookName) => (hookParameters) => ({ + doSomething: () => { + // ... + } + }); + } +}) +``` + ## Limitations 1. Plugins support overrides. That means that if 2 plugins declare the same `name`, the latest will be kept. diff --git a/package/src/core/define-all-hooks-plugin.ts b/package/src/core/define-all-hooks-plugin.ts new file mode 100644 index 00000000..523ed841 --- /dev/null +++ b/package/src/core/define-all-hooks-plugin.ts @@ -0,0 +1,41 @@ +import { definePlugin } from "./define-plugin.js"; +import type { Hooks, Plugin } from "./types.js"; + +export type AllHooksPluginDefinition> = { + name: TName; + setup: (...params: Parameters['setup']>) => + (hookName: H) => + (...hookParams: Parameters) => TApi; +}; + +/** + * A plugin that exposes the same API for all hooks. + */ +export type AllHooksPlugin> = Plugin< + TName, + Record +>; + +/** + * Allows defining a type-safe plugin that can be used from any Astro hook. + * + * This wraps {@link definePlugin} and receives a factory for the API to be + * called dynamically for each hook. This allows plugins to support any hook + * even those added by new versions of astro or hooks added by other integrations. + * + * @see https://astro-integration-kit.netlify.app/utilities/define-plugin/ + */ +export const defineAllHooksPlugin = >( + plugin: AllHooksPluginDefinition +): AllHooksPlugin => + definePlugin({ + ...plugin, + setup: (...params) => { + const hookFactory = plugin.setup(...params); + + return new Proxy(Object.freeze({}) as ReturnType['setup']>, { + has: (_, prop) => typeof prop === 'string', + get: (_, prop) => hookFactory(prop as keyof Hooks), + }); + }, + }); diff --git a/package/src/core/index.ts b/package/src/core/index.ts index fb3bea4b..87e8ce99 100644 --- a/package/src/core/index.ts +++ b/package/src/core/index.ts @@ -1,6 +1,7 @@ export { createResolver } from "./create-resolver.js"; export { defineIntegration } from "./define-integration.js"; export { definePlugin } from "./define-plugin.js"; +export { defineAllHooksPlugin } from "./define-all-hooks-plugin.js"; export { defineUtility } from "./define-utility.js"; export { withPlugins } from "./with-plugins.js"; export * from "./types.js";