Skip to content

Commit

Permalink
feat(modular-station): Expose simpler global hooks API (#124)
Browse files Browse the repository at this point in the history
Co-authored-by: Florian Lefebvre <69633530+florian-lefebvre@users.noreply.github.com>
  • Loading branch information
Fryuni and florian-lefebvre committed Jul 8, 2024
1 parent 1ba729c commit fe6f703
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-spoons-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@inox-tools/content-utils': patch
---

Simplify internal hook wiring using Modular Station's global hooks
7 changes: 7 additions & 0 deletions .changeset/rich-experts-exist.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 43 additions & 0 deletions docs/src/content/docs/modular-station/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions examples/content-injection/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
);
Expand Down
19 changes: 5 additions & 14 deletions packages/content-utils/src/integration/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -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),
Expand All @@ -103,7 +94,7 @@ export const integration = withApi(
},
},
...api,
});
};
},
})
);
Expand Down
12 changes: 3 additions & 9 deletions packages/content-utils/src/runtime/git.ts
Original file line number Diff line number Diff line change
@@ -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 = '';

Expand All @@ -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
*/
Expand Down Expand Up @@ -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,
Expand All @@ -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
*/
Expand All @@ -80,7 +74,7 @@ export async function listGitTrackedFiles(): Promise<string[]> {
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),
Expand Down
4 changes: 4 additions & 0 deletions packages/modular-station/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./hooks": {
"types": "./dist/globalHooks.d.ts",
"default": "./dist/globalHooks.js"
}
},
"files": [
Expand Down
28 changes: 28 additions & 0 deletions packages/modular-station/src/globalHooks.ts
Original file line number Diff line number Diff line change
@@ -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);
},
};
46 changes: 45 additions & 1 deletion packages/modular-station/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -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> = F extends (...params: infer P) => any
? (...params: P) => Promise<void> | void
Expand Down Expand Up @@ -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);
};

0 comments on commit fe6f703

Please sign in to comment.