Allow dependency usage to be grouped by package (#224751)

## Summary
This pull request introduces a new feature to group dependency usage by
package directories, alongside existing options for grouping by owner or
source. It includes updates to the CLI, dependency graph logic, and new
utility functions and tests to support the feature.

### CLI updates:
* Enhanced the `group-by` option in `configureYargs` to allow grouping
by `package` in addition to `owner` and `source`.
(`packages/kbn-dependency-usage/src/cli.ts`,
[packages/kbn-dependency-usage/src/cli.tsL50-R51](diffhunk://#diff-ef89f6725b6dde85fbfea1050625607f583373da328165c575070f0579e5f23aL50-R51))

### Dependency graph logic:
* Added the `groupByPackage` function to group dependencies by package
directories, and integrated it into the
`identifyDependencyUsageWithCruiser` method.
(`packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.ts`,
[[1]](diffhunk://#diff-19e7d98370cc898df6e0a28a61575490b35a0ff6013a8c5080aa8c9fa0065e71R17)
[[2]](diffhunk://#diff-19e7d98370cc898df6e0a28a61575490b35a0ff6013a8c5080aa8c9fa0065e71R120-R123)

### Utility functions:
* Implemented `groupByPackage` utility to identify package directories
based on the presence of `kibana.jsonc` files and group dependencies
accordingly.
(`packages/kbn-dependency-usage/src/lib/group_by_package.ts`,
[packages/kbn-dependency-usage/src/lib/group_by_package.tsR1-R80](diffhunk://#diff-4e2be55a320215ee636cd352393c09c0e90524a5b6121b034d1360a6afed4d67R1-R80))

### Unit tests:
* Added comprehensive tests for `groupByPackage`, covering scenarios
such as nested directories, missing package files, and handling of empty
dependencies.
(`packages/kbn-dependency-usage/src/lib/group_by_package.test.ts`,
[packages/kbn-dependency-usage/src/lib/group_by_package.test.tsR1-R231](diffhunk://#diff-69398d48b1e5a2ea0ed52ca35fc2d877e143a6dcefd71d2399931ba242b962b5R1-R231))

### Example usage
```sh
(base) ➜  kibana git:(dep-usage/group-by-package) ✗ ./scripts/dependency_usage.sh -c 20 -g package -p x-pack/solutions/security          
Searching for dependencies in paths: x-pack/solutions/security
Dependencies will be collapsed to depth: 20
cruiser is used for building dependency graph
Successfully built dependency graph using cruiser. Analyzing...
No output file specified, displaying results below:

{
  "x-pack/solutions/security/packages/connectors": [
    "minimatch",
    "@elastic/eui",
    "@emotion/react",
    "@testing-library/react",
    "react",
    "lodash"
  ],
  "x-pack/solutions/security/packages/data-stream-adapter": [
    "lodash"
  ],
  "x-pack/solutions/security/packages/data-table": [
    "io-ts",
    "enzyme",
    "lodash",
    "react",
    "@elastic/eui",
    "@hello-pangea/dnd",
    "@tanstack/react-query",
    "react-redux",
    "redux",
    "styled-components",
    "@testing-library/react",
    "memoize-one",
    "typescript-fsa",
    "typescript-fsa-reducers",
    "reselect"
  ],
  "x-pack/solutions/security/packages/distribution-bar": [
    "@elastic/eui",
    "react",
    "@testing-library/react",
    "@elastic/numeral",
    "@emotion/react"
  ],
  "x-pack/solutions/security/packages/ecs-data-quality-dashboard": [
    "minimatch",
    "react",
    "@testing-library/react",
    "@testing-library/user-event",
    "@elastic/eui",
    "@emotion/react",
    "@elastic/ecs",
    "@elastic/numeral",
    "lodash",
    "@emotion/styled",
    "moment-timezone",
    "moment",
    "@elastic/charts",
    "uuid",
    "@tanstack/react-query",
    "rxjs",
    "@testing-library/jest-dom"
  ],
  "x-pack/solutions/security/packages/expandable-flyout": [
    "@testing-library/react",
    "react",
    "@elastic/eui",
    "@emotion/react",
    "lodash",
    "@testing-library/user-event",
    "@emotion/css",
    "react-router-dom",
    "rxjs",
    "react-redux",
    "@reduxjs/toolkit",
    "react-fast-compare",
    "reselect"
  ],
  "x-pack/solutions/security/packages/features": [
    "minimatch"
  ],
  "x-pack/solutions/security/packages/index-adapter": [
    "@elastic/elasticsearch",
    "lodash",
    "@elastic/ecs",
    "rxjs"
  ],
  "x-pack/solutions/security/packages/kbn-cloud-security-posture/graph": [
    "babel-jest",
    "@emotion/react",
    "react",
    "@storybook/addon-actions",
    "rxjs",
    "@tanstack/react-query",
    "webpack-merge",
    "@storybook/manager-api",
    "@storybook/theming",
    "@testing-library/jest-dom",
    "@elastic/eui",
    "@testing-library/react",
    "react-use",
    "@xyflow/react",
    "lodash",
    "@emotion/styled",
    "polished",
    "@storybook/react",
    "@testing-library/user-event",
    "expect",
    "@dagrejs/dagre"
  ],
  "x-pack/solutions/security/packages/kbn-cloud-security-posture/public": [
    "@elastic/eui",
    "@emotion/react",
    "react",
    "react-dom",
    "@testing-library/react",
    "@tanstack/react-query",
    "rxjs",
    "react-router-dom"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-autocomplete": [
    "@testing-library/jest-dom",
    "@testing-library/react",
    "react",
    "@elastic/eui",
    "enzyme",
    "lodash",
    "moment"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-exception-list-components": [
    "@testing-library/jest-dom",
    "@testing-library/react",
    "react",
    "@elastic/eui",
    "@emotion/react",
    "@emotion/css",
    "@emotion/styled"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-hook-utils": [
    "@testing-library/react",
    "react",
    "rxjs"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-io-ts-alerting-types": [
    "io-ts",
    "fp-ts",
    "uuid"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types": [
    "fp-ts",
    "io-ts"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-list-api": [
    "fp-ts"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-list-hooks": [
    "fp-ts",
    "react",
    "@tanstack/react-query",
    "@testing-library/react"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-list-utils": [
    "lodash",
    "uuid"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-t-grid": [
    "lodash"
  ],
  "x-pack/solutions/security/packages/kbn-securitysolution-utils": [
    "uuid",
    "axios",
    "p-limit"
  ],
  "x-pack/solutions/security/packages/navigation": [
    "minimatch",
    "react",
    "@elastic/eui",
    "@emotion/react",
    "@testing-library/react"
  ],
  "x-pack/solutions/security/packages/side-nav": [
    "minimatch",
    "@elastic/eui",
    "@emotion/react",
    "react",
    "@emotion/css",
    "@testing-library/react",
    "classnames",
    "@testing-library/user-event",
    "lodash"
  ],
  "x-pack/solutions/security/packages/storybook/config": [
    "@storybook/addon-actions",
    "@storybook/manager-api",
    "@storybook/theming",
    "jest-mock"
  ],
  "x-pack/solutions/security/packages/upselling": [
    "minimatch",
    "@elastic/eui",
    "react",
    "@testing-library/react",
    "@emotion/react",
    "@emotion/styled",
    "rxjs"
  ],
  "x-pack/solutions/security/plugins/cloud_security_posture": [
    "react",
    "@testing-library/react",
    "history",
    "@tanstack/react-query",
    "react-router-dom",
    "@elastic/eui",
    "@emotion/react",
    "lodash",
    "react-use",
    "chance",
    "@emotion/css",
    "uuid",
    "@testing-library/user-event",
    "semver",
    "@testing-library/jest-dom",
    "moment",
    "@elastic/charts",
    "msw",
    "rxjs",
    "io-ts",
    "@testing-library/dom",
    "@elastic/elasticsearch"
  ],
  "x-pack/solutions/security/plugins/ecs_data_quality_dashboard": [
    "minimatch",
    "rxjs",
    "moment-timezone",
    "fp-ts",
    "io-ts"
  ],
  "x-pack/solutions/security/plugins/elastic_assistant": [
    "@elastic/elasticsearch",
    "axios",
    "p-limit",
    "yargs",
    "@langchain/core",
    "langchain",
    "globby",
    "uuid",
    "lodash",
    "p-retry",
    "rxjs",
    "js-yaml",
    "langsmith",
    "@langchain/langgraph",
    "moment",
    "@elastic/ecs",
    "p-map",
    "@testing-library/react",
    "elastic-apm-node",
    "moment-timezone",
    "expect"
  ],
  "x-pack/solutions/security/plugins/lists": [
    "moment",
    "react",
    "@elastic/eui",
    "enzyme",
    "styled-components",
    "@storybook/addon-actions",
    "@testing-library/react",
    "fast-deep-equal",
    "@tanstack/react-query",
    "uuid",
    "lodash",
    "io-ts",
    "fp-ts",
    "elastic-apm-node",
    "sinon",
    "@hapi/boom",
    "mustache",
    "p-retry"
  ],
  "x-pack/solutions/security/plugins/security_solution_ess": [
    "react",
    "rxjs",
    "@testing-library/react",
    "@elastic/eui"
  ],
  "x-pack/solutions/security/plugins/security_solution_serverless": [
    "react",
    "@elastic/eui",
    "@emotion/react",
    "@testing-library/react",
    "rxjs",
    "@emotion/styled",
    "chance",
    "lodash",
    "node-fetch"
  ],
  "x-pack/solutions/security/plugins/security_solution": [
    "lodash",
    "io-ts",
    "fp-ts",
    "seedrandom",
    "uuid",
    "semver",
    "moment",
    "axios",
    "ipaddr.js",
    "rxjs",
    "@emotion/react",
    "@elastic/eui",
    "react",
    "copy-to-clipboard",
    "@testing-library/react",
    "react-router-dom",
    "react-redux",
    "react-use",
    "react-reverse-portal",
    "styled-components",
    "react-dom",
    "@elastic/charts",
    "@testing-library/user-event",
    "@tanstack/react-query",
    "@testing-library/dom",
    "@emotion/css",
    "@testing-library/jest-dom",
    "remark-parse-no-trim",
    "unified",
    "@emotion/styled",
    "react-router-dom-v5-compat",
    "d3",
    "minimatch",
    "enzyme",
    "deepmerge",
    "fast-deep-equal",
    "@emotion/jest",
    "@elastic/numeral",
    "polished",
    "@hello-pangea/dnd",
    "use-resize-observer",
    "jest-styled-components",
    "moment-timezone",
    "classnames",
    "react-hook-form",
    "query-string",
    "fetch-mock",
    "@storybook/addon-actions",
    "react-markdown",
    "reselect",
    "redux",
    "mustache",
    "memoize-one",
    "history",
    "react-router",
    "typescript-fsa",
    "typescript-fsa-reducers",
    "@reduxjs/toolkit",
    "immer",
    "reduce-reducers",
    "redux-devtools-extension",
    "redux-thunk",
    "@formatjs/intl-utils",
    "react-diff-view",
    "unidiff",
    "diff",
    "json-stable-stringify",
    "sinon",
    "dedent",
    "object-hash",
    "papaparse",
    "d3-scale",
    "i18n-iso-countries",
    "@elastic/ecs",
    "@cypress/grep",
    "cypress-data-session",
    "cypress-recurse",
    "execa",
    "p-retry",
    "@cypress/debugging-proxy",
    "p-map",
    "pretty-ms",
    "@elastic/apm-rum",
    "ts-easing",
    "rbush",
    "suricata-sid-db",
    "react-window",
    "formik",
    "extract-zip",
    "js-yaml",
    "tar",
    "chalk",
    "@hapi/hapi",
    "node-fetch",
    "strip-ansi",
    "inquirer",
    "@elastic/elasticsearch",
    "yargs",
    "minimist",
    "del",
    "globby",
    "xml2js",
    "@langchain/core",
    "cypress",
    "find-cypress-specs",
    "cli-table3",
    "@babel/generator",
    "@babel/parser",
    "@langchain/langgraph",
    "langchain",
    "node-diff3",
    "snakecase-keys",
    "@hapi/boom",
    "elastic-apm-node",
    "set-value",
    "murmurhash",
    "js-sha256",
    "fastest-levenshtein",
    "langsmith",
    "adm-zip",
    "camelcase-keys"
  ],
  "x-pack/solutions/security/plugins/session_view": [
    "react",
    "@elastic/eui",
    "@emotion/react",
    "@testing-library/user-event",
    "@testing-library/react",
    "lodash",
    "uuid",
    "memoize-one",
    "@tanstack/react-query",
    "byte-size",
    "react-use",
    "xterm",
    "use-resize-observer",
    "strip-ansi",
    "history"
  ],
  "x-pack/solutions/security/test": [
    "moment",
    "chance",
    "lodash",
    "expect",
    "fast-deep-equal",
    "@mswjs/http-middleware",
    "msw",
    "uuid"
  ]
}
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2025-06-25 11:09:28 -04:00 committed by GitHub
parent 4b925523bf
commit ea18158480
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 322 additions and 4 deletions

View file

@ -47,8 +47,8 @@ export const configureYargs = () => {
})
.option('group-by', {
alias: 'g',
describe: chalk.magenta('Group results by either owner or source (package/plugin)'),
choices: ['owner', 'source'],
describe: chalk.magenta('Group results by owner, source, or package'),
choices: ['owner', 'source', 'package'],
})
.option('summary', {
alias: 's',

View file

@ -7,7 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { KIBANA_SOLUTIONS } from '@kbn/projects-solutions-groups';
// TODO: This cannot be imported until Kibana supports ESM
// import { KIBANA_SOLUTIONS } from '@kbn/projects-solutions-groups';
const KIBANA_SOLUTIONS = ['observability', 'security', 'search', 'chat'] as const;
export const aggregationGroups: string[] = [
...KIBANA_SOLUTIONS.flatMap((solution) => [

View file

@ -14,6 +14,7 @@ import nodePath from 'path';
import { groupFilesByOwners } from '../../lib/group_by_owners.ts';
import { groupBySource } from '../../lib/group_by_source.ts';
import { groupByPackage } from '../../lib/group_by_package.ts';
import { createCollapseRegexWithDepth } from '../../lib/collapse_with_depth.ts';
import { aggregationGroups, excludePaths } from '../common/constants.ts';
@ -116,6 +117,10 @@ export async function identifyDependencyUsageWithCruiser(
return groupFilesByOwners(violations);
}
if (groupBy === 'package') {
return groupByPackage(violations);
}
if (dependencyName) {
const dependencyRegex = new RegExp(`node_modules/${dependencyName}`);

View file

@ -0,0 +1,231 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import fs from 'fs';
import path from 'path';
import { groupByPackage } from './group_by_package';
// Mock fs and path modules
jest.mock('fs');
jest.mock('path');
describe('groupByPackage', () => {
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
});
it('should group dependencies by their package directories', () => {
// Mock path.dirname to simulate directory structure
const mockDirname = jest.fn().mockImplementation((filePath) => {
if (filePath === 'src/package1/file1.js' || filePath === 'src/package1/sub/file2.js') {
// First call for the file's immediate directory
if (filePath === 'src/package1/file1.js') return 'src/package1';
if (filePath === 'src/package1/sub/file2.js') return 'src/package1/sub';
} else if (filePath === 'src/package1/sub') {
// When checking parent directory of 'src/package1/sub'
return 'src/package1';
} else if (filePath === 'src/package2/file2.js') {
return 'src/package2';
}
// Default implementation
return filePath;
});
// Mock path.join to handle path concatenation
const mockJoin = jest
.fn()
.mockImplementation((...args) => args.join('/').replace(/\/\//g, '/'));
// Mock path.parse to handle root directory detection
const mockParse = jest.fn().mockReturnValue({ root: '/' });
// Mock fs.existsSync to simulate kibana.jsonc files
const mockExistsSync = jest.fn().mockImplementation((filePath) => {
if (filePath === 'src/package1/kibana.jsonc') return true;
if (filePath === 'src/package2/kibana.jsonc') return true;
// No kibana.jsonc in 'src/package1/sub'
if (filePath === 'src/package1/sub/kibana.jsonc') return false;
return false;
});
// Apply mocks
(path.dirname as jest.Mock).mockImplementation(mockDirname);
(path.join as jest.Mock).mockImplementation(mockJoin);
(path.parse as jest.Mock).mockImplementation(mockParse);
(fs.existsSync as jest.Mock).mockImplementation(mockExistsSync);
const dependencies = [
{ from: 'src/package1/file1.js', to: 'node_modules/module1' },
{ from: 'src/package1/sub/file2.js', to: 'node_modules/module2' },
{ from: 'src/package2/file2.js', to: 'node_modules/module3' },
];
const result = groupByPackage(dependencies);
expect(result).toEqual({
'src/package1': ['module1', 'module2'],
'src/package2': ['module3'],
});
});
it('should handle a dependency with no package directory', () => {
// Mock directoryname to return consistent paths
const mockDirname = jest.fn().mockImplementation((filePath) => {
if (filePath === 'src/no-package/file.js') return 'src/no-package';
return filePath;
});
// Mock path.join for consistent behavior
const mockJoin = jest
.fn()
.mockImplementation((...args) => args.join('/').replace(/\/\//g, '/'));
// Mock path.parse to handle root directory detection
const mockParse = jest.fn().mockReturnValue({ root: '/' });
// Mock fs.existsSync to return false (no kibana.jsonc exists)
const mockExistsSync = jest.fn().mockReturnValue(false);
// Apply mocks
(path.dirname as jest.Mock).mockImplementation(mockDirname);
(path.join as jest.Mock).mockImplementation(mockJoin);
(path.parse as jest.Mock).mockImplementation(mockParse);
(fs.existsSync as jest.Mock).mockImplementation(mockExistsSync);
const dependencies = [{ from: 'src/no-package/file.js', to: 'node_modules/module1' }];
const result = groupByPackage(dependencies);
// Should fall back to the file's directory when no package found
expect(result).toEqual({
'src/no-package': ['module1'],
});
});
it('should group multiple dependencies from files in the same package', () => {
// Mock path.dirname for consistent behavior
const mockDirname = jest.fn().mockImplementation((filePath) => {
if (filePath.startsWith('src/package1/')) return 'src/package1';
return filePath;
});
// Mock path.join for path concatenation
const mockJoin = jest
.fn()
.mockImplementation((...args) => args.join('/').replace(/\/\//g, '/'));
// Mock path.parse for root directory
const mockParse = jest.fn().mockReturnValue({ root: '/' });
// Mock fs.existsSync to simulate kibana.jsonc file
const mockExistsSync = jest.fn().mockImplementation((filePath) => {
return filePath === 'src/package1/kibana.jsonc';
});
// Apply mocks
(path.dirname as jest.Mock).mockImplementation(mockDirname);
(path.join as jest.Mock).mockImplementation(mockJoin);
(path.parse as jest.Mock).mockImplementation(mockParse);
(fs.existsSync as jest.Mock).mockImplementation(mockExistsSync);
const dependencies = [
{ from: 'src/package1/file1.js', to: 'node_modules/module1' },
{ from: 'src/package1/file2.js', to: 'node_modules/module2' },
{ from: 'src/package1/nested/file3.js', to: 'node_modules/module3' },
];
const result = groupByPackage(dependencies);
expect(result).toEqual({
'src/package1': ['module1', 'module2', 'module3'],
});
});
it('should remove "node_modules/" prefix from dependencies', () => {
// Mock path.dirname for consistent behavior
const mockDirname = jest.fn().mockReturnValue('src/package1');
// Mock path.join for path concatenation
const mockJoin = jest
.fn()
.mockImplementation((...args) => args.join('/').replace(/\/\//g, '/'));
// Mock path.parse for root directory
const mockParse = jest.fn().mockReturnValue({ root: '/' });
// Mock fs.existsSync to simulate kibana.jsonc file
const mockExistsSync = jest.fn().mockReturnValue(true);
// Apply mocks
(path.dirname as jest.Mock).mockImplementation(mockDirname);
(path.join as jest.Mock).mockImplementation(mockJoin);
(path.parse as jest.Mock).mockImplementation(mockParse);
(fs.existsSync as jest.Mock).mockImplementation(mockExistsSync);
const dependencies = [
{ from: 'src/package1/file1.js', to: 'node_modules/module1' },
{ from: 'src/package1/file1.js', to: 'node_modules/module2' },
];
const result = groupByPackage(dependencies);
expect(result).toEqual({
'src/package1': ['module1', 'module2'],
});
});
it('should return an empty object if there are no dependencies', () => {
const result = groupByPackage([]);
expect(result).toEqual({});
});
it('should handle multiple packages in nested directory structure', () => {
// Mock path.dirname to simulate directory structure
const mockDirname = jest.fn().mockImplementation((filePath) => {
if (filePath === 'src/parent/package1/file1.js') return 'src/parent/package1';
if (filePath === 'src/parent/package2/file2.js') return 'src/parent/package2';
return path.dirname(filePath);
});
// Mock path.join to handle path concatenation
const mockJoin = jest
.fn()
.mockImplementation((...args) => args.join('/').replace(/\/\//g, '/'));
// Mock path.parse to handle root directory detection
const mockParse = jest.fn().mockReturnValue({ root: '/' });
// Mock fs.existsSync to simulate kibana.jsonc files
const mockExistsSync = jest.fn().mockImplementation((filePath) => {
if (filePath === 'src/parent/package1/kibana.jsonc') return true;
if (filePath === 'src/parent/package2/kibana.jsonc') return true;
return false;
});
// Apply mocks
(path.dirname as jest.Mock).mockImplementation(mockDirname);
(path.join as jest.Mock).mockImplementation(mockJoin);
(path.parse as jest.Mock).mockImplementation(mockParse);
(fs.existsSync as jest.Mock).mockImplementation(mockExistsSync);
const dependencies = [
{ from: 'src/parent/package1/file1.js', to: 'node_modules/module1' },
{ from: 'src/parent/package2/file2.js', to: 'node_modules/module2' },
];
const result = groupByPackage(dependencies);
expect(result).toEqual({
'src/parent/package1': ['module1'],
'src/parent/package2': ['module2'],
});
});
});

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import fs from 'fs';
import path from 'path';
/**
* Determines the package directory for a given file path
* A package is defined as a directory containing a kibana.jsonc file
*
* @param filePath The path of the file to find the package for
* @returns The package directory path or the original path if no package found
*/
function findPackageDirectory(filePath: string): string {
let currentDir = path.dirname(filePath);
const rootDir = path.parse(currentDir).root;
// Traverse up the directory tree until we find a kibana.jsonc file or hit the root
while (true) {
// Check if current directory has kibana.jsonc
if (fs.existsSync(path.join(currentDir, 'kibana.jsonc'))) {
return currentDir;
}
// If we've reached the root and haven't found kibana.jsonc, break the loop
if (currentDir === rootDir) {
break;
}
// Move up to parent directory
const parentDir = path.dirname(currentDir);
// Break if we can't go up any further (safety check)
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
// If no package directory found, return the directory of the original path
return path.dirname(filePath);
}
/**
* Groups dependencies by package (directories containing kibana.jsonc files)
*
* @param dependencies Array of from/to dependency pairs
* @returns Record mapping package paths to their dependencies
*/
export function groupByPackage(dependencies: Array<{ from: string; to: string }>) {
const packageMap = new Map<string, Set<string>>();
for (const dep of dependencies) {
const { from, to } = dep;
// Find the package directory for the source file
const packageDir = findPackageDirectory(from);
if (!packageMap.has(packageDir)) {
packageMap.set(packageDir, new Set());
}
packageMap.get(packageDir)!.add(to.replace(/^node_modules\//, ''));
}
// Convert the map to a record
const result: Record<string, string[]> = {};
for (const [packageDir, deps] of packageMap.entries()) {
result[packageDir] = Array.from(deps);
}
return result;
}