mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
5d6d45c341
commit
34bf83b54f
31 changed files with 1779 additions and 26 deletions
|
@ -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
1
.github/CODEOWNERS
vendored
|
@ -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
2
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
153
packages/kbn-dependency-usage/README.md
Normal file
153
packages/kbn-dependency-usage/README.md
Normal 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.
|
||||
|
15
packages/kbn-dependency-usage/jest.config.js
Normal file
15
packages/kbn-dependency-usage/jest.config.js
Normal 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'],
|
||||
};
|
6
packages/kbn-dependency-usage/kibana.jsonc
Normal file
6
packages/kbn-dependency-usage/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"devOnly": true,
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/dependency-usage",
|
||||
"owner": "@elastic/kibana-security"
|
||||
}
|
11
packages/kbn-dependency-usage/package.json
Normal file
11
packages/kbn-dependency-usage/package.json
Normal 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/*"
|
||||
}
|
||||
}
|
97
packages/kbn-dependency-usage/src/cli.test.ts
Normal file
97
packages/kbn-dependency-usage/src/cli.test.ts
Normal 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();
|
||||
});
|
||||
});
|
169
packages/kbn-dependency-usage/src/cli.ts
Normal file
169
packages/kbn-dependency-usage/src/cli.ts
Normal 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();
|
||||
}
|
|
@ -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',
|
||||
];
|
10
packages/kbn-dependency-usage/src/dependency_graph/index.ts
Normal file
10
packages/kbn-dependency-usage/src/dependency_graph/index.ts
Normal 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';
|
|
@ -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'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
99
packages/kbn-dependency-usage/src/lib/code_owners.test.ts
Normal file
99
packages/kbn-dependency-usage/src/lib/code_owners.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
84
packages/kbn-dependency-usage/src/lib/code_owners.ts
Normal file
84
packages/kbn-dependency-usage/src/lib/code_owners.ts
Normal 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 ?? [];
|
||||
}
|
|
@ -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)/([^/]+)/([^/]+)');
|
||||
});
|
||||
});
|
18
packages/kbn-dependency-usage/src/lib/collapse_with_depth.ts
Normal file
18
packages/kbn-dependency-usage/src/lib/collapse_with_depth.ts
Normal 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;
|
||||
}
|
116
packages/kbn-dependency-usage/src/lib/group_by_owners.test.ts
Normal file
116
packages/kbn-dependency-usage/src/lib/group_by_owners.test.ts
Normal 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'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
89
packages/kbn-dependency-usage/src/lib/group_by_owners.ts
Normal file
89
packages/kbn-dependency-usage/src/lib/group_by_owners.ts
Normal 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;
|
||||
}
|
|
@ -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({});
|
||||
});
|
||||
});
|
30
packages/kbn-dependency-usage/src/lib/group_by_source.ts
Normal file
30
packages/kbn-dependency-usage/src/lib/group_by_source.ts
Normal 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;
|
||||
}
|
11
packages/kbn-dependency-usage/src/lib/index.ts
Normal file
11
packages/kbn-dependency-usage/src/lib/index.ts
Normal 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';
|
20
packages/kbn-dependency-usage/tsconfig.json
Normal file
20
packages/kbn-dependency-usage/tsconfig.json
Normal 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",
|
||||
],
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
7
scripts/dependency_usage.sh
Executable 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 doesn’t 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 "$@"
|
|
@ -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,
|
||||
|
|
|
@ -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
184
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue