diff --git a/packages/cli/src/lib/implementation/filter-plugins.middleware.ts b/packages/cli/src/lib/implementation/filter-plugins.middleware.ts new file mode 100644 index 000000000..e59a07fc0 --- /dev/null +++ b/packages/cli/src/lib/implementation/filter-plugins.middleware.ts @@ -0,0 +1,59 @@ +import { filterItemRefsBy } from '@code-pushup/utils'; +import type { OnlyPluginsOptions } from './only-plugins.model'; +import type { SkipPluginsOptions } from './skip-plugins.model'; +import { validatePluginFilterOption } from './validate-plugin-filter-options.utils'; + +export function filterPluginsMiddleware< + T extends SkipPluginsOptions & OnlyPluginsOptions, +>(originalProcessArgs: T): T { + const { + plugins, + categories = [], + skipPlugins = [], + onlyPlugins = [], + verbose, + } = originalProcessArgs; + + if (skipPlugins.length === 0 && onlyPlugins.length === 0) { + return { ...originalProcessArgs, categories }; + } + + validatePluginFilterOption( + 'skipPlugins', + { plugins, categories }, + { pluginsToFilter: skipPlugins, verbose }, + ); + validatePluginFilterOption( + 'onlyPlugins', + { plugins, categories }, + { pluginsToFilter: onlyPlugins, verbose }, + ); + + const validSkipPlugins = new Set( + skipPlugins.filter(sP => plugins.some(p => p.slug === sP)), + ); + const pluginsAfterSkip = plugins.filter( + ({ slug }) => !validSkipPlugins.has(slug), + ); + + const validOnlyPlugins = new Set( + onlyPlugins.filter(oP => pluginsAfterSkip.some(p => p.slug === oP)), + ); + const filteredPlugins = + validOnlyPlugins.size > 0 + ? pluginsAfterSkip.filter(({ slug }) => validOnlyPlugins.has(slug)) + : pluginsAfterSkip; + + const filteredCategories = + filteredPlugins.length > 0 + ? filterItemRefsBy(categories, ({ plugin }) => + filteredPlugins.some(({ slug }) => slug === plugin), + ) + : categories; + + return { + ...originalProcessArgs, + plugins: filteredPlugins, + categories: filteredCategories, + }; +} diff --git a/packages/cli/src/lib/implementation/filter-plugins.middleware.unit.test.ts b/packages/cli/src/lib/implementation/filter-plugins.middleware.unit.test.ts new file mode 100644 index 000000000..d863a656f --- /dev/null +++ b/packages/cli/src/lib/implementation/filter-plugins.middleware.unit.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, vi } from 'vitest'; +import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; +import { ui } from '@code-pushup/utils'; +import { filterPluginsMiddleware } from './filter-plugins.middleware'; + +vi.mock('@code-pushup/core', async () => { + const { CORE_CONFIG_MOCK }: typeof import('@code-pushup/test-utils') = + await vi.importActual('@code-pushup/test-utils'); + const core: object = await vi.importActual('@code-pushup/core'); + return { + ...core, + readRcByPath: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), + autoloadRc: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), + }; +}); + +describe('filterPluginsMiddleware', () => { + it('should fill undefined categories with empty array', () => { + expect( + filterPluginsMiddleware({ + plugins: [{ slug: 'p1' } as PluginConfig], + }), + ).toStrictEqual({ + plugins: [{ slug: 'p1' }], + categories: [], + }); + }); + + it('should forward equal values when neither skipPlugins nor onlyPlugins is set', () => { + expect( + filterPluginsMiddleware({ + plugins: [{ slug: 'p1' } as PluginConfig], + categories: [ + { slug: 'c1', refs: [{ plugin: 'p1' }] } as CategoryConfig, + ], + }), + ).toStrictEqual({ + plugins: [{ slug: 'p1' }], + categories: [{ slug: 'c1', refs: [{ plugin: 'p1' }] }], + }); + }); + + it('should return original values when neither skipPlugins nor onlyPlugins is provided', () => { + const originalPlugins = [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[]; + const originalCategories = [ + { + slug: 'c1', + refs: [{ plugin: 'p1', slug: 'a1-p1' }], + }, + ] as CategoryConfig[]; + + const { plugins, categories } = filterPluginsMiddleware({ + plugins: originalPlugins, + categories: originalCategories, + }); + + expect(plugins).toStrictEqual(originalPlugins); + expect(categories).toStrictEqual(originalCategories); + }); + + it('should return original values when skipPlugins and onlyPlugins are empty', () => { + const originalPlugins = [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[]; + const originalCategories = [ + { + slug: 'c1', + refs: [{ plugin: 'p1', slug: 'a1-p1' }], + }, + ] as CategoryConfig[]; + + const { plugins, categories } = filterPluginsMiddleware({ + plugins: originalPlugins, + categories: originalCategories, + skipPlugins: [], + onlyPlugins: [], + }); + + expect(plugins).toStrictEqual(originalPlugins); + expect(categories).toStrictEqual(originalCategories); + }); + + it('should filter plugins for slug "p1" in onlyPlugins', () => { + const { plugins } = filterPluginsMiddleware({ + onlyPlugins: ['p1'], + plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], + categories: [], + }); + expect(plugins).toStrictEqual([expect.objectContaining({ slug: 'p1' })]); + }); + + it('should filter plugins for slug "p2" in skipPlugins', () => { + const { plugins, categories } = filterPluginsMiddleware({ + skipPlugins: ['p2'], + plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], + categories: [ + { + slug: 'c1', + refs: [ + { plugin: 'p1', slug: 'a1-p1' }, + { plugin: 'p2', slug: 'a2-p1' }, + ], + }, + { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, + ] as CategoryConfig[], + }); + + expect(plugins).toStrictEqual([expect.objectContaining({ slug: 'p1' })]); + expect(categories).toStrictEqual([ + expect.objectContaining({ + slug: 'c1', + refs: [{ plugin: 'p1', slug: 'a1-p1' }], + }), + ]); + }); + + it('should forward plugins and categories for a slug not present in plugins', () => { + const originalCategories = [ + { + slug: 'c1', + refs: [ + { plugin: 'p1', slug: 'a1-p1' }, + { plugin: 'p2', slug: 'a2-p1' }, + ], + }, + { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, + ] as CategoryConfig[]; + const originalPlugins = [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[]; + const { categories, plugins } = filterPluginsMiddleware({ + onlyPlugins: ['wrong-slug'], + plugins: originalPlugins, + categories: originalCategories, + }); + expect(categories).toStrictEqual(originalCategories); + expect(plugins).toStrictEqual(originalPlugins); + }); + + it('should filter categories for slug "p1" in onlyPlugins', () => { + const { categories } = filterPluginsMiddleware({ + onlyPlugins: ['p1'], + plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], + categories: [ + { + slug: 'c1', + refs: [ + { plugin: 'p1', slug: 'a1-p1' }, + { plugin: 'p2', slug: 'a2-p1' }, + ], + }, + { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, + ] as CategoryConfig[], + }); + expect(categories).toStrictEqual([ + expect.objectContaining({ + slug: 'c1', + refs: [{ plugin: 'p1', slug: 'a1-p1' }], + }), + ]); + }); + + it('should filter plugins when both skipPlugins and onlyPlugins are provided', () => { + const { plugins } = filterPluginsMiddleware({ + onlyPlugins: ['p1'], + skipPlugins: ['p2'], + plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], + categories: [], + }); + expect(plugins).toStrictEqual([expect.objectContaining({ slug: 'p1' })]); + }); + + it('should trigger verbose logging when skipPlugins or onlyPlugins removes categories', () => { + const loggerSpy = vi.spyOn(ui().logger, 'info'); + + filterPluginsMiddleware({ + onlyPlugins: ['p1'], + skipPlugins: ['p2'], + plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], + categories: [ + { + slug: 'c1', + refs: [ + { plugin: 'p1', slug: 'a1-p1' }, + { plugin: 'p2', slug: 'a2-p1' }, + ], + }, + { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, + ] as CategoryConfig[], + verbose: true, + }); + + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('removed the following categories'), + ); + }); +}); diff --git a/packages/cli/src/lib/implementation/only-plugins.middleware.ts b/packages/cli/src/lib/implementation/only-plugins.middleware.ts deleted file mode 100644 index 492605b55..000000000 --- a/packages/cli/src/lib/implementation/only-plugins.middleware.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { filterItemRefsBy } from '@code-pushup/utils'; -import type { OnlyPluginsOptions } from './only-plugins.model'; -import { validatePluginFilterOption } from './validate-plugin-filter-options.utils'; - -export function onlyPluginsMiddleware( - originalProcessArgs: T, -): T { - const { categories = [], onlyPlugins: originalOnlyPlugins } = - originalProcessArgs; - - if (originalOnlyPlugins && originalOnlyPlugins.length > 0) { - const { verbose, plugins, onlyPlugins = [] } = originalProcessArgs; - - validatePluginFilterOption( - 'onlyPlugins', - { plugins, categories }, - { pluginsToFilter: onlyPlugins, verbose }, - ); - - const validOnlyPlugins = onlyPlugins.filter(oP => - plugins.find(p => p.slug === oP), - ); - const onlyPluginsSet = new Set(validOnlyPlugins); - - return { - ...originalProcessArgs, - plugins: - onlyPluginsSet.size > 0 - ? plugins.filter(({ slug }) => onlyPluginsSet.has(slug)) - : plugins, - categories: - onlyPluginsSet.size > 0 - ? filterItemRefsBy(categories, ({ plugin }) => - onlyPluginsSet.has(plugin), - ) - : categories, - }; - } - - return { - ...originalProcessArgs, - // if undefined fill categories with empty array - categories, - }; -} diff --git a/packages/cli/src/lib/implementation/only-plugins.middleware.unit.test.ts b/packages/cli/src/lib/implementation/only-plugins.middleware.unit.test.ts deleted file mode 100644 index 94a456ba7..000000000 --- a/packages/cli/src/lib/implementation/only-plugins.middleware.unit.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, expect, vi } from 'vitest'; -import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; -import { onlyPluginsMiddleware } from './only-plugins.middleware'; - -vi.mock('@code-pushup/core', async () => { - const { CORE_CONFIG_MOCK }: typeof import('@code-pushup/test-utils') = - await vi.importActual('@code-pushup/test-utils'); - const core: object = await vi.importActual('@code-pushup/core'); - return { - ...core, - readRcByPath: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), - autoloadRc: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), - }; -}); - -describe('onlyPluginsMiddleware', () => { - it('should fill undefined categories with empty array', () => { - expect( - onlyPluginsMiddleware({ - plugins: [{ slug: 'p1' } as PluginConfig], - }), - ).toStrictEqual({ - plugins: [{ slug: 'p1' }], - categories: [], - }); - }); - - it('should forward equal values if not set', () => { - expect( - onlyPluginsMiddleware({ - plugins: [{ slug: 'p1' } as PluginConfig], - categories: [ - { slug: 'c1', refs: [{ plugin: 'p1' }] } as CategoryConfig, - ], - }), - ).toStrictEqual({ - plugins: [{ slug: 'p1' }], - categories: [{ slug: 'c1', refs: [{ plugin: 'p1' }] }], - }); - }); - - it('should filter plugins plugins for slug "p1"', () => { - const { plugins } = onlyPluginsMiddleware({ - onlyPlugins: ['p1'], - plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], - categories: [], - }); - expect(plugins).toStrictEqual([expect.objectContaining({ slug: 'p1' })]); - }); - - it('should forward plugins and categories for a slug not present in plugins', () => { - const originalCategories = [ - { - slug: 'c1', - refs: [ - { plugin: 'p1', slug: 'a1-p1' }, - { plugin: 'p2', slug: 'a2-p1' }, - ], - }, - { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, - ] as CategoryConfig[]; - const originalPlugins = [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[]; - const { categories, plugins } = onlyPluginsMiddleware({ - onlyPlugins: ['wrong-slug'], - plugins: originalPlugins, - categories: originalCategories, - }); - expect(categories).toBe(originalCategories); - expect(plugins).toBe(originalPlugins); - }); - - it('should filter categories for slug "p1"', () => { - const { categories } = onlyPluginsMiddleware({ - onlyPlugins: ['p1'], - plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], - categories: [ - { - slug: 'c1', - refs: [ - { plugin: 'p1', slug: 'a1-p1' }, - { plugin: 'p2', slug: 'a2-p1' }, - ], - }, - { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, - ] as CategoryConfig[], - }); - expect(categories).toStrictEqual([ - expect.objectContaining({ - slug: 'c1', - refs: [{ plugin: 'p1', slug: 'a1-p1' }], - }), - ]); - }); -}); diff --git a/packages/cli/src/lib/implementation/skip-plugins.middleware.ts b/packages/cli/src/lib/implementation/skip-plugins.middleware.ts deleted file mode 100644 index c11c5aee7..000000000 --- a/packages/cli/src/lib/implementation/skip-plugins.middleware.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { filterItemRefsBy } from '@code-pushup/utils'; -import type { SkipPluginsOptions } from './skip-plugins.model'; -import { validatePluginFilterOption } from './validate-plugin-filter-options.utils'; - -export function skipPluginsMiddleware( - originalProcessArgs: T, -): T { - const { categories = [], skipPlugins: originalSkipPlugins } = - originalProcessArgs; - - if (originalSkipPlugins && originalSkipPlugins.length > 0) { - const { verbose, plugins, skipPlugins = [] } = originalProcessArgs; - - validatePluginFilterOption( - 'skipPlugins', - { plugins, categories }, - { pluginsToFilter: skipPlugins, verbose }, - ); - - const validSkipPlugins = skipPlugins.filter(sP => - plugins.find(p => p.slug === sP), - ); - - const skipPluginsSet = new Set(validSkipPlugins); - - return { - ...originalProcessArgs, - plugins: - skipPluginsSet.size > 0 - ? plugins.filter(({ slug }) => !skipPluginsSet.has(slug)) - : plugins, - categories: - skipPluginsSet.size > 0 - ? filterItemRefsBy( - categories, - ({ plugin }) => !skipPluginsSet.has(plugin), - ) - : categories, - }; - } - - return { - ...originalProcessArgs, - // if undefined fill categories with empty array - categories, - }; -} diff --git a/packages/cli/src/lib/implementation/skip-plugins.middleware.unit.test.ts b/packages/cli/src/lib/implementation/skip-plugins.middleware.unit.test.ts deleted file mode 100644 index 8317acda5..000000000 --- a/packages/cli/src/lib/implementation/skip-plugins.middleware.unit.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, expect, vi } from 'vitest'; -import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; -import { skipPluginsMiddleware } from './skip-plugins.middleware'; - -vi.mock('@code-pushup/core', async () => { - const { CORE_CONFIG_MOCK }: typeof import('@code-pushup/test-utils') = - await vi.importActual('@code-pushup/test-utils'); - const core: object = await vi.importActual('@code-pushup/core'); - return { - ...core, - readRcByPath: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), - autoloadRc: vi.fn().mockResolvedValue(CORE_CONFIG_MOCK), - }; -}); - -describe('skipPluginsMiddleware', () => { - it('should fill undefined categories with empty array', () => { - expect( - skipPluginsMiddleware({ - plugins: [{ slug: 'p1' } as PluginConfig], - }), - ).toStrictEqual({ - plugins: [{ slug: 'p1' }], - categories: [], - }); - }); - - it('should forward equal values if not set', () => { - expect( - skipPluginsMiddleware({ - plugins: [{ slug: 'p1' } as PluginConfig], - categories: [ - { slug: 'c1', refs: [{ plugin: 'p1' }] } as CategoryConfig, - ], - }), - ).toStrictEqual({ - plugins: [{ slug: 'p1' }], - categories: [{ slug: 'c1', refs: [{ plugin: 'p1' }] }], - }); - }); - - it('should filter plugins for slug "p1"', () => { - const { plugins } = skipPluginsMiddleware({ - skipPlugins: ['p1'], - plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], - categories: [], - }); - expect(plugins).toStrictEqual([expect.objectContaining({ slug: 'p2' })]); - }); - - it('should forward plugins and categories for a slug not present in plugins', () => { - const originalCategories = [ - { - slug: 'c1', - refs: [ - { plugin: 'p1', slug: 'a1-p1' }, - { plugin: 'p2', slug: 'a2-p1' }, - ], - }, - { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, - ] as CategoryConfig[]; - const originalPlugins = [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[]; - const { categories, plugins } = skipPluginsMiddleware({ - skipPlugins: ['wrong-slug'], - plugins: originalPlugins, - categories: originalCategories, - }); - expect(categories).toBe(originalCategories); - expect(plugins).toBe(originalPlugins); - }); - - it('should filter categories for slug "p1"', () => { - const { categories } = skipPluginsMiddleware({ - skipPlugins: ['p1'], - plugins: [{ slug: 'p1' }, { slug: 'p2' }] as PluginConfig[], - categories: [ - { - slug: 'c1', - refs: [ - { plugin: 'p1', slug: 'a1-p1' }, - { plugin: 'p2', slug: 'a2-p1' }, - ], - }, - { slug: 'c2', refs: [{ plugin: 'p2', slug: 'a1-p2' }] }, - ] as CategoryConfig[], - }); - expect(categories).toStrictEqual([ - expect.objectContaining({ - slug: 'c1', - refs: [{ plugin: 'p2', slug: 'a2-p1' }], - }), - expect.objectContaining({ - slug: 'c2', - refs: [{ plugin: 'p2', slug: 'a1-p2' }], - }), - ]); - }); -}); diff --git a/packages/cli/src/lib/middlewares.ts b/packages/cli/src/lib/middlewares.ts index 1a4097ba5..029bff650 100644 --- a/packages/cli/src/lib/middlewares.ts +++ b/packages/cli/src/lib/middlewares.ts @@ -1,7 +1,6 @@ import type { MiddlewareFunction } from 'yargs'; import { coreConfigMiddleware } from './implementation/core-config.middleware'; -import { onlyPluginsMiddleware } from './implementation/only-plugins.middleware'; -import { skipPluginsMiddleware } from './implementation/skip-plugins.middleware'; +import { filterPluginsMiddleware } from './implementation/filter-plugins.middleware'; export const middlewares = [ { @@ -9,11 +8,8 @@ export const middlewares = [ applyBeforeValidation: false, }, { - middlewareFunction: onlyPluginsMiddleware as unknown as MiddlewareFunction, - applyBeforeValidation: false, - }, - { - middlewareFunction: skipPluginsMiddleware as unknown as MiddlewareFunction, + middlewareFunction: + filterPluginsMiddleware as unknown as MiddlewareFunction, applyBeforeValidation: false, }, ];