Dependency usage CLI (#198920)

## Summary


[dependency-cruiser](https://github.com/sverweij/dependency-cruiser/tree/main)
is used for building dependency graph.

### Show all dependencies for a specific package/plugin or directory

#### Run for all plugins
```bash
bash scripts/dependency_usage.sh -p x-pack/plugins -o ./tmp/deps-result-all.json
```

#### Run for single plugin
```bash
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -o ./tmp/deps-result-single.json
```

#### Run for multiple plugins
```bash
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution x-pack/plugins/security -o ./tmp/deps-result-multiple.json
```

#### Run for `x-pack/packages`
```bash
bash scripts/dependency_usage.sh -p x-pack/packages -o ./tmp/deps-packages-1.json
```

#### Run for `packages`
```bash
bash scripts/dependency_usage.sh -p packages -o ./tmp/deps-packages-2.json
```

#### Benchmark

| Analysis              | Real Time   | User Time   | Sys Time   |
|-----------------------|-------------|-------------|------------|
| All plugins           | 7m 21.126s  | 7m 53.099s  | 20.581s    |
| Single plugin         | 31.360s     | 45.352s     | 2.208s     |
| Multiple plugins      | 36.403s     | 50.563s     | 2.814s     |
| x-pack/packages       | 6.638s      | 12.646s     | 0.654s     |
| packages              | 25.744s     | 39.073s     | 2.191s     |


#### Show all packages/plugins within a directory that use a specific
dependency

```sh
bash scripts/dependency_usage.sh -d rxjs -p x-pack/plugins/security_solution
```
---
#### Show all packages/plugins within a directory grouped by code owner
```sh
bash scripts/dependency_usage.sh -d rxjs -p x-pack/plugins -g owner
```
---

#### Group by code owner with adjustable collapse depth for fine-grained
grouping
**Fine-grained grouping**:
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 4
```
**Collapsed grouping**: groups the results under a higher-level owner
(e.g., `security_solution` as a single group).
```bash
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 1
```
---

#### Show all dependencies matching a pattern (e.g., `react-*`) within a
package
```bash
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -d 'react-*' -o ./tmp/result.json
```

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

__Related: https://github.com/elastic/kibana/issues/196767__

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Elena Shostak 2024-11-25 14:07:40 +01:00 committed by GitHub
parent 5d6d45c341
commit 34bf83b54f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1779 additions and 26 deletions

View file

@ -2015,6 +2015,15 @@ module.exports = {
'@kbn/imports/no_group_crossing_imports': 'warn',
},
},
{
files: ['packages/kbn-dependency-usage/**/*.{ts,tsx}'],
rules: {
// disabling it since package is a CLI tool
'no-console': 'off',
// disabling it since package is marked as module and it requires extension for files written
'@kbn/imports/uniform_imports': 'off',
},
},
],
};

1
.github/CODEOWNERS vendored
View file

@ -329,6 +329,7 @@ packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-di
packages/kbn-data-stream-adapter @elastic/security-threat-hunting
packages/kbn-data-view-utils @elastic/kibana-data-discovery
packages/kbn-datemath @elastic/kibana-data-discovery
packages/kbn-dependency-usage @elastic/kibana-security
packages/kbn-dev-cli-errors @elastic/kibana-operations
packages/kbn-dev-cli-runner @elastic/kibana-operations
packages/kbn-dev-proc-runner @elastic/kibana-operations

2
.gitignore vendored
View file

@ -22,6 +22,7 @@ target
*.iml
*.log
types.eslint.config.js
types.eslint.config.cjs
__tmp__
# Ignore example plugin builds
@ -159,3 +160,4 @@ x-pack/test/security_solution_playwright/playwright/.cache/
x-pack/test/security_solution_playwright/.auth/
x-pack/test/security_solution_playwright/.env
.codeql
.dependency-graph-log.json

View file

@ -1429,6 +1429,7 @@
"@kbn/core-ui-settings-server-mocks": "link:packages/core/ui-settings/core-ui-settings-server-mocks",
"@kbn/core-usage-data-server-mocks": "link:packages/core/usage-data/core-usage-data-server-mocks",
"@kbn/cypress-config": "link:packages/kbn-cypress-config",
"@kbn/dependency-usage": "link:packages/kbn-dependency-usage",
"@kbn/dev-cli-errors": "link:packages/kbn-dev-cli-errors",
"@kbn/dev-cli-runner": "link:packages/kbn-dev-cli-runner",
"@kbn/dev-proc-runner": "link:packages/kbn-dev-proc-runner",
@ -1708,6 +1709,7 @@
"cypress-recurse": "^1.35.2",
"date-fns": "^2.29.3",
"dependency-check": "^4.1.0",
"dependency-cruiser": "^16.4.2",
"ejs": "^3.1.10",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",

View file

@ -0,0 +1,153 @@
# @kbn/dependency-usage
A CLI tool for analyzing dependencies across packages and plugins. This tool provides commands to check dependency usage, aggregate it, debug dependency graphs, and more.
---
## Table of Contents
1. [Show all packages/plugins using a dependency](#show-all-packagesplugins-using-a-dependency)
2. [Show dependencies grouped by code owner](#show-dependencies-grouped-by-code-owner)
3. [List all dependencies for a package or directory](#list-all-dependencies-for-source-directory)
4. [Group by code owner with adjustable collapse depth](#group-by-code-owner-with-adjustable-collapse-depth)
5. [Show dependencies matching a pattern](#show-dependencies-matching-a-pattern)
6. [Verbose flag to debug dependency graph issues](#verbose-flag-to-debug-dependency-graph-issues)
---
### 1. Show all packages/plugins using a specific dependency
Use this command to list all packages or plugins within a directory that use a specified dependency.
```sh
bash scripts/dependency_usage.sh -d <dependency> -p <path_to_directory>
```
or
```sh
bash scripts/dependency_usage.sh --dependency-name <dependency> --paths <path_to_directory>
```
**Example**:
```sh
bash scripts/dependency_usage.sh -d rxjs -p x-pack/plugins/security_solution
```
- `-d rxjs`: Specifies the dependency to look for (`rxjs`).
- `-p x-pack/plugins/security_solution`: Sets the directory to search within (`x-pack/plugins/security_solution`).
---
### 2. Show dependencies grouped by code owner
Group the dependencies used within a directory by code owner.
```sh
bash scripts/dependency_usage.sh -p <path_to_directory> -g owner
```
or
```sh
bash scripts/dependency_usage.sh --paths <path_to_directory> --group-by owner
```
**Example**:
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins -g owner
```
- `-p x-pack/plugins`: Sets the directory to scan for plugins using this dependency.
- `-g owner`: Groups results by code owner.
- **Output**: Lists all dependencies for `x-pack/plugins`, organized by code owner.
---
### 3. List all dependencies for source directory
To display all dependencies used within a specific directory.
```sh
bash scripts/dependency_usage.sh -p <path_to_directory>
```
or
```sh
bash scripts/dependency_usage.sh --paths <path_to_directory>
```
**Example**:
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution
```
- `-p x-pack/plugins/security_solution`: Specifies the package or directory for which to list all dependencies.
- **Output**: Lists all dependencies for `x-pack/plugins/security_solution`.
---
### 4. Group by code owner with adjustable collapse depth
When a package or plugin has multiple subteams, use the `--collapse-depth` option to control how granular the grouping by code owner should be.
#### Detailed Subteam Grouping
Shows all subteams within `security_solution`.
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 4
```
#### Collapsed Grouping
Groups the results under a higher-level owner (e.g., `security_solution` as a single group).
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 1
```
**Explanation**:
- `-p x-pack/plugins/security_solution`: Specifies the directory to scan.
- `-g owner`: Groups results by code owner.
- `--collapse-depth`: Defines the depth for grouping, where higher numbers show more granular subteams.
- **Output**: Lists dependencies grouped by code owner at different levels of depth based on the `--collapse-depth` value.
---
### 5. Show dependencies matching a pattern
Search for dependencies that match a specific pattern (such as `react-*`) within a package and output the results to a specified file.
```sh
bash scripts/dependency_usage.sh -p <path_to_directory> -d '<pattern>' -o <output_file>
```
**Example**:
```sh
bash scripts/dependency_usage.sh -d 'react-*' -p x-pack/plugins/security_solution -o ./tmp/results.json
```
- `-p x-pack/plugins/security_solution`: Specifies the directory or package to search within.
- `-d 'react-*'`: Searches for dependencies that match the pattern `react-*`.
- `-o ./tmp/results.json`: Outputs the results to a specified file (`results.json` in the `./tmp` directory).
- **Output**: Saves a list of all dependencies matching `react-*` in `x-pack/plugins/security_solution` to `./tmp/results.json`.
---
### 6. Verbose flag to debug dependency graph issues
Enable verbose mode to log additional details for debugging dependency graphs. This includes generating a non-aggregated dependency graph in `.dependency-graph-log.json`.
```sh
bash scripts/dependency_usage.sh -p <path_to_directory> -o <output_file> -v
```
**Example**:
```sh
bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -o ./tmp/results.json
```
- `-p x-pack/plugins/security_solution`: Specifies the target directory or package to analyze.
- `-o ./tmp/results.json`: Saves the output to the `results.json` file in the `./tmp` directory.
- `-v`: Enables verbose mode.
**Output**: Saves a list of all dependencies in `x-pack/plugins/security_solution` to `./tmp/results.json`. Additionally, it logs a detailed, non aggregated dependency graph to `.dependency-graph-log.json` for debugging purposes.
---
For further information on additional flags and options, refer to the script's help command.

View file

@ -0,0 +1,15 @@
/*
* 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".
*/
/* eslint-disable no-restricted-syntax */
export default {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-dependency-usage'],
};

View file

@ -0,0 +1,6 @@
{
"devOnly": true,
"type": "shared-common",
"id": "@kbn/dependency-usage",
"owner": "@elastic/kibana-security"
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"name": "@kbn/dependency-usage",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"type": "module",
"exports": {
"./src/*": "./src/*"
}
}

View file

@ -0,0 +1,97 @@
/*
* 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 { identifyDependencyUsageWithCruiser } from './dependency_graph/providers/cruiser.ts';
import { configureYargs } from './cli';
jest.mock('chalk', () => ({
green: jest.fn((str) => str),
yellow: jest.fn((str) => str),
cyan: jest.fn((str) => str),
magenta: jest.fn((str) => str),
blue: jest.fn((str) => str),
bold: { magenta: jest.fn((str) => str), blue: jest.fn((str) => str) },
}));
jest.mock('./dependency_graph/providers/cruiser', () => ({
identifyDependencyUsageWithCruiser: jest.fn(),
}));
jest.mock('./cli', () => ({
...jest.requireActual('./cli'),
runCLI: jest.fn(),
}));
describe('dependency-usage CLI', () => {
const parser = configureYargs()
.fail((message: string) => {
throw new Error(message);
})
.exitProcess(false);
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should handle verbose option', () => {
const argv = parser.parse(['--paths', './plugins', '--verbose']);
expect(argv.verbose).toBe(true);
expect(identifyDependencyUsageWithCruiser).toHaveBeenCalledWith(
expect.any(Array),
undefined,
expect.objectContaining({ isVerbose: true })
);
});
it('should group results by specified group-by option', () => {
const argv = parser.parse(['--paths', './src', '--group-by', 'owner']);
expect(argv['group-by']).toBe('owner');
expect(identifyDependencyUsageWithCruiser).toHaveBeenCalledWith(
expect.any(Array),
undefined,
expect.objectContaining({ groupBy: 'owner' })
);
});
it('should use default values when optional arguments are not provided', () => {
const argv = parser.parse([]);
expect(argv.paths).toEqual(['.']);
expect(argv['dependency-name']).toBeUndefined();
expect(argv['collapse-depth']).toBe(1);
expect(argv.verbose).toBe(false);
});
it('should throw an error if summary is used without dependency-name', () => {
expect(() => {
parser.parse(['--summary', '--paths', './src']);
}).toThrow('Summary option can only be used when a dependency name is provided');
});
it('should validate collapse-depth as a positive integer', () => {
expect(() => {
parser.parse(['--paths', './src', '--collapse-depth', '0']);
}).toThrow('Collapse depth must be a positive integer');
});
it('should output results to specified output path', () => {
const argv = parser.parse(['--paths', './src', '--output-path', './output.json']);
expect(argv['output-path']).toBe('./output.json');
});
it('should print results to console if no output path is specified', () => {
const argv = parser.parse(['--paths', './src']);
expect(argv['output-path']).toBeUndefined();
});
});

View file

@ -0,0 +1,169 @@
/*
* 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 nodePath from 'path';
import yargs from 'yargs';
import chalk from 'chalk';
import fs from 'fs';
import { identifyDependencyUsageWithCruiser } from './dependency_graph/providers/cruiser.ts';
interface CLIArgs {
dependencyName?: string;
paths: string[];
groupBy: string;
summary: boolean;
outputPath: string;
collapseDepth: number;
tool: string;
verbose: boolean;
}
export const configureYargs = () => {
return yargs(process.argv.slice(2))
.command(
'*',
chalk.green('Identify the usage of a dependency in the given paths and output as JSON'),
(y) => {
y.version(false)
.option('dependency-name', {
alias: 'd',
describe: chalk.yellow('The name of the dependency to search for'),
type: 'string',
demandOption: false,
})
.option('paths', {
alias: 'p',
describe: chalk.cyan('The paths to search within (can be multiple)'),
type: 'string',
array: true,
default: ['.'],
})
.option('group-by', {
alias: 'g',
describe: chalk.magenta('Group results by either owner or source (package/plugin)'),
choices: ['owner', 'source'],
})
.option('summary', {
alias: 's',
describe: chalk.magenta(
'Output a summary instead of full details. Applies only when a dependency name is provided'
),
type: 'boolean',
})
.option('collapse-depth', {
alias: 'c',
describe: chalk.blue('Specify the directory depth level for collapsing'),
type: 'number',
default: 1,
})
.option('output-path', {
alias: 'o',
describe: chalk.blue('Specify the output file to save results as JSON'),
type: 'string',
})
.option('verbose', {
alias: 'v',
describe: chalk.blue('Outputs verbose graph details to a file'),
type: 'boolean',
default: false,
})
.check(({ summary, dependencyName, collapseDepth }: Partial<CLIArgs>) => {
if (summary && !dependencyName) {
throw new Error('Summary option can only be used when a dependency name is provided');
}
if (collapseDepth !== undefined && collapseDepth <= 0) {
throw new Error('Collapse depth must be a positive integer');
}
return true;
})
.example(
'--dependency-name lodash --paths ./src ./lib',
chalk.blue(
'Searches for "lodash" usage in the ./src and ./lib directories and outputs as JSON'
)
);
},
async (argv: CLIArgs) => {
const {
dependencyName,
paths,
groupBy,
summary,
collapseDepth,
outputPath,
verbose: isVerbose,
} = argv;
if (dependencyName) {
console.log(
`Searching for dependency ${chalk.bold.magenta(
dependencyName
)} in paths: ${chalk.bold.magenta(paths.join(', '))}`
);
} else {
console.log(
`Searching for dependencies in paths: ${chalk.bold.magenta(paths.join(', '))}`
);
}
if (collapseDepth > 1) {
console.log(`Dependencies will be collapsed to depth: ${chalk.bold.blue(collapseDepth)}`);
}
try {
console.log(`${chalk.bold.magenta('cruiser')} is used for building dependency graph`);
const result = await identifyDependencyUsageWithCruiser(paths, dependencyName, {
groupBy,
summary,
collapseDepth,
isVerbose,
});
if (outputPath) {
const isJsonFile = nodePath.extname(outputPath) === '.json';
const outputFile = isJsonFile
? outputPath
: nodePath.join(outputPath, 'dependency-usage.json');
const outputDir = nodePath.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFile(outputFile, JSON.stringify(result, null, 2), (err) => {
if (err) {
console.error(chalk.red(`Failed to save results to ${outputFile}: ${err.message}`));
} else {
console.log(chalk.green(`Results successfully saved to ${outputFile}`));
}
});
} else {
console.log(chalk.yellow('No output file specified, displaying results below:\n'));
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
console.error('Error fetching dependency usage:', error.message);
}
}
)
.help();
};
export const runCLI = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
configureYargs().argv;
};
if (!process.env.JEST_WORKER_ID) {
runCLI();
}

View file

@ -0,0 +1,37 @@
/*
* 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".
*/
export const aggregationGroups = [
'x-pack/plugins',
'x-pack/packages',
'src/plugins',
'packages',
'src',
'x-pack/test',
'x-pack/test_serverless',
];
export const excludePaths = [
'(^|/)target($|/)',
'^kbn',
'^@kbn',
'^.buildkite',
'^docs',
'^dev_docs',
'^examples',
'^scripts',
'^bazel',
'^x-pack/examples',
'^oas_docs',
'^api_docs',
'^kbn_pm',
'^.es',
'^.codeql',
'^.github',
];

View file

@ -0,0 +1,10 @@
/*
* 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".
*/
export { identifyDependencyUsageWithCruiser } from './providers/cruiser.ts';

View file

@ -0,0 +1,354 @@
/*
* 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 { identifyDependencyUsageWithCruiser as identifyDependencyUsage } from './cruiser.ts';
import { cruise } from 'dependency-cruiser';
import * as groupBy from '../../lib/group_by_owners.ts';
import * as groupBySource from '../../lib/group_by_source.ts';
const codeOwners: Record<string, string[]> = {
'plugins/security': ['team_security'],
'plugins/data_visualization': ['team_visualization'],
'plugins/data_charts': ['team_visualization'],
'plugins/analytics': ['team_analytics'],
'plugins/notification': ['team_alerts', 'team_notifications'],
'plugins/security_solution/public/entity_analytics/components': ['team_security_analytics'],
'plugins/security_solution/public/entity_analytics/components/componentA.ts': [
'team_security_analytics',
],
'plugins/security_solution/public/entity_analytics/components/componentB.ts': [
'team_security_analytics',
],
'plugins/security_solution/server/lib/analytics/analytics.ts': ['team_security_analytics'],
'plugins/security_solution/common/api/detection_engine': ['team_security_solution'],
};
jest.mock('dependency-cruiser', () => ({
cruise: jest.fn(),
}));
const mockCruiseResult = {
output: {
summary: {
violations: [
{
from: 'plugins/security',
to: 'node_modules/rxjs',
},
{
from: 'plugins/data_visualization',
to: 'node_modules/rxjs',
},
{
from: 'plugins/data_charts',
to: 'node_modules/rxjs',
},
{
from: 'plugins/analytics',
to: 'node_modules/rxjs',
},
{
from: 'plugins/analytics',
to: 'node_modules/@hapi/boom',
},
],
},
modules: [
{
source: 'node_modules/rxjs',
dependents: [
'plugins/security/server/index.ts',
'plugins/data_charts/public/charts.ts',
'plugins/data_visualization/public/visualization.ts',
'plugins/data_visualization/public/ingest.ts',
'plugins/analytics/server/analytics.ts',
],
},
{
source: 'node_modules/@hapi/boom',
dependents: ['plugins/analytics'],
},
],
},
};
jest.mock('../../lib/code_owners', () => ({
getCodeOwnersForFile: jest.fn().mockImplementation((filePath: string) => codeOwners[filePath]),
getPathsWithOwnersReversed: () => ({}),
}));
describe('identifyDependencyUsage', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should respect collapseDepth param', async () => {
(cruise as jest.Mock).mockResolvedValue(mockCruiseResult);
await identifyDependencyUsage([], 'rxjs', {
groupBy: 'owner',
collapseDepth: 2,
summary: false,
});
await identifyDependencyUsage([], undefined, {
groupBy: 'owner',
collapseDepth: 1,
summary: false,
});
const [, configWithDepth2] = (cruise as jest.Mock).mock.calls[0];
const [, configWithDepth1] = (cruise as jest.Mock).mock.calls[1];
expect(configWithDepth2.collapse).toMatchInlineSnapshot(
`"^(x-pack/plugins|x-pack/packages|src/plugins|packages|src|x-pack/test|x-pack/test_serverless)/([^/]+)/([^/]+)"`
);
expect(configWithDepth1.collapse).toMatchInlineSnapshot(
`"^(x-pack/plugins|x-pack/packages|src/plugins|packages|src|x-pack/test|x-pack/test_serverless)/([^/]+)|^node_modules/(@[^/]+/[^/]+|[^/]+)"`
);
});
it('should group dependencies by codeowners', async () => {
(cruise as jest.Mock).mockResolvedValue(mockCruiseResult);
const groupFilesByOwnersSpy = jest.spyOn(groupBy, 'groupFilesByOwners');
const result = await identifyDependencyUsage([], undefined, {
groupBy: 'owner',
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(groupFilesByOwnersSpy).toHaveBeenCalledWith(mockCruiseResult.output.summary.violations);
expect(result).toEqual({
team_security: {
modules: ['plugins/security'],
deps: ['rxjs'],
teams: ['team_security'],
},
team_visualization: {
modules: ['plugins/data_visualization', 'plugins/data_charts'],
deps: ['rxjs'],
teams: ['team_visualization'],
},
team_analytics: {
modules: ['plugins/analytics'],
deps: ['rxjs', '@hapi/boom'],
teams: ['team_analytics'],
},
});
});
it('should group dependencies by source directory', async () => {
(cruise as jest.Mock).mockResolvedValue(mockCruiseResult);
const groupFilesByOwnersSpy = jest.spyOn(groupBySource, 'groupBySource');
const result = await identifyDependencyUsage([], undefined, {
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(groupFilesByOwnersSpy).toHaveBeenCalledWith(mockCruiseResult.output.summary.violations);
expect(result).toEqual({
'plugins/security': ['rxjs'],
'plugins/data_visualization': ['rxjs'],
'plugins/data_charts': ['rxjs'],
'plugins/analytics': ['rxjs', '@hapi/boom'],
});
});
it('should search for specific dependency and return full dependents list', async () => {
(cruise as jest.Mock).mockResolvedValue(mockCruiseResult);
const result = await identifyDependencyUsage([], 'rxjs', {
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({
modules: [
'plugins/security',
'plugins/data_visualization',
'plugins/data_charts',
'plugins/analytics',
],
dependents: {
rxjs: [
'plugins/security/server/index.ts',
'plugins/data_charts/public/charts.ts',
'plugins/data_visualization/public/visualization.ts',
'plugins/data_visualization/public/ingest.ts',
'plugins/analytics/server/analytics.ts',
],
},
});
});
it('should search for specific dependency and return only summary', async () => {
(cruise as jest.Mock).mockResolvedValue(mockCruiseResult);
const result = await identifyDependencyUsage([], 'rxjs', {
collapseDepth: 1,
summary: true,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({
modules: [
'plugins/security',
'plugins/data_visualization',
'plugins/data_charts',
'plugins/analytics',
],
});
});
it('should handle empty cruise result', async () => {
(cruise as jest.Mock).mockResolvedValue({
output: { summary: { violations: [] }, modules: [] },
});
const result = await identifyDependencyUsage([], undefined, {
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({});
});
it('should handle no violations', async () => {
(cruise as jest.Mock).mockResolvedValue({
output: { summary: { violations: [] }, modules: mockCruiseResult.output.modules },
});
const result = await identifyDependencyUsage([], undefined, {
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({});
});
it('should return empty structure if specific dependency name does not exist', async () => {
(cruise as jest.Mock).mockResolvedValue({
output: { summary: { violations: [] }, modules: mockCruiseResult.output.modules },
});
const result = await identifyDependencyUsage([], 'nonexistent_dependency', {
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({
modules: [],
dependents: {},
});
});
it('should handle unknown ownership when grouping by owner', async () => {
const customCruiseResult = {
output: {
summary: {
violations: [
{ from: 'plugins/unknown_plugin', to: 'node_modules/some_module' },
{ from: 'plugins/security', to: 'node_modules/rxjs' },
],
},
modules: [],
},
};
(cruise as jest.Mock).mockResolvedValue(customCruiseResult);
const result = await identifyDependencyUsage([], undefined, {
groupBy: 'owner',
collapseDepth: 1,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({
unknown: {
modules: ['plugins/unknown_plugin'],
deps: ['some_module'],
teams: ['unknown'],
},
team_security: {
modules: ['plugins/security'],
deps: ['rxjs'],
teams: ['team_security'],
},
});
});
it('should search for specific dependency and group by owner', async () => {
const customCruiseResult = {
output: {
summary: {
violations: [
{
from: 'plugins/security_solution/public/entity_analytics/components/componentA.ts',
to: 'node_modules/lodash/fp.js',
},
{
from: 'plugins/security_solution/public/entity_analytics/components/componentB.ts',
to: 'node_modules/lodash/partition.js',
},
{
from: 'plugins/security_solution/server/lib/analytics/analytics.ts',
to: 'node_modules/lodash/partition.js',
},
{
from: 'plugins/security_solution/server/lib/analytics/analytics.ts',
to: 'node_modules/lodash/cloneDeep.js',
},
{
from: 'plugins/security_solution/common/api/detection_engine',
to: 'node_modules/lodash/sortBy.js',
},
],
},
modules: [],
},
};
(cruise as jest.Mock).mockResolvedValue(customCruiseResult);
const result = await identifyDependencyUsage([], 'lodash', {
groupBy: 'owner',
collapseDepth: 3,
summary: false,
});
expect(cruise).toHaveBeenCalled();
expect(result).toEqual({
team_security_analytics: {
modules: [
'plugins/security_solution/public/entity_analytics/components/componentA.ts',
'plugins/security_solution/public/entity_analytics/components/componentB.ts',
'plugins/security_solution/server/lib/analytics/analytics.ts',
],
deps: ['lodash/fp.js', 'lodash/partition.js', 'lodash/cloneDeep.js'],
teams: ['team_security_analytics'],
},
team_security_solution: {
modules: ['plugins/security_solution/common/api/detection_engine'],
deps: ['lodash/sortBy.js'],
teams: ['team_security_solution'],
},
});
});
});

View file

@ -0,0 +1,147 @@
/*
* 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 chalk from 'chalk';
import { cruise } from 'dependency-cruiser';
import fs from 'fs';
import nodePath from 'path';
import { groupFilesByOwners } from '../../lib/group_by_owners.ts';
import { groupBySource } from '../../lib/group_by_source.ts';
import { createCollapseRegexWithDepth } from '../../lib/collapse_with_depth.ts';
import { aggregationGroups, excludePaths } from '../common/constants.ts';
interface DependencyGraphOptions {
isVerbose?: boolean;
summary?: boolean;
collapseDepth: number;
groupBy?: string;
}
type PathsToAnalyze = string[];
type DependencyName = string | undefined;
const invokeDependencyCruiser = async (
paths: PathsToAnalyze,
dependencyName: DependencyName,
{ summary, collapseDepth }: Omit<DependencyGraphOptions, 'groupBy' | 'verbose'>
) => {
const collapseByNodeModule = !dependencyName || (dependencyName && summary);
const collapseByNodeModuleRegex = '^node_modules/(@[^/]+/[^/]+|[^/]+)';
const collapseRules = [createCollapseRegexWithDepth(aggregationGroups, collapseDepth)];
if (collapseByNodeModule) {
collapseRules.push(collapseByNodeModuleRegex);
}
const captureRule = dependencyName
? {
name: `dependency-usage ${dependencyName}`,
severity: 'info',
from: { pathNot: '^node_modules' },
to: { path: dependencyName },
}
: {
name: 'external-deps',
severity: 'info',
from: { path: paths.map((path) => `^${path}`) },
to: { path: '^node_modules' },
};
const result = await cruise(paths, {
ruleSet: {
// @ts-ignore
forbidden: [captureRule],
},
doNotFollow: {
path: 'node_modules',
},
extensions: ['.ts', '.tsx'],
focus: '^node_modules',
exclude: {
path: excludePaths,
},
onlyReachable: paths.map((path) => `^${path}`).join('|'),
includeOnly: ['^node_modules', ...paths.map((path) => `^${path}`)],
validate: true,
collapse: collapseRules.join('|'),
});
return result;
};
export async function identifyDependencyUsageWithCruiser(
paths: PathsToAnalyze,
dependencyName: string | undefined,
{ groupBy, summary, collapseDepth, isVerbose }: DependencyGraphOptions
) {
const result = await invokeDependencyCruiser(paths, dependencyName, {
summary,
collapseDepth,
});
if (typeof result.output === 'string') {
throw new Error('Unexpected string output from cruise result');
}
console.log(
`${chalk.green(`Successfully`)} built dependency graph using ${chalk.bold.magenta(
'cruiser'
)}. Analyzing...`
);
if (isVerbose) {
const verboseLogPath = nodePath.join(process.cwd(), '.dependency-graph-log.json');
fs.writeFile(verboseLogPath, JSON.stringify(result, null, 2), (err) => {
if (err) {
console.error(
chalk.red(`Failed to save dependency graph log to ${verboseLogPath}: ${err.message}`)
);
} else {
console.log(chalk.yellow(`Dependency graph log saved to ${verboseLogPath}`));
}
});
}
const { violations } = result.output.summary;
if (groupBy === 'owner') {
return groupFilesByOwners(violations);
}
if (dependencyName) {
const dependencyRegex = new RegExp(`node_modules/${dependencyName}`);
const dependentsList = result.output.modules.reduce<Record<string, string[]>>(
(acc, { source, dependents }) => {
if (!dependencyRegex.test(source)) {
return acc;
}
const transformedDependencyName = source.split('/')[1];
if (!acc[transformedDependencyName]) {
acc[transformedDependencyName] = [];
}
acc[transformedDependencyName].push(...dependents);
return acc;
},
{}
);
return {
modules: [...new Set(violations.map(({ from }) => from))],
...(!summary && { dependents: dependentsList }),
};
}
return groupBySource(violations);
}

View file

@ -0,0 +1,99 @@
/*
* 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 { getCodeOwnersForFile, PathWithOwners } from './code_owners';
describe('getCodeOwnersForFile', () => {
it('should return teams for exact file match', () => {
const reversedCodeowners = [
{
path: 'src/file1.js',
teams: ['team_a'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath === 'src/file1.js' }),
},
},
] as PathWithOwners[];
const result = getCodeOwnersForFile('src/file1.js', reversedCodeowners);
expect(result).toEqual(['team_a']);
});
it('should return "unknown" if no ownership is found', () => {
const reversedCodeowners = [
{
path: 'src/file1.js',
teams: ['team_a'],
ignorePattern: { test: (filePath: string) => ({ ignored: filePath === 'src/file1.js' }) },
},
] as PathWithOwners[];
const result = getCodeOwnersForFile('src/unknown_file.js', reversedCodeowners);
expect(result).toEqual(['unknown']);
});
it('should return teams for partial match if no exact match exists', () => {
const reversedCodeowners = [
{
path: 'src/folder',
teams: ['team_c'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath.startsWith('src/folder') }),
},
},
] as PathWithOwners[];
const result = getCodeOwnersForFile('src/folder/subfolder/file.js', reversedCodeowners);
expect(result).toEqual(['team_c']);
});
it('should handle root directory without ownership but with subdirectory owners', () => {
const reversedCodeowners = [
{
path: 'folder/some/test',
teams: ['team_a'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath.startsWith('folder/some/test') }),
},
},
{
path: 'folder/another/test',
teams: ['team_b'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath.startsWith('folder/another/test') }),
},
},
] as PathWithOwners[];
const result = getCodeOwnersForFile('folder', reversedCodeowners);
expect(result).toEqual(['team_a', 'team_b']);
});
it('should return all unique teams if multiple subdirectories match', () => {
const reversedCodeowners = [
{
path: 'folder/some/test',
teams: ['team_a'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath.startsWith('folder/some/test') }),
},
},
{
path: 'folder/another/test',
teams: ['team_b'],
ignorePattern: {
test: (filePath: string) => ({ ignored: filePath.startsWith('folder/another/test') }),
},
},
] as PathWithOwners[];
const result = getCodeOwnersForFile('folder/another/test/file.js', reversedCodeowners);
expect(result).toEqual(['team_b']);
});
});

View file

@ -0,0 +1,84 @@
/*
* 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".
*/
// @ts-ignore
import { REPO_ROOT } from '@kbn/repo-info';
import { join as joinPath } from 'path';
import { existsSync, readFileSync } from 'fs';
import type { Ignore } from 'ignore';
import ignore from 'ignore';
export interface PathWithOwners {
path: string;
teams: string[];
ignorePattern: Ignore;
}
const existOrThrow = (targetFile: string) => {
if (existsSync(targetFile) === false)
throw Error(`Unable to determine code owners: file ${targetFile} Not Found`);
};
/**
* Get the .github/CODEOWNERS entries, prepared for path matching.
* The last matching CODEOWNERS entry has highest precedence:
* https://help.github.com/articles/about-codeowners/
* so entries are returned in reversed order to later search for the first match.
*/
export function getPathsWithOwnersReversed(): PathWithOwners[] {
const codeownersPath = joinPath(REPO_ROOT, '.github', 'CODEOWNERS');
existOrThrow(codeownersPath);
const codeownersContent = readFileSync(codeownersPath, { encoding: 'utf8', flag: 'r' });
const codeownersLines = codeownersContent.split(/\r?\n/);
const codeowners = codeownersLines
.map((line) => line.trim())
.filter((line) => line && line[0] !== '#');
const pathsWithOwners: PathWithOwners[] = codeowners.map((c) => {
const [path, ...ghTeams] = c.split(/\s+/);
const cleanedPath = path.replace(/\/$/, ''); // remove trailing slash
const parsedTeams = ghTeams
.map((t) => t.replace('@', '').split(','))
.flat()
.filter((t) => t.startsWith('elastic'));
return {
path: cleanedPath,
teams: parsedTeams,
// register CODEOWNERS entries with the `ignores` lib for later path matching
ignorePattern: ignore().add([cleanedPath]),
};
});
return pathsWithOwners.reverse();
}
export function getCodeOwnersForFile(
filePath: string,
reversedCodeowners?: PathWithOwners[]
): string[] {
const pathsWithOwners = reversedCodeowners ?? getPathsWithOwnersReversed();
const match = pathsWithOwners.find((p) => p.ignorePattern.test(filePath).ignored);
if (!match?.teams.length) {
const allTeams = pathsWithOwners
.filter((p) => p.path.includes(filePath) && p.teams.length)
.map((p) => p.teams)
.flat();
if (!allTeams.length) {
return ['unknown'];
}
return [...new Set(allTeams)];
}
return match?.teams ?? [];
}

View file

@ -0,0 +1,36 @@
/*
* 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 { createCollapseRegexWithDepth } from './collapse_with_depth';
describe('createCollapseRegexWithDepth', () => {
it('should generate regex with a base path and depth of 0', () => {
const basePath = ['app/components'];
const depth = 0;
const regex = createCollapseRegexWithDepth(basePath, depth);
expect(regex).toBe('^(app/components)');
});
it('should generate regex with a base path and depth of 1', () => {
const basePath = ['src'];
const depth = 1;
const regex = createCollapseRegexWithDepth(basePath, depth);
expect(regex).toBe('^(src)/([^/]+)');
});
it('should generate regex with a base path and depth of 2', () => {
const basePath = ['src'];
const depth = 2;
const regex = createCollapseRegexWithDepth(basePath, depth);
expect(regex).toBe('^(src)/([^/]+)/([^/]+)');
});
});

View file

@ -0,0 +1,18 @@
/*
* 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".
*/
export function createCollapseRegexWithDepth(basePaths: string[], depth: number) {
let regex = `^(${basePaths.join('|')})`;
for (let i = 0; i < depth; i++) {
regex += `/([^/]+)`;
}
return regex;
}

View file

@ -0,0 +1,116 @@
/*
* 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 { groupFilesByOwners } from './group_by_owners';
jest.mock('./code_owners', () => ({
getPathsWithOwnersReversed: jest.fn(),
getCodeOwnersForFile: jest.fn((file: string) => {
const owners: Record<string, string[]> = {
'/src/file1.js': ['team_a'],
'/src/file2.js': ['team_b'],
'/src/file3.js': ['team_a', 'team_c'],
};
return owners[file];
}),
}));
describe('groupFilesByOwners', () => {
it('should group files by single owners correctly', () => {
const dependencies = [
{ from: '/src/file1.js', to: 'node_modules/module1' },
{ from: '/src/file2.js', to: 'node_modules/module2' },
];
const result = groupFilesByOwners(dependencies);
expect(result).toEqual({
team_a: {
modules: ['/src/file1.js'],
deps: ['module1'],
teams: ['team_a'],
},
team_b: {
modules: ['/src/file2.js'],
deps: ['module2'],
teams: ['team_b'],
},
});
});
it('should group files with multiple owners under "multiple_teams"', () => {
const dependencies = [
{ from: '/src/file3.js', to: 'node_modules/module3' },
{ from: '/src/file3.js', to: 'node_modules/module4' },
];
const result = groupFilesByOwners(dependencies);
expect(result).toEqual({
multiple_teams: [
{
modules: ['/src/file3.js'],
deps: ['module3', 'module4'],
teams: ['team_a', 'team_c'],
},
],
});
});
it('should handle files with unknown owners', () => {
const dependencies = [{ from: '/src/file_unknown.js', to: 'node_modules/module_unknown' }];
const result = groupFilesByOwners(dependencies);
expect(result).toEqual({
unknown: {
modules: ['/src/file_unknown.js'],
deps: ['module_unknown'],
teams: ['unknown'],
},
});
});
it('should correctly handle mixed ownership scenarios', () => {
const dependencies = [
{ from: '/src/file1.js', to: 'node_modules/module1' },
{ from: '/src/file2.js', to: 'node_modules/module2' },
{ from: '/src/file3.js', to: 'node_modules/module3' },
{ from: '/src/file3.js', to: 'node_modules/module4' },
{ from: '/src/file_unknown.js', to: 'node_modules/module_unknown' },
];
const result = groupFilesByOwners(dependencies);
expect(result).toEqual({
team_a: {
modules: ['/src/file1.js'],
deps: ['module1'],
teams: ['team_a'],
},
team_b: {
modules: ['/src/file2.js'],
deps: ['module2'],
teams: ['team_b'],
},
multiple_teams: [
{
modules: ['/src/file3.js'],
deps: ['module3', 'module4'],
teams: ['team_a', 'team_c'],
},
],
unknown: {
modules: ['/src/file_unknown.js'],
deps: ['module_unknown'],
teams: ['unknown'],
},
});
});
});

View file

@ -0,0 +1,89 @@
/*
* 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 { getCodeOwnersForFile, getPathsWithOwnersReversed } from './code_owners.ts';
interface DependencyByOwnerEntry<T = string[]> {
modules: T;
deps: T;
teams: T;
}
const UNKNOWN_OWNER = 'unknown';
const MULTIPLE_TEAMS_OWNER = 'multiple_teams';
export function groupFilesByOwners(dependencies: Array<{ from: string; to: string }>) {
const ownerFilesMap = new Map();
const reversedCodeowners = getPathsWithOwnersReversed();
for (const dep of dependencies) {
const { from, to } = dep;
const owners = getCodeOwnersForFile(from, reversedCodeowners) ?? [UNKNOWN_OWNER];
const ownerKey = owners.length > 1 ? MULTIPLE_TEAMS_OWNER : owners[0];
if (ownerKey === MULTIPLE_TEAMS_OWNER) {
if (!ownerFilesMap.has(ownerKey)) {
ownerFilesMap.set(ownerKey, new Map());
}
const modulesMap = ownerFilesMap.get(ownerKey);
if (!modulesMap.has(from)) {
modulesMap.set(from, { deps: new Set(), modules: new Set(), teams: new Set() });
}
const moduleEntry = modulesMap.get(from);
moduleEntry.deps.add(to.replace(/^node_modules\//, ''));
moduleEntry.modules.add(from);
for (const owner of owners) {
moduleEntry.teams.add(owner);
}
continue;
}
if (!ownerFilesMap.has(ownerKey)) {
ownerFilesMap.set(ownerKey, { deps: new Set(), modules: new Set(), teams: new Set(owners) });
}
ownerFilesMap.get(ownerKey).deps.add(to.replace(/^node_modules\//, ''));
ownerFilesMap.get(ownerKey).modules.add(from);
}
const result: Record<string, DependencyByOwnerEntry | DependencyByOwnerEntry[]> = {};
const transformRecord = (entry: DependencyByOwnerEntry<Set<string>>) => ({
modules: Array.from(entry.modules),
deps: Array.from(entry.deps),
teams: Array.from(entry.teams),
});
for (const [key, ownerRecord] of ownerFilesMap.entries()) {
const isMultiTeamRecord = key === MULTIPLE_TEAMS_OWNER;
if (isMultiTeamRecord) {
if (!Array.isArray(result[MULTIPLE_TEAMS_OWNER])) {
result[MULTIPLE_TEAMS_OWNER] = [];
}
for (const [, multiTeamRecord] of ownerRecord.entries()) {
(result[key] as DependencyByOwnerEntry[]).push(transformRecord(multiTeamRecord));
}
continue;
}
result[key] = transformRecord(ownerRecord);
}
return result;
}

View file

@ -0,0 +1,86 @@
/*
* 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 { groupBySource } from './group_by_source.ts';
describe('groupBySource', () => {
it('should group dependencies by their source files', () => {
const dependencies = [
{ from: 'src/file1.js', to: 'node_modules/module1' },
{ from: 'src/file1.js', to: 'node_modules/module2' },
{ from: 'src/file2.js', to: 'node_modules/module3' },
];
const result = groupBySource(dependencies);
expect(result).toEqual({
'src/file1.js': ['module1', 'module2'],
'src/file2.js': ['module3'],
});
});
it('should handle a single dependency', () => {
const dependencies = [{ from: 'src/file1.js', to: 'node_modules/module1' }];
const result = groupBySource(dependencies);
expect(result).toEqual({
'src/file1.js': ['module1'],
});
});
it('should handle multiple dependencies from the same source', () => {
const dependencies = [
{ from: 'src/file1.js', to: 'node_modules/module1' },
{ from: 'src/file1.js', to: 'node_modules/module2' },
{ from: 'src/file1.js', to: 'node_modules/module3' },
];
const result = groupBySource(dependencies);
expect(result).toEqual({
'src/file1.js': ['module1', 'module2', 'module3'],
});
});
it('should handle dependencies from different sources', () => {
const dependencies = [
{ from: 'src/file1.js', to: 'node_modules/module1' },
{ from: 'src/file2.js', to: 'node_modules/module2' },
{ from: 'src/file3.js', to: 'node_modules/module3' },
];
const result = groupBySource(dependencies);
expect(result).toEqual({
'src/file1.js': ['module1'],
'src/file2.js': ['module2'],
'src/file3.js': ['module3'],
});
});
it('should remove "node_modules/" prefix from dependencies', () => {
const dependencies = [
{ from: 'src/file1.js', to: 'node_modules/module1' },
{ from: 'src/file1.js', to: 'node_modules/module2' },
];
const result = groupBySource(dependencies);
expect(result).toEqual({
'src/file1.js': ['module1', 'module2'],
});
});
it('should return an empty object if there are no dependencies', () => {
const result = groupBySource([]);
expect(result).toEqual({});
});
});

View file

@ -0,0 +1,30 @@
/*
* 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".
*/
export function groupBySource(dependencies: Array<{ from: string; to: string }>) {
const packageMap = new Map();
for (const dep of dependencies) {
const { from, to } = dep;
if (!packageMap.has(from)) {
packageMap.set(from, new Set());
}
packageMap.get(from).add(to.replace(/^node_modules\//, ''));
}
const result: Record<string, string[]> = {};
for (const [key, value] of packageMap.entries()) {
result[key] = Array.from(value);
}
return result;
}

View file

@ -0,0 +1,11 @@
/*
* 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".
*/
export { groupFilesByOwners } from './group_by_owners.ts';
export { groupBySource } from './group_by_source.ts';

View file

@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"outDir": "target/types",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"noEmit": true,
"allowImportingTsExtensions": true,
},
"include": ["**/*.ts"],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/repo-info",
],
}

View file

@ -35,6 +35,7 @@ const { parseKbnImportReq } = require('./modern/parse_kbn_import_req');
const { getRepoRels, getRepoRelsSync } = require('./modern/get_repo_rels');
const Jsonc = require('./utils/jsonc');
const { getPluginPackagesFilter, getPluginSearchPaths } = require('./modern/plugins');
const { readPackageJson } = require('./modern/parse_package_json');
module.exports = {
Package,
@ -52,4 +53,5 @@ module.exports = {
parseKbnImportReq,
getRepoRels,
getRepoRelsSync,
readPackageJson,
};

View file

@ -14,6 +14,7 @@ import { REPO_ROOT } from '@kbn/repo-info';
import { makeMatcher } from '@kbn/picomatcher';
import { type Package, findPackageForPath, getRepoRelsSync } from '@kbn/repo-packages';
import { createFailError } from '@kbn/dev-cli-errors';
import { readPackageJson } from '@kbn/repo-packages';
import { readTsConfig, parseTsConfig, TsConfig } from './ts_configfile';
@ -151,6 +152,8 @@ export class TsProject {
public readonly directory: string;
/** the package this tsconfig file is within, if any */
public readonly pkg?: Package;
/** the package is esm or not */
public readonly isEsm?: boolean;
/**
* if this project is within a package then this will
* be set to the import request that maps to the root of this project
@ -187,6 +190,7 @@ export class TsProject {
: undefined;
this._disableTypeCheck = !!opts?.disableTypeCheck;
this.isEsm = readPackageJson(`${this.dir}/package.json`)?.type === 'module';
}
private _name: string | undefined;

7
scripts/dependency_usage.sh Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
# Need to tun the script with ts-node/esm since dependency-cruiser is only available as an ESM module.
# We specify the correct tsconfig.json file to ensure compatibility, as our current setup doesnt fully support ESM modules.
# Should be resolved after https://github.com/elastic/kibana/issues/198790 is done.
NODE_NO_WARNINGS=1 TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=packages/kbn-dependency-usage/tsconfig.json \
node --loader ts-node/esm packages/kbn-dependency-usage/src/cli.ts "$@"

View file

@ -28,7 +28,7 @@ export function runEslintWithTypes() {
async ({ log, flags }) => {
const ignoreFilePath = Path.resolve(REPO_ROOT, '.eslintignore');
const configTemplate = Fs.readFileSync(
Path.resolve(__dirname, 'types.eslint.config.template.js'),
Path.resolve(__dirname, 'types.eslint.config.template.cjs'),
'utf8'
);
@ -65,7 +65,7 @@ export function runEslintWithTypes() {
const failures = await Rx.lastValueFrom(
Rx.from(projects).pipe(
mergeMap(async (project) => {
const configFilePath = Path.resolve(project.directory, 'types.eslint.config.js');
const configFilePath = Path.resolve(project.directory, 'types.eslint.config.cjs');
Fs.writeFileSync(
configFilePath,

View file

@ -756,6 +756,8 @@
"@kbn/default-nav-management/*": ["packages/default-nav/management/*"],
"@kbn/default-nav-ml": ["packages/default-nav/ml"],
"@kbn/default-nav-ml/*": ["packages/default-nav/ml/*"],
"@kbn/dependency-usage": ["packages/kbn-dependency-usage"],
"@kbn/dependency-usage/*": ["packages/kbn-dependency-usage/*"],
"@kbn/dev-cli-errors": ["packages/kbn-dev-cli-errors"],
"@kbn/dev-cli-errors/*": ["packages/kbn-dev-cli-errors/*"],
"@kbn/dev-cli-runner": ["packages/kbn-dev-cli-runner"],

184
yarn.lock
View file

@ -5298,6 +5298,10 @@
version "0.0.0"
uid ""
"@kbn/dependency-usage@link:packages/kbn-dependency-usage":
version "0.0.0"
uid ""
"@kbn/dev-cli-errors@link:packages/kbn-dev-cli-errors":
version "0.0.0"
uid ""
@ -13107,11 +13111,23 @@ acorn-import-attributes@^1.9.5:
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn-jsx-walk@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz#a5ed648264e68282d7c2aead80216bfdf232573a"
integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==
acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-loose@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55"
integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==
dependencies:
acorn "^8.11.0"
acorn-node@^1.6.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
@ -13126,10 +13142,12 @@ acorn-walk@^7.0.0, acorn-walk@^7.2.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.4:
version "8.3.4"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7"
integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==
dependencies:
acorn "^8.11.0"
acorn@^6.4.1:
version "6.4.2"
@ -13141,10 +13159,10 @@ acorn@^7.0.0, acorn@^7.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.0.4, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.1, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2, acorn@^8.9.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3"
integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==
address@^1.0.1:
version "1.1.2"
@ -13269,15 +13287,15 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0, ajv@^8.8.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0, ajv@^8.17.1, ajv@^8.8.0:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
dependencies:
fast-deep-equal "^3.1.1"
fast-deep-equal "^3.1.3"
fast-uri "^3.0.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ansi-align@^3.0.0:
version "3.0.1"
@ -16972,6 +16990,34 @@ dependency-check@^4.1.0:
read-package-json "^2.0.10"
resolve "^1.1.7"
dependency-cruiser@^16.4.2:
version "16.4.2"
resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-16.4.2.tgz#586487e1ac355912a0ad2310b830b63054733e01"
integrity sha512-mQZM95WwIvKzYYdj+1RgIBuJ6qbr1cfyzTt62dDJVrWAShfhV9IEkG/Xv4S2iD5sT+Gt3oFWyZjwNufAhcbtWA==
dependencies:
acorn "^8.12.1"
acorn-jsx "^5.3.2"
acorn-jsx-walk "^2.0.0"
acorn-loose "^8.4.0"
acorn-walk "^8.3.4"
ajv "^8.17.1"
commander "^12.1.0"
enhanced-resolve "^5.17.1"
ignore "^6.0.2"
interpret "^3.1.1"
is-installed-globally "^1.0.0"
json5 "^2.2.3"
memoize "^10.0.0"
picocolors "^1.1.0"
picomatch "^4.0.2"
prompts "^2.4.2"
rechoir "^0.8.0"
safe-regex "^2.1.1"
semver "^7.6.3"
teamcity-service-messages "^0.1.14"
tsconfig-paths-webpack-plugin "^4.1.0"
watskeburt "^4.1.0"
dependency-tree@^10.0.9:
version "10.0.9"
resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7"
@ -17670,10 +17716,10 @@ enhanced-resolve@^4.5.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enhanced-resolve@^5.14.1, enhanced-resolve@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787"
integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==
enhanced-resolve@^5.14.1, enhanced-resolve@^5.16.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0:
version "5.17.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@ -18770,6 +18816,11 @@ fast-text-encoding@^1.0.0:
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==
fast-uri@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241"
integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==
fast-xml-parser@4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f"
@ -19779,6 +19830,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl
once "^1.3.0"
path-is-absolute "^1.0.0"
global-directory@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e"
integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==
dependencies:
ini "4.1.1"
global-dirs@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
@ -20789,6 +20847,11 @@ ignore@^5.0.5, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.3.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
ignore@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-6.0.2.tgz#77cccb72a55796af1b6d2f9eb14fa326d24f4283"
integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@ -20885,6 +20948,11 @@ ini@2.0.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
ini@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1"
integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==
ini@^1.3.5, ini@~1.3.0:
version "1.3.7"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
@ -20972,6 +21040,11 @@ interpret@^2.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
interpret@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
intl-messageformat@10.5.12:
version "10.5.12"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.12.tgz#a0c1a20da896b7a1f4ba1b59c8ba5d9943c29c3f"
@ -21296,6 +21369,14 @@ is-hexadecimal@^1.0.0:
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69"
integrity sha1-bghLvJIGH7sJcexYts5tQE4k2mk=
is-installed-globally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-1.0.0.tgz#08952c43758c33d815692392f7f8437b9e436d5a"
integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==
dependencies:
global-directory "^4.0.1"
is-path-inside "^4.0.0"
is-installed-globally@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
@ -21412,6 +21493,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-path-inside@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db"
integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==
is-plain-obj@2.1.0, is-plain-obj@^2.0.0, is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
@ -23775,6 +23861,13 @@ memoize-one@^6.0.0:
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
memoize@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.0.0.tgz#43fa66b2022363c7c50cf5dfab732a808a3d7147"
integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==
dependencies:
mimic-function "^5.0.0"
memoizerific@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
@ -25936,16 +26029,21 @@ picocolors@^0.2.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picocolors@^1.0.0, picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
pidusage@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-3.0.2.tgz#6faa5402b2530b3af2cf93d13bcf202889724a53"
@ -26746,7 +26844,7 @@ promise.prototype.finally@^3.1.0:
es-abstract "^1.9.0"
function-bind "^1.1.1"
prompts@^2.0.1, prompts@^2.4.0, prompts@~2.4.2:
prompts@^2.0.1, prompts@^2.4.0, prompts@^2.4.2, prompts@~2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
@ -27868,6 +27966,13 @@ rechoir@^0.7.0:
dependencies:
resolve "^1.9.0"
rechoir@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
dependencies:
resolve "^1.20.0"
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@ -28043,6 +28148,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp-tree@~0.1.1:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==
regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334"
@ -28697,6 +28807,13 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
safe-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2"
integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==
dependencies:
regexp-tree "~0.1.1"
safe-squel@^5.12.5:
version "5.12.5"
resolved "https://registry.yarnpkg.com/safe-squel/-/safe-squel-5.12.5.tgz#9597cec498dc184a15fe94082b7bcc80cb4d048b"
@ -30760,6 +30877,11 @@ tcp-port-used@^1.0.2:
debug "4.3.1"
is2 "^2.0.6"
teamcity-service-messages@^0.1.14:
version "0.1.14"
resolved "https://registry.yarnpkg.com/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz#193d420a5e4aef8e5e50b8c39e7865e08fbb5d8a"
integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==
teex@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12"
@ -31259,6 +31381,15 @@ ts-pnp@^1.1.6:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
tsconfig-paths-webpack-plugin@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763"
integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.7.0"
tsconfig-paths "^4.1.2"
tsconfig-paths@^3.14.2:
version "3.14.2"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@ -31269,7 +31400,7 @@ tsconfig-paths@^3.14.2:
minimist "^1.2.6"
strip-bom "^3.0.0"
tsconfig-paths@^4.2.0:
tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
@ -32621,6 +32752,11 @@ watchpack@^2.2.0, watchpack@^2.4.1:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
watskeburt@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-4.1.0.tgz#3c0227669be646a97424b631164b1afe3d4d5344"
integrity sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw==
wbuf@^1.1.0, wbuf@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"