From 676438b5df0ccdd8a0d22365274e5741817d194c Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Fri, 21 Jun 2024 14:11:57 -0300 Subject: [PATCH] fix: Allow plugins to initalize themselves on unused hooks (#114) --- .changeset/cool-islands-brake.md | 5 + package/src/core/with-plugins.ts | 7 +- package/tests/unit/with-plugins.spec.ts | 185 ++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 .changeset/cool-islands-brake.md create mode 100644 package/tests/unit/with-plugins.spec.ts diff --git a/.changeset/cool-islands-brake.md b/.changeset/cool-islands-brake.md new file mode 100644 index 0000000..6481a49 --- /dev/null +++ b/.changeset/cool-islands-brake.md @@ -0,0 +1,5 @@ +--- +"astro-integration-kit": patch +--- + +Fixes initialization of plugins when necessary hooks are not used by consumer integrations diff --git a/package/src/core/with-plugins.ts b/package/src/core/with-plugins.ts index 8e9560f..2331baf 100644 --- a/package/src/core/with-plugins.ts +++ b/package/src/core/with-plugins.ts @@ -56,7 +56,12 @@ export const withPlugins = < > => plugin.setup({ name }), ); - const definedHooks = Object.keys(providedHooks) as Array; + const definedHooks = ([ + ...Object.keys(providedHooks), + ...resolvedPlugins.flatMap(Object.keys), + ] as Array) + // Deduplicate the hook names + .filter((hookName, index, list) => list.indexOf(hookName) === index); const hooks: AstroIntegration["hooks"] = Object.fromEntries( definedHooks.map((hookName) => [ diff --git a/package/tests/unit/with-plugins.spec.ts b/package/tests/unit/with-plugins.spec.ts new file mode 100644 index 0000000..945bdca --- /dev/null +++ b/package/tests/unit/with-plugins.spec.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from "vitest"; +import { definePlugin } from "../../src/core/define-plugin.js"; +import { withPlugins } from "../../src/core/with-plugins.js"; +import type { AstroIntegrationLogger } from "astro"; + +describe('withPlugins', () => { + const fooPlugin = definePlugin({ + name: 'foo', + setup({ name }) { + let innerState: string = 'initial state'; + + return { + 'astro:build:start': ({ logger }) => { + logger.info(`Called from plugin "foo" on integration "${name}".`); + + return { + foo: (msg: string) => { + logger.info(`Calling "foo" with msg: ${msg}`); + }, + setState: (state: string) => { + innerState = state; + }, + }; + }, + 'astro:server:done': ({ logger }) => ({ + getState: () => { + logger.info('Reading state'); + return innerState; + }, + }), + }; + }, + }); + + const otherFooPlugin = definePlugin({ + name: 'foo', + setup({ name }) { + return { + 'astro:build:start': ({ logger }) => { + logger.info(`Called from plugin "otherFoo" on integration "${name}".`); + + return { + foo: (msg: string) => { + logger.info(`Calling "foo" (from otherFoo) with msg: ${msg}`); + } + }; + }, + }; + }, + }); + + const barPlugin = definePlugin({ + name: 'bar', + setup({ name }) { + return { + 'astro:build:start': ({ logger }) => { + logger.info(`Called from plugin "bar" on integration "${name}".`); + + return { + foo: (msg: string) => { + logger.info(`Calling "foo" (from bar) with msg: ${msg}`); + } + }; + }, + }; + }, + }); + + it('should provide the plugins API to the hooks', () => { + const integration = withPlugins({ + name: 'my-integration', + plugins: [fooPlugin], + hooks: { + 'astro:build:start': ({ foo, setState }) => { + foo('from integration'); + setState('integrationState') + }, + 'astro:server:done': ({ getState }) => { + expect(getState()).toEqual('integrationState'); + }, + }, + }); + + const logger = new MemoryLogger(); + + integration.hooks['astro:build:start']?.({ logger }); + integration.hooks['astro:server:done']?.({ logger }); + + expect(logger.log).toStrictEqual([ + 'Called from plugin "foo" on integration "my-integration".', + 'Calling "foo" with msg: from integration', + 'Reading state', + ]); + }); + + it('should override plugins with the same name', () => { + const integration = withPlugins({ + name: 'my-integration', + plugins: [fooPlugin, otherFooPlugin], + hooks: { + 'astro:build:start': ({ foo }) => { + foo('from integration'); + } + }, + }); + + const logger = new MemoryLogger(); + + integration.hooks['astro:build:start']?.({ logger }); + + expect(logger.log).toStrictEqual([ + 'Called from plugin "otherFoo" on integration "my-integration".', + 'Calling "foo" (from otherFoo) with msg: from integration', + ]); + }); + + it('should override plugin APIs with the same name', () => { + const integration = withPlugins({ + name: 'my-integration', + plugins: [fooPlugin, barPlugin], + hooks: { + 'astro:build:start': ({ foo }) => { + foo('from integration'); + } + }, + }); + + const logger = new MemoryLogger(); + + integration.hooks['astro:build:start']?.({ logger }); + + expect(logger.log).toStrictEqual([ + 'Called from plugin "foo" on integration "my-integration".', + 'Called from plugin "bar" on integration "my-integration".', + 'Calling "foo" (from bar) with msg: from integration', + ]); + }); + + it('should run plugin hooks that are not part of the integration', () => { + const integration = withPlugins({ + name: 'my-integration', + plugins: [fooPlugin], + hooks: { + 'astro:server:done': ({ getState }) => { + expect(getState()).toEqual('initial state'); + }, + }, + }); + + const logger = new MemoryLogger(); + + integration.hooks['astro:build:start']?.({ logger }); + integration.hooks['astro:server:done']?.({ logger }); + + expect(logger.log).toStrictEqual([ + 'Called from plugin "foo" on integration "my-integration".', + 'Reading state' + ]); + }); +}); + +class MemoryLogger implements AstroIntegrationLogger { + log: string[] = []; + + options = {} as any; + label: string = ''; + + fork(): AstroIntegrationLogger { + return this; + } + + info(message: string): void { + this.log.push(message); + } + warn(message: string): void { + this.log.push(message); + } + error(message: string): void { + this.log.push(message); + } + debug(message: string): void { + this.log.push(message); + } +} +