From f182253cc96e1d284d6e882e78f32f7bd2dd26ae Mon Sep 17 00:00:00 2001 From: Stefan Hoth <45467+stefanhoth@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:31:36 +0200 Subject: [PATCH 1/3] feat: add dependency filtering capabilities - Add --include and --exclude CLI options for regex-based filtering - Implement filterDependencies function with include/exclude logic - Support filtering via configuration files (.libyearrc.json, etc.) - Add comprehensive unit tests for filtering functionality - Update README with filtering documentation and examples - Note: package.json configuration not working with cosmiconfig v9.0.0 --- README.md | 78 ++++++++++++++++++++++++- package.json | 2 +- src/cli/args.ts | 10 +++- src/cli/configuration.ts | 6 ++ src/cli/index.ts | 63 ++++++++++++-------- src/filter.test.ts | 122 +++++++++++++++++++++++++++++++++++++++ src/filter.ts | 63 ++++++++++++++++++++ src/libyear.ts | 6 ++ src/types.ts | 6 ++ 9 files changed, 328 insertions(+), 28 deletions(-) create mode 100644 src/filter.test.ts create mode 100644 src/filter.ts diff --git a/README.md b/README.md index fd3768a..1bae19b 100644 --- a/README.md +++ b/README.md @@ -219,11 +219,60 @@ Default `false`. Column to sort individual results by. Default `null`. +## Filtering + +### `--include=` + +Include only dependencies matching the specified regex pattern(s). +Can be specified multiple times for multiple patterns. +If no include patterns are specified, all dependencies are included (subject to exclude patterns). + +### `--exclude=` + +Exclude dependencies matching the specified regex pattern(s). +Can be specified multiple times for multiple patterns. +Exclude patterns are applied before include patterns. + +**Note**: Filtering can also be configured via configuration files. See [Configuration](#configuration) for details. + +## Examples + +### Filtering Dependencies + +Filter to show only TypeScript type definitions: + +```sh +npx libyear --include "^@types/" +``` + +Exclude all type definitions from the analysis: + +```sh +npx libyear --exclude "^@types/" +``` + +Show only ESLint-related packages: + +```sh +npx libyear --include ".*eslint.*" +``` + +Combine include and exclude patterns: + +```sh +npx libyear --include "^@types/" --exclude "^@types/node" +``` + +Show multiple specific package types: + +```sh +npx libyear --include "^@types/" --include "^eslint" +``` + ## Configuration -`libyear` can be configured via [cosmiconfig-supported](https://github.com/davidtheclark/cosmiconfig) formats. +`libyear` can be configured via [cosmiconfig-supported](https://github.com/davidtheclark/cosmiconfig) formats. Configuration files are searched in the following order (first found wins): -- `package.json` (under `{ "configs": { "libyear": { ... } } }`) - `.libyearrc` - `.libyearrc.cjs` - `.libyearrc.js` @@ -236,6 +285,9 @@ Default `null`. - `libyear.config.js` - `libyear.config.mjs` - `libyear.config.ts` +- `package.json` (under `{ "configs": { "libyear": { ... } } }`) + +**Note**: If any of the above configuration files exist in your project, the `package.json` configuration will not be read. This is the expected behavior of cosmiconfig. Custom configuration files can be provided via the [`--config`](#--configpath) CLI option. @@ -243,6 +295,10 @@ Configuration is expected in the following structure. ```json5 { + filtering: { + include: ["^@types/", "^eslint"], // array of regex patterns to include + exclude: ["^@types/node"], // array of regex patterns to exclude + }, overrides: { "^@types/": { defer: "2020-01-01", // string (ISO formatted Date) @@ -283,6 +339,24 @@ Configuration is expected in the following structure. } ``` +### Filtering + +Configuration files support a `filtering` property for persistent dependency filtering rules. + +- `include` - Array of regex patterns to include only matching dependencies +- `exclude` - Array of regex patterns to exclude matching dependencies + +**Note**: CLI filtering options take precedence over configuration-based filtering. + +```json +{ + "filtering": { + "include": ["^@types/", "^eslint"], + "exclude": ["^@types/node"] + } +} +``` + ### Overrides Configuration files support an `overrides` property. diff --git a/package.json b/package.json index 0ea00aa..5b84b97 100644 --- a/package.json +++ b/package.json @@ -80,4 +80,4 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.36.0" } -} +} \ No newline at end of file diff --git a/src/cli/args.ts b/src/cli/args.ts index 6d56193..6e39fc3 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -90,10 +90,18 @@ export const getArgs = (): Args => { sort: { type: "string", }, + include: { + type: "string", + multiple: true, + }, + exclude: { + type: "string", + multiple: true, + }, }, }); - return Object.fromEntries( + return Object.fromEntries( Object.entries(values).map(([key, value]) => [camelCase(key), value]), ); }; diff --git a/src/cli/configuration.ts b/src/cli/configuration.ts index 9fb2346..136dfeb 100644 --- a/src/cli/configuration.ts +++ b/src/cli/configuration.ts @@ -17,6 +17,8 @@ const getCliConfiguration = ({ limitMinorIndividual, limitPatchCollective, limitPatchIndividual, + include, + exclude, }: Args) => ({ limit: { drift: { @@ -44,6 +46,10 @@ const getCliConfiguration = ({ individual: safeParseInt(limitPatchIndividual), }, }, + filtering: { + include, + exclude, + }, }); const getCosmiconfig = async (filePath?: string): Promise => { diff --git a/src/cli/index.ts b/src/cli/index.ts index ba6471d..2666a6b 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,6 +20,8 @@ export const cli = async (): Promise => { preReleases, quiet, sort, + include, + exclude, ...rest } = getArgs(); @@ -38,6 +40,8 @@ export const cli = async (): Promise => { "--pre-releases Include pre-releases in latest versions.", "--quiet, -q Exclude up-to-date dependencies from results.", "--sort Column to sort individual results by.", + "--include Include only dependencies matching regex pattern(s).", + "--exclude Exclude dependencies matching regex pattern(s).", "--limit-drift-collective, -D Drift limit to warn on for all dependencies.", "--limit-drift-individual, -d Drift limit to warn on for individual dependencies.", "--limit-pulse-collective, -P Pulse limit to warn on for all dependencies.", @@ -58,40 +62,51 @@ export const cli = async (): Promise => { // run libyear try { + const configuration = await getConfiguration(rest); + + // Merge CLI filtering with config filtering (CLI takes precedence) + const mergedInclude = include?.length + ? include + : configuration.filtering?.include; + const mergedExclude = exclude?.length + ? exclude + : configuration.filtering?.exclude; + const report = await libyear( getParsedPackageManager( packageManager ?? (await getInferredPackageManager()), ), - { all, dev, preReleases, quiet, sort }, + { + all, + dev, + preReleases, + quiet, + sort, + include: mergedInclude, + exclude: mergedExclude, + }, ); if (json) { console.log(JSON.stringify(report)); } else { - const { overrides, limit } = await getConfiguration(rest).then( - ({ - overrides, - limit: { drift, pulse, releases, major, minor, patch } = {}, - }) => ({ - overrides, - limit: { - driftCollective: validateLimit(drift?.collective), - driftIndividual: validateLimit(drift?.individual), - pulseCollective: validateLimit(pulse?.collective), - pulseIndividual: validateLimit(pulse?.individual), - releasesCollective: validateLimit(releases?.collective), - releasesIndividual: validateLimit(releases?.individual), - majorCollective: validateLimit(major?.collective), - majorIndividual: validateLimit(major?.individual), - minorCollective: validateLimit(minor?.collective), - minorIndividual: validateLimit(minor?.individual), - patchCollective: validateLimit(patch?.collective), - patchIndividual: validateLimit(patch?.individual), - }, - }), - ); + const { overrides, limit } = configuration; + const processedLimit = { + driftCollective: validateLimit(limit?.drift?.collective), + driftIndividual: validateLimit(limit?.drift?.individual), + pulseCollective: validateLimit(limit?.pulse?.collective), + pulseIndividual: validateLimit(limit?.pulse?.individual), + releasesCollective: validateLimit(limit?.releases?.collective), + releasesIndividual: validateLimit(limit?.releases?.individual), + majorCollective: validateLimit(limit?.major?.collective), + majorIndividual: validateLimit(limit?.major?.individual), + minorCollective: validateLimit(limit?.minor?.collective), + minorIndividual: validateLimit(limit?.minor?.individual), + patchCollective: validateLimit(limit?.patch?.collective), + patchIndividual: validateLimit(limit?.patch?.individual), + }; - print(report, limit, overrides); + print(report, processedLimit, overrides); } } catch (error) { if (json) { diff --git a/src/filter.test.ts b/src/filter.test.ts new file mode 100644 index 0000000..18fd0af --- /dev/null +++ b/src/filter.test.ts @@ -0,0 +1,122 @@ +import { test, describe } from "node:test"; +import assert from "node:assert"; +import { filterDependencies } from "./filter.ts"; +import type { Dependency } from "./types.ts"; + +const mockDependencies: Dependency[] = [ + { + dependency: "@types/node", + drift: 0, + pulse: 0.01, + releases: 0, + major: 0, + minor: 0, + patch: 0, + }, + { + dependency: "eslint", + drift: 0, + pulse: 0.02, + releases: 0, + major: 0, + minor: 0, + patch: 0, + }, + { + dependency: "typescript", + drift: 0, + pulse: 0, + releases: 0, + major: 0, + minor: 0, + patch: 0, + }, + { + dependency: "@types/lodash-es", + drift: 0, + pulse: 1.77, + releases: 0, + major: 0, + minor: 0, + patch: 0, + }, +]; + +describe("filterDependencies", () => { + test("returns all dependencies when no filters are provided", () => { + const result = filterDependencies(mockDependencies); + assert.deepStrictEqual(result, mockDependencies); + }); + + test("filters by include pattern", () => { + const result = filterDependencies(mockDependencies, ["^@types/"]); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0]!.dependency, "@types/node"); + assert.strictEqual(result[1]!.dependency, "@types/lodash-es"); + }); + + test("filters by multiple include patterns", () => { + const result = filterDependencies(mockDependencies, [ + "^@types/", + "^eslint", + ]); + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0]!.dependency, "@types/node"); + assert.strictEqual(result[1]!.dependency, "eslint"); + assert.strictEqual(result[2]!.dependency, "@types/lodash-es"); + }); + + test("filters by exclude pattern", () => { + const result = filterDependencies(mockDependencies, undefined, [ + "^@types/", + ]); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0]!.dependency, "eslint"); + assert.strictEqual(result[1]!.dependency, "typescript"); + }); + + test("filters by multiple exclude patterns", () => { + const result = filterDependencies(mockDependencies, undefined, [ + "^@types/", + "^eslint", + ]); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0]!.dependency, "typescript"); + }); + + test("combines include and exclude patterns", () => { + const result = filterDependencies( + mockDependencies, + ["^@types/"], + ["^@types/node"], + ); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0]!.dependency, "@types/lodash-es"); + }); + + test("handles exact match patterns", () => { + const result = filterDependencies(mockDependencies, ["^typescript$"]); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0]!.dependency, "typescript"); + }); + + test("handles simple patterns", () => { + const result = filterDependencies(mockDependencies, ["typescript"]); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0]!.dependency, "typescript"); + }); + + test("handles empty include array", () => { + const result = filterDependencies(mockDependencies, [], ["^@types/"]); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0]!.dependency, "eslint"); + assert.strictEqual(result[1]!.dependency, "typescript"); + }); + + test("handles empty exclude array", () => { + const result = filterDependencies(mockDependencies, ["^@types/"], []); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0]!.dependency, "@types/node"); + assert.strictEqual(result[1]!.dependency, "@types/lodash-es"); + }); +}); diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..07971f1 --- /dev/null +++ b/src/filter.ts @@ -0,0 +1,63 @@ +import type { Dependency } from "./types.ts"; + +/** + * Filters dependencies based on include and exclude patterns + * @param dependencies - Array of dependencies to filter + * @param include - Array of regex patterns to include (if empty, all are included) + * @param exclude - Array of regex patterns to exclude + * @returns Filtered array of dependencies + */ +export const filterDependencies = ( + dependencies: Dependency[], + include?: string[], + exclude?: string[], +): Dependency[] => { + if (!include?.length && !exclude?.length) { + return dependencies; + } + + return dependencies.filter((dependency) => { + const depName = dependency.dependency; + + // Check exclude patterns first - if any match, exclude the dependency + if (exclude?.length) { + for (const pattern of exclude) { + try { + const regex = new RegExp(pattern); + if (regex.test(depName)) { + return false; + } + } catch (error) { + // Invalid regex pattern - log warning and continue + console.warn( + `Warning: Invalid exclude regex pattern "${pattern}": ${error}`, + ); + } + } + } + + // If no include patterns, include all non-excluded dependencies + if (!include?.length) { + return true; + } + + // Check include patterns - if any match, include the dependency + for (const pattern of include) { + try { + const regex = new RegExp(pattern); + if (regex.test(depName)) { + return true; + } + } catch (error) { + // Invalid regex pattern - log warning and continue + console.warn( + `Warning: Invalid include regex pattern "${pattern}": ${error}`, + ); + } + } + + // If we have include patterns but none matched, exclude the dependency + return false; + }); +}; + diff --git a/src/libyear.ts b/src/libyear.ts index a41e3c4..9d825cc 100644 --- a/src/libyear.ts +++ b/src/libyear.ts @@ -7,6 +7,7 @@ import { compare, sort, valid } from "semver"; import { calculateDrift, calculatePulse } from "./date.ts"; import { getDependencies } from "./fetch/dependencies.ts"; import { getPackageInfo } from "./fetch/package-info.ts"; +import { filterDependencies } from "./filter.ts"; import type { Dependencies, Dependency, @@ -29,6 +30,8 @@ export const libyear = async ( preReleases?: boolean; quiet?: boolean; sort?: Metric; + include?: string[]; + exclude?: string[]; }, ): Promise => Promise.all( @@ -105,4 +108,7 @@ export const libyear = async ( flags?.sort != null ? orderBy(dependencies, flags.sort, "desc") : dependencies, + ) + .then((dependencies) => + filterDependencies(dependencies, flags?.include, flags?.exclude), ); diff --git a/src/types.ts b/src/types.ts index 7a2ce19..e25df34 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,10 @@ export type Configuration = { individual?: Pick; } >; + filtering?: { + include?: string[]; + exclude?: string[]; + }; }; export type Args = { @@ -53,6 +57,8 @@ export type Args = { preReleases?: boolean; quiet?: boolean; sort?: Metric; + include?: string[]; + exclude?: string[]; }; export type Dependency = Record & { From a3716ddac69817fc1f54391ca0ba737ba75079c9 Mon Sep 17 00:00:00 2001 From: Stefan Hoth <45467+stefanhoth@users.noreply.github.com> Date: Sun, 21 Sep 2025 12:07:50 +0200 Subject: [PATCH 2/3] refactor: simplify filtering to only support --include-only CLI option - Remove --exclude CLI option and related logic - Remove configuration file support for filtering - Rename --include to --include-only for clarity - Update filterDependencies function to only handle include patterns - Update tests to cover simplified functionality - Update README documentation to reflect changes - Address maintainer feedback to avoid users indefinitely excluding dependencies --- README.md | 52 +++++++--------------------------------- src/cli/args.ts | 6 +---- src/cli/configuration.ts | 6 ----- src/cli/index.ts | 17 +++---------- src/filter.test.ts | 48 +++++++------------------------------ src/filter.ts | 37 +++++----------------------- src/libyear.ts | 5 ++-- src/types.ts | 7 +----- 8 files changed, 30 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 1bae19b..b9d29c6 100644 --- a/README.md +++ b/README.md @@ -221,19 +221,11 @@ Default `null`. ## Filtering -### `--include=` +### `--include-only=` Include only dependencies matching the specified regex pattern(s). Can be specified multiple times for multiple patterns. -If no include patterns are specified, all dependencies are included (subject to exclude patterns). - -### `--exclude=` - -Exclude dependencies matching the specified regex pattern(s). -Can be specified multiple times for multiple patterns. -Exclude patterns are applied before include patterns. - -**Note**: Filtering can also be configured via configuration files. See [Configuration](#configuration) for details. +If no patterns are specified, all dependencies are included. ## Examples @@ -242,31 +234,25 @@ Exclude patterns are applied before include patterns. Filter to show only TypeScript type definitions: ```sh -npx libyear --include "^@types/" -``` - -Exclude all type definitions from the analysis: - -```sh -npx libyear --exclude "^@types/" +npx libyear --include-only "^@types/" ``` Show only ESLint-related packages: ```sh -npx libyear --include ".*eslint.*" +npx libyear --include-only ".*eslint.*" ``` -Combine include and exclude patterns: +Filter to show only packages from a specific scope: ```sh -npx libyear --include "^@types/" --exclude "^@types/node" +npx libyear --include-only "^@myorg/" ``` Show multiple specific package types: ```sh -npx libyear --include "^@types/" --include "^eslint" +npx libyear --include-only "^@types/" --include-only "^eslint" ``` ## Configuration @@ -295,10 +281,6 @@ Configuration is expected in the following structure. ```json5 { - filtering: { - include: ["^@types/", "^eslint"], // array of regex patterns to include - exclude: ["^@types/node"], // array of regex patterns to exclude - }, overrides: { "^@types/": { defer: "2020-01-01", // string (ISO formatted Date) @@ -339,23 +321,7 @@ Configuration is expected in the following structure. } ``` -### Filtering - -Configuration files support a `filtering` property for persistent dependency filtering rules. - -- `include` - Array of regex patterns to include only matching dependencies -- `exclude` - Array of regex patterns to exclude matching dependencies - -**Note**: CLI filtering options take precedence over configuration-based filtering. - -```json -{ - "filtering": { - "include": ["^@types/", "^eslint"], - "exclude": ["^@types/node"] - } -} -``` +```` ### Overrides @@ -378,7 +344,7 @@ To match a specific dependency, make sure to include the starts with (`^`) and e "^libyear$": {} } } -``` +```` There may be cases where matching many dependencies is desired. For instance, type definitions are often updated less regularly than source code. diff --git a/src/cli/args.ts b/src/cli/args.ts index 6e39fc3..2c0cce2 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -90,11 +90,7 @@ export const getArgs = (): Args => { sort: { type: "string", }, - include: { - type: "string", - multiple: true, - }, - exclude: { + "include-only": { type: "string", multiple: true, }, diff --git a/src/cli/configuration.ts b/src/cli/configuration.ts index 136dfeb..9fb2346 100644 --- a/src/cli/configuration.ts +++ b/src/cli/configuration.ts @@ -17,8 +17,6 @@ const getCliConfiguration = ({ limitMinorIndividual, limitPatchCollective, limitPatchIndividual, - include, - exclude, }: Args) => ({ limit: { drift: { @@ -46,10 +44,6 @@ const getCliConfiguration = ({ individual: safeParseInt(limitPatchIndividual), }, }, - filtering: { - include, - exclude, - }, }); const getCosmiconfig = async (filePath?: string): Promise => { diff --git a/src/cli/index.ts b/src/cli/index.ts index 2666a6b..b39d9d8 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,8 +20,7 @@ export const cli = async (): Promise => { preReleases, quiet, sort, - include, - exclude, + includeOnly, ...rest } = getArgs(); @@ -40,8 +39,7 @@ export const cli = async (): Promise => { "--pre-releases Include pre-releases in latest versions.", "--quiet, -q Exclude up-to-date dependencies from results.", "--sort Column to sort individual results by.", - "--include Include only dependencies matching regex pattern(s).", - "--exclude Exclude dependencies matching regex pattern(s).", + "--include-only Include only dependencies matching regex pattern(s).", "--limit-drift-collective, -D Drift limit to warn on for all dependencies.", "--limit-drift-individual, -d Drift limit to warn on for individual dependencies.", "--limit-pulse-collective, -P Pulse limit to warn on for all dependencies.", @@ -64,14 +62,6 @@ export const cli = async (): Promise => { try { const configuration = await getConfiguration(rest); - // Merge CLI filtering with config filtering (CLI takes precedence) - const mergedInclude = include?.length - ? include - : configuration.filtering?.include; - const mergedExclude = exclude?.length - ? exclude - : configuration.filtering?.exclude; - const report = await libyear( getParsedPackageManager( packageManager ?? (await getInferredPackageManager()), @@ -82,8 +72,7 @@ export const cli = async (): Promise => { preReleases, quiet, sort, - include: mergedInclude, - exclude: mergedExclude, + includeOnly, }, ); diff --git a/src/filter.test.ts b/src/filter.test.ts index 18fd0af..3da9d13 100644 --- a/src/filter.test.ts +++ b/src/filter.test.ts @@ -48,14 +48,14 @@ describe("filterDependencies", () => { assert.deepStrictEqual(result, mockDependencies); }); - test("filters by include pattern", () => { + test("filters by include-only pattern", () => { const result = filterDependencies(mockDependencies, ["^@types/"]); assert.strictEqual(result.length, 2); assert.strictEqual(result[0]!.dependency, "@types/node"); assert.strictEqual(result[1]!.dependency, "@types/lodash-es"); }); - test("filters by multiple include patterns", () => { + test("filters by multiple include-only patterns", () => { const result = filterDependencies(mockDependencies, [ "^@types/", "^eslint", @@ -66,34 +66,6 @@ describe("filterDependencies", () => { assert.strictEqual(result[2]!.dependency, "@types/lodash-es"); }); - test("filters by exclude pattern", () => { - const result = filterDependencies(mockDependencies, undefined, [ - "^@types/", - ]); - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0]!.dependency, "eslint"); - assert.strictEqual(result[1]!.dependency, "typescript"); - }); - - test("filters by multiple exclude patterns", () => { - const result = filterDependencies(mockDependencies, undefined, [ - "^@types/", - "^eslint", - ]); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0]!.dependency, "typescript"); - }); - - test("combines include and exclude patterns", () => { - const result = filterDependencies( - mockDependencies, - ["^@types/"], - ["^@types/node"], - ); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0]!.dependency, "@types/lodash-es"); - }); - test("handles exact match patterns", () => { const result = filterDependencies(mockDependencies, ["^typescript$"]); assert.strictEqual(result.length, 1); @@ -106,17 +78,13 @@ describe("filterDependencies", () => { assert.strictEqual(result[0]!.dependency, "typescript"); }); - test("handles empty include array", () => { - const result = filterDependencies(mockDependencies, [], ["^@types/"]); - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0]!.dependency, "eslint"); - assert.strictEqual(result[1]!.dependency, "typescript"); + test("handles empty include-only array", () => { + const result = filterDependencies(mockDependencies, []); + assert.deepStrictEqual(result, mockDependencies); }); - test("handles empty exclude array", () => { - const result = filterDependencies(mockDependencies, ["^@types/"], []); - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0]!.dependency, "@types/node"); - assert.strictEqual(result[1]!.dependency, "@types/lodash-es"); + test("handles invalid regex patterns gracefully", () => { + const result = filterDependencies(mockDependencies, ["[invalid"]); + assert.strictEqual(result.length, 0); }); }); diff --git a/src/filter.ts b/src/filter.ts index 07971f1..2c2e375 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,48 +1,24 @@ import type { Dependency } from "./types.ts"; /** - * Filters dependencies based on include and exclude patterns + * Filters dependencies to include only those matching the specified patterns * @param dependencies - Array of dependencies to filter - * @param include - Array of regex patterns to include (if empty, all are included) - * @param exclude - Array of regex patterns to exclude + * @param includeOnly - Array of regex patterns to include (if empty, all are included) * @returns Filtered array of dependencies */ export const filterDependencies = ( dependencies: Dependency[], - include?: string[], - exclude?: string[], + includeOnly?: string[], ): Dependency[] => { - if (!include?.length && !exclude?.length) { + if (!includeOnly?.length) { return dependencies; } return dependencies.filter((dependency) => { const depName = dependency.dependency; - // Check exclude patterns first - if any match, exclude the dependency - if (exclude?.length) { - for (const pattern of exclude) { - try { - const regex = new RegExp(pattern); - if (regex.test(depName)) { - return false; - } - } catch (error) { - // Invalid regex pattern - log warning and continue - console.warn( - `Warning: Invalid exclude regex pattern "${pattern}": ${error}`, - ); - } - } - } - - // If no include patterns, include all non-excluded dependencies - if (!include?.length) { - return true; - } - // Check include patterns - if any match, include the dependency - for (const pattern of include) { + for (const pattern of includeOnly) { try { const regex = new RegExp(pattern); if (regex.test(depName)) { @@ -51,7 +27,7 @@ export const filterDependencies = ( } catch (error) { // Invalid regex pattern - log warning and continue console.warn( - `Warning: Invalid include regex pattern "${pattern}": ${error}`, + `Warning: Invalid include-only regex pattern "${pattern}": ${error}`, ); } } @@ -60,4 +36,3 @@ export const filterDependencies = ( return false; }); }; - diff --git a/src/libyear.ts b/src/libyear.ts index 9d825cc..af935d8 100644 --- a/src/libyear.ts +++ b/src/libyear.ts @@ -30,8 +30,7 @@ export const libyear = async ( preReleases?: boolean; quiet?: boolean; sort?: Metric; - include?: string[]; - exclude?: string[]; + includeOnly?: string[]; }, ): Promise => Promise.all( @@ -110,5 +109,5 @@ export const libyear = async ( : dependencies, ) .then((dependencies) => - filterDependencies(dependencies, flags?.include, flags?.exclude), + filterDependencies(dependencies, flags?.includeOnly), ); diff --git a/src/types.ts b/src/types.ts index e25df34..afed939 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,10 +29,6 @@ export type Configuration = { individual?: Pick; } >; - filtering?: { - include?: string[]; - exclude?: string[]; - }; }; export type Args = { @@ -57,8 +53,7 @@ export type Args = { preReleases?: boolean; quiet?: boolean; sort?: Metric; - include?: string[]; - exclude?: string[]; + includeOnly?: string[]; }; export type Dependency = Record & { From d1565fc2780e706535d4249a43e99f40410e7c7c Mon Sep 17 00:00:00 2001 From: Stefan Hoth <45467+stefanhoth@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:11:11 +0200 Subject: [PATCH 3/3] test: make filter tests more resilient to dependency order changes - Replace position-based assertions with content-based checks - Use includes() to verify expected dependencies regardless of order - Ensure tests remain stable if internal dependency ordering changes - Improve test maintainability and robustness --- src/filter.test.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/filter.test.ts b/src/filter.test.ts index 3da9d13..1ce7991 100644 --- a/src/filter.test.ts +++ b/src/filter.test.ts @@ -51,8 +51,14 @@ describe("filterDependencies", () => { test("filters by include-only pattern", () => { const result = filterDependencies(mockDependencies, ["^@types/"]); assert.strictEqual(result.length, 2); - assert.strictEqual(result[0]!.dependency, "@types/node"); - assert.strictEqual(result[1]!.dependency, "@types/lodash-es"); + + // Check that we have the expected dependencies regardless of order + const dependencyNames = result.map(dep => dep.dependency); + assert.ok(dependencyNames.includes("@types/node")); + assert.ok(dependencyNames.includes("@types/lodash-es")); + + // Ensure we don't have any unexpected dependencies + assert.strictEqual(dependencyNames.length, 2); }); test("filters by multiple include-only patterns", () => { @@ -61,9 +67,15 @@ describe("filterDependencies", () => { "^eslint", ]); assert.strictEqual(result.length, 3); - assert.strictEqual(result[0]!.dependency, "@types/node"); - assert.strictEqual(result[1]!.dependency, "eslint"); - assert.strictEqual(result[2]!.dependency, "@types/lodash-es"); + + // Check that we have the expected dependencies regardless of order + const dependencyNames = result.map(dep => dep.dependency); + assert.ok(dependencyNames.includes("@types/node")); + assert.ok(dependencyNames.includes("eslint")); + assert.ok(dependencyNames.includes("@types/lodash-es")); + + // Ensure we don't have any unexpected dependencies + assert.strictEqual(dependencyNames.length, 3); }); test("handles exact match patterns", () => {