From fe6f703a27c6dd8d49c73a64f5d84579a7cb6d11 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Mon, 8 Jul 2024 11:15:23 -0300 Subject: [PATCH] feat(modular-station): Expose simpler global hooks API (#124) Co-authored-by: Florian Lefebvre <69633530+florian-lefebvre@users.noreply.github.com> --- .changeset/bright-spoons-perform.md | 5 ++ .changeset/rich-experts-exist.md | 7 +++ .../src/content/docs/modular-station/hooks.md | 43 +++++++++++++++++ examples/content-injection/integration.ts | 4 +- .../content-utils/src/integration/index.ts | 19 ++------ packages/content-utils/src/runtime/git.ts | 12 ++--- packages/modular-station/package.json | 4 ++ packages/modular-station/src/globalHooks.ts | 28 +++++++++++ packages/modular-station/src/hooks.ts | 46 ++++++++++++++++++- 9 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 .changeset/bright-spoons-perform.md create mode 100644 .changeset/rich-experts-exist.md create mode 100644 packages/modular-station/src/globalHooks.ts diff --git a/.changeset/bright-spoons-perform.md b/.changeset/bright-spoons-perform.md new file mode 100644 index 00000000..4527acee --- /dev/null +++ b/.changeset/bright-spoons-perform.md @@ -0,0 +1,5 @@ +--- +'@inox-tools/content-utils': patch +--- + +Simplify internal hook wiring using Modular Station's global hooks diff --git a/.changeset/rich-experts-exist.md b/.changeset/rich-experts-exist.md new file mode 100644 index 00000000..b035a772 --- /dev/null +++ b/.changeset/rich-experts-exist.md @@ -0,0 +1,7 @@ +--- +'@inox-tools/modular-station': patch +--- + +Add a global hook API for custom hooks without using an AIK plugin. + +This allows for simpler and more intuitive implementations of hooks outside of main lifecycle in the implementations of official Astro hooks. diff --git a/docs/src/content/docs/modular-station/hooks.md b/docs/src/content/docs/modular-station/hooks.md index beef51d5..8f3b15a0 100644 --- a/docs/src/content/docs/modular-station/hooks.md +++ b/docs/src/content/docs/modular-station/hooks.md @@ -52,8 +52,49 @@ declare global { } ``` +### Registering and triggering hooks + +Source integrations can trigger hooks using the global hooks API. To do so, they must first call the `registerGlobalHooks` function as early as possible on their `astro:config:setup` hook with the hook parameters. + +```ts title="source-integration/index.ts" ins={1,7} +import { registerGlobalHooks } from '@inox-tools/modular-station'; + +export function (): AstroIntegration => ({ + name: 'source-integration', + hooks: { + 'astro:config:setup': (params) => { + registerGlobalHooks(params); + } + } +}); +``` + +With that in place the hooks can be triggered by the `hooks` API exported from `@inox-tools/modular-station/hooks`: + +```ts title="source-integration/some-module.ts" +import { hooks } from '@inox-tools/modular-station/hooks'; + +hooks.run( + // Hook name + 'source:integration:hook', + // Callback to make the arguments for each target integration + // Receives the logger for the target integration + (logger) => ['param', { from: 'source' }, logger] +); +``` + +The global hooks API can be called from anywhere, including integration code, virtual modules, normal project files and TS modules. + +Calling the hooks API from the server runtime or from client-side code is a no-op. The observed behavior is the same as if the hook wasn't implemented by any integration, but no error will be thrown. If you want to detect when and where you code is running, you can use the [Astro When](/astro-when) Inox Tool. + ### Hook provider plugin +:::caution[For advanced authors] +Using the hooks provider instead of the global hooks trigger provides advanced control without any inherit global state, but requires you to manage the transferring of function references between integration code running at config time and module code running at build/render time inside of bundled code. + +If you are not familiar with the shared module graph between the Astro builder, integration code, virtual modules and Astro project files, you should use the [global hooks triggering](#registering-and-triggering-hooks). +::: + Source integrations can trigger hooks using the Hook Provider Plugin, which does the heavy lifting of: - Properly collecting the target integrations in use on the Astro project, including integrations added dynamically; @@ -62,6 +103,8 @@ Source integrations can trigger hooks using the Hook Provider Plugin, which does The Hook Provider Plugin is a special kind of [Astro Integration Kit plugin](https://astro-integration-kit.netlify.app/core/with-plugins/) that provides the most effective implementation of the hook triggering mechanism for _all_ your hooks, even other custom hooks in case your source integration is a target of some other integration. +The `hooks` property injected by this plugin has the same API as the [global hooks API](#registering-and-triggering-hooks). + ```ts title="source-integration/index.ts" ins={2,9,12-18} import { defineIntegration, withPlugins } from 'astro-integration-kit'; import { hookProviderPlugin } from '@inox-tools/modular-station'; diff --git a/examples/content-injection/integration.ts b/examples/content-injection/integration.ts index 07c340d4..d5a1fac3 100644 --- a/examples/content-injection/integration.ts +++ b/examples/content-injection/integration.ts @@ -12,10 +12,10 @@ export default defineIntegration({ seedTemplateDirectory: './src/integration', }); }, - '@it-astro:content:gitTrackedListResolved': ({ trackedFiles, ignoreFiles, logger }) => { + '@it/content:git:listed': ({ trackedFiles, ignoreFiles, logger }) => { logger.info('Content utils tracking files: ' + trackedFiles); }, - '@it-astro:content:gitCommitResolved': ({ file, age, resolvedDate, logger }) => { + '@it/content:git:resolved': ({ file, age, resolvedDate, logger }) => { logger.warn( `Content utils resolved the ${age} commit date for file ${file} as: ${resolvedDate}` ); diff --git a/packages/content-utils/src/integration/index.ts b/packages/content-utils/src/integration/index.ts index 07a57ef9..a15f26d2 100644 --- a/packages/content-utils/src/integration/index.ts +++ b/packages/content-utils/src/integration/index.ts @@ -1,8 +1,8 @@ -import { withApi, onHook, hookProviderPlugin } from '@inox-tools/modular-station'; +import { withApi, onHook, registerGlobalHooks } from '@inox-tools/modular-station'; import { emptyState } from './state.js'; import { resolveContentPaths } from '../internal/resolver.js'; import { mkdirSync, writeFileSync } from 'node:fs'; -import { addVitePlugin, withPlugins, defineIntegration } from 'astro-integration-kit'; +import { addVitePlugin, defineIntegration } from 'astro-integration-kit'; import { injectorPlugin } from './injectorPlugin.js'; import { seedCollections, type SeedCollectionsOptions } from './seedCollections.js'; import { gitTimeBuildPlugin, gitTimeDevPlugin } from './gitTimePlugin.js'; @@ -61,12 +61,11 @@ export const integration = withApi( ), }; - return withPlugins({ - name, - plugins: [hookProviderPlugin], + return { hooks: { 'astro:config:setup': (params) => { state.logger = params.logger; + registerGlobalHooks(params); state.contentPaths = resolveContentPaths(params.config); @@ -83,14 +82,6 @@ export const integration = withApi( warnDuplicated: true, }); - (globalThis as any)[ - Symbol.for('@inox-tools/content-utils:triggers/gitTrackedListResolved') - ] = params.hooks.getTrigger('@it/content:git:listed'); - - (globalThis as any)[ - Symbol.for('@inox-tools/content-utils:triggers/gitCommitResolved') - ] = params.hooks.getTrigger('@it/content:git:resolved'); - addVitePlugin(params, { plugin: params.command === 'dev' ? gitTimeDevPlugin(state) : gitTimeBuildPlugin(state), @@ -103,7 +94,7 @@ export const integration = withApi( }, }, ...api, - }); + }; }, }) ); diff --git a/packages/content-utils/src/runtime/git.ts b/packages/content-utils/src/runtime/git.ts index e50cbd93..e8907587 100644 --- a/packages/content-utils/src/runtime/git.ts +++ b/packages/content-utils/src/runtime/git.ts @@ -1,6 +1,6 @@ -import type { HookTrigger } from '@inox-tools/modular-station'; import { spawnSync } from 'node:child_process'; import { basename, dirname, join, sep, resolve } from 'node:path'; +import { hooks } from '@inox-tools/modular-station/hooks'; let contentPath: string = ''; @@ -11,9 +11,6 @@ export function setContentPath(path: string) { contentPath = path; } -const getCommitResolvedHook = (): HookTrigger<'@it/content:git:resolved'> => - (globalThis as any)[Symbol.for('@inox-tools/content-utils:triggers/gitCommitResolved')]; - /** * @internal */ @@ -46,7 +43,7 @@ export async function getCommitDate(file: string, age: 'oldest' | 'latest'): Pro let resolvedDate = new Date(Number(match.groups.timestamp) * 1000); - await getCommitResolvedHook()((logger) => [ + await hooks.run('@it/content:git:resolved', (logger) => [ { logger, resolvedDate, @@ -61,9 +58,6 @@ export async function getCommitDate(file: string, age: 'oldest' | 'latest'): Pro return resolvedDate; } -const getTrackedListResolvedHook = (): HookTrigger<'@it/content:git:listed'> => - (globalThis as any)[Symbol.for('@inox-tools/content-utils:triggers/gitTrackedListResolved')]; - /** * @internal */ @@ -80,7 +74,7 @@ export async function listGitTrackedFiles(): Promise { const output = result.stdout.trim(); let files = output.split('\n'); - await getTrackedListResolvedHook()((logger) => [ + await hooks.run('@it/content:git:listed', (logger) => [ { logger, trackedFiles: Array.from(files), diff --git a/packages/modular-station/package.json b/packages/modular-station/package.json index 1c2bed06..b5ef64b8 100644 --- a/packages/modular-station/package.json +++ b/packages/modular-station/package.json @@ -13,6 +13,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./hooks": { + "types": "./dist/globalHooks.d.ts", + "default": "./dist/globalHooks.js" } }, "files": [ diff --git a/packages/modular-station/src/globalHooks.ts b/packages/modular-station/src/globalHooks.ts new file mode 100644 index 00000000..3474e77e --- /dev/null +++ b/packages/modular-station/src/globalHooks.ts @@ -0,0 +1,28 @@ +import type { AstroIntegration, AstroIntegrationLogger } from 'astro'; +import { runHook, type PluginApi } from './hooks.js'; + +let logger: AstroIntegrationLogger; +let integrations: AstroIntegration[]; + +export const setGlobal = ( + newLogger: AstroIntegrationLogger, + newIntegrations: AstroIntegration[] +) => { + logger = newLogger; + integrations = newIntegrations; +}; + +export const hooks: PluginApi['hooks'] = { + run: (hook, params) => { + if (logger === undefined || integrations === undefined || integrations.length === 0) { + return Promise.reject(new Error('Cannot run hook at this point')); + } + return runHook(integrations, logger, hook, params); + }, + getTrigger: (hook) => (params) => { + if (logger === undefined || integrations === undefined || integrations.length === 0) { + return Promise.resolve(); + } + return runHook(integrations, logger, hook, params); + }, +}; diff --git a/packages/modular-station/src/hooks.ts b/packages/modular-station/src/hooks.ts index eefa97a4..0944b582 100644 --- a/packages/modular-station/src/hooks.ts +++ b/packages/modular-station/src/hooks.ts @@ -1,6 +1,8 @@ -import type { Hooks } from 'astro-integration-kit'; +import type { HookParameters, Hooks } from 'astro-integration-kit'; import type { AstroIntegration, AstroIntegrationLogger } from 'astro'; import { DEFAULT_HOOK_FACTORY, allHooksPlugin } from './allHooksPlugin.js'; +import { Once } from '@inox-tools/utils/once'; +import { setGlobal } from './globalHooks.js'; type ToHookFunction = F extends (...params: infer P) => any ? (...params: P) => Promise | void @@ -86,3 +88,45 @@ export const hookProviderPlugin = allHooksPlugin({ }; }, }); + +const pregisterOnce = new Once(); +const globalHookIntegrationName = '@inox-tools/modular-station/global-hooks'; +const versionMarker = Symbol(globalHookIntegrationName); + +type MarkedIntegration = AstroIntegration & { + [versionMarker]: true; +}; + +export const registerGlobalHooks = (params: HookParameters<'astro:config:setup'>) => { + // Register immediately so hooks can be triggered from calls within the current hook + setGlobal(params.logger, params.config.integrations); + + if ( + params.config.integrations.some( + (i) => + i.name === globalHookIntegrationName && + // Check for a version marker so duplicate dependencies + // of incompatible versions don't conflict + versionMarker in i && + i[versionMarker] === true + ) + ) { + // Global hooks already registered + return; + } + + const integration: MarkedIntegration = { + name: globalHookIntegrationName, + [versionMarker]: true, + hooks: { + 'astro:config:setup': (params) => { + setGlobal(params.logger, params.config.integrations); + }, + 'astro:config:done': (params) => { + setGlobal(params.logger, params.config.integrations); + }, + }, + }; + + params.config.integrations.push(integration); +};