diff --git a/README.md b/README.md index fd3768a..b9d29c6 100644 --- a/README.md +++ b/README.md @@ -219,11 +219,46 @@ Default `false`. Column to sort individual results by. Default `null`. +## Filtering + +### `--include-only=` + +Include only dependencies matching the specified regex pattern(s). +Can be specified multiple times for multiple patterns. +If no patterns are specified, all dependencies are included. + +## Examples + +### Filtering Dependencies + +Filter to show only TypeScript type definitions: + +```sh +npx libyear --include-only "^@types/" +``` + +Show only ESLint-related packages: + +```sh +npx libyear --include-only ".*eslint.*" +``` + +Filter to show only packages from a specific scope: + +```sh +npx libyear --include-only "^@myorg/" +``` + +Show multiple specific package types: + +```sh +npx libyear --include-only "^@types/" --include-only "^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 +271,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. @@ -283,6 +321,8 @@ Configuration is expected in the following structure. } ``` +```` + ### Overrides Configuration files support an `overrides` property. @@ -304,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/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..2c0cce2 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -90,10 +90,14 @@ export const getArgs = (): Args => { sort: { type: "string", }, + "include-only": { + type: "string", + multiple: true, + }, }, }); - return Object.fromEntries( + return Object.fromEntries( Object.entries(values).map(([key, value]) => [camelCase(key), value]), ); }; diff --git a/src/cli/index.ts b/src/cli/index.ts index ba6471d..b39d9d8 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,6 +20,7 @@ export const cli = async (): Promise => { preReleases, quiet, sort, + includeOnly, ...rest } = getArgs(); @@ -38,6 +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-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.", @@ -58,40 +60,42 @@ export const cli = async (): Promise => { // run libyear try { + const configuration = await getConfiguration(rest); + const report = await libyear( getParsedPackageManager( packageManager ?? (await getInferredPackageManager()), ), - { all, dev, preReleases, quiet, sort }, + { + all, + dev, + preReleases, + quiet, + sort, + includeOnly, + }, ); 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..1ce7991 --- /dev/null +++ b/src/filter.test.ts @@ -0,0 +1,102 @@ +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-only pattern", () => { + const result = filterDependencies(mockDependencies, ["^@types/"]); + assert.strictEqual(result.length, 2); + + // 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", () => { + const result = filterDependencies(mockDependencies, [ + "^@types/", + "^eslint", + ]); + assert.strictEqual(result.length, 3); + + // 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", () => { + 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-only array", () => { + const result = filterDependencies(mockDependencies, []); + assert.deepStrictEqual(result, mockDependencies); + }); + + 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 new file mode 100644 index 0000000..2c2e375 --- /dev/null +++ b/src/filter.ts @@ -0,0 +1,38 @@ +import type { Dependency } from "./types.ts"; + +/** + * Filters dependencies to include only those matching the specified patterns + * @param dependencies - Array of dependencies to filter + * @param includeOnly - Array of regex patterns to include (if empty, all are included) + * @returns Filtered array of dependencies + */ +export const filterDependencies = ( + dependencies: Dependency[], + includeOnly?: string[], +): Dependency[] => { + if (!includeOnly?.length) { + return dependencies; + } + + return dependencies.filter((dependency) => { + const depName = dependency.dependency; + + // Check include patterns - if any match, include the dependency + for (const pattern of includeOnly) { + 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-only 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..af935d8 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,7 @@ export const libyear = async ( preReleases?: boolean; quiet?: boolean; sort?: Metric; + includeOnly?: string[]; }, ): Promise => Promise.all( @@ -105,4 +107,7 @@ export const libyear = async ( flags?.sort != null ? orderBy(dependencies, flags.sort, "desc") : dependencies, + ) + .then((dependencies) => + filterDependencies(dependencies, flags?.includeOnly), ); diff --git a/src/types.ts b/src/types.ts index 7a2ce19..afed939 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,7 @@ export type Args = { preReleases?: boolean; quiet?: boolean; sort?: Metric; + includeOnly?: string[]; }; export type Dependency = Record & {