├── example ├── .gitignore ├── package.json ├── index.js ├── index.html └── package-lock.json ├── .gitignore ├── git-metric-example.png ├── tsconfig.json ├── .eslintrc.json ├── .github └── workflows │ └── npm-publish.yml ├── src ├── utils.ts ├── gitUtils.ts ├── MeasurementService.ts └── index.ts ├── LICENSE.md ├── package.json ├── README.md ├── test └── e2e │ ├── utils.ts │ └── e2e.test.ts └── jest.config.js /example/.gitignore: -------------------------------------------------------------------------------- 1 | data.json 2 | TypeScriptSamples 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | testimio 3 | .vscode/ 4 | dist 5 | -------------------------------------------------------------------------------- /git-metric-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omril321/git-metric/HEAD/git-metric-example.png -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.1", 4 | "description": "A usage example for git-metric", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "Omri Lavi", 10 | "license": "MIT", 11 | "dependencies": { 12 | "git-clone": "^0.1.0", 13 | "git-metric": "latest", 14 | "serve-handler": "^6.1.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["ES2015"], 6 | "types": ["node", "jest"], 7 | "declaration": true, 8 | "outDir": "dist", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "rootDir": "src" 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "ignorePatterns": "example", 16 | "plugins": [ 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | npm-test-and-publish: 8 | name: npm-test-and-publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - run: npm install 14 | - run: npm run prepare 15 | - run: npm test 16 | - name: Publish if version has been updated 17 | uses: pascalgn/npm-publish-action@1.3.6 18 | with: 19 | tag_name: "v%s" 20 | tag_message: "v%s" 21 | commit_pattern: "^Release (\\S+)" 22 | workspace: "." 23 | publish_command: "yarn" 24 | publish_args: "--non-interactive" 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import { CommitDetails } from '.'; 3 | 4 | export function processAsPromise(process: child_process.ChildProcessWithoutNullStreams): Promise { 5 | return new Promise(function (resolve, reject) { 6 | process.on('close', resolve); 7 | process.on('exit', resolve); 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const rejectError = (msg: any) => { 10 | reject(new Error(msg.toString())); 11 | } 12 | 13 | process.on('error', rejectError); 14 | 15 | process.stderr.on('data', rejectError); 16 | }); 17 | } 18 | 19 | export function getNonModifiedFiles(commit: CommitDetails): string[] { 20 | return commit.files.filter((_, index) => commit.status[index] !== 'M'); 21 | } 22 | 23 | export function buildFilesStringFromGlobs(globs: string[]): string | undefined { 24 | return globs.join(' '); 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021] Omri Lavi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-metric", 3 | "version": "0.1.7", 4 | "description": "Git Metric is a tool for tracking file-based metrics in a git repository. Git Metric is ideal for projects with an ongoing incremental migration.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/*" 9 | ], 10 | "scripts": { 11 | "test": "jest", 12 | "start": "ts-node ./src/index.ts", 13 | "prepare": "tsc" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/omril321/git-metric.git" 18 | }, 19 | "keywords": [ 20 | "git", 21 | "metric", 22 | "repository", 23 | "migration", 24 | "incremental", 25 | "track", 26 | "files", 27 | "extension", 28 | "content" 29 | ], 30 | "author": "Omri Lavi", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/omril321/git-metric.git/issues" 34 | }, 35 | "homepage": "https://github.com/omril321/git-metric.git#readme", 36 | "dependencies": { 37 | "@types/glob": "^7.1.3", 38 | "@types/glob-to-regexp": "^0.4.0", 39 | "@types/lodash": "^4.14.165", 40 | "fs-extra": "^9.0.1", 41 | "gitlog": "4.0.3", 42 | "glob": "^7.1.6", 43 | "glob-to-regexp": "^0.4.1", 44 | "isomorphic-git": "^1.8.0", 45 | "lodash": "^4.17.20", 46 | "simple-git": "^2.31.0", 47 | "ts-jest": "^26.4.4", 48 | "typescript": "^4.1.2" 49 | }, 50 | "devDependencies": { 51 | "@types/fs-extra": "^9.0.4", 52 | "@types/jest": "^26.0.19", 53 | "@typescript-eslint/eslint-plugin": "^4.11.1", 54 | "@typescript-eslint/parser": "^4.11.1", 55 | "eslint": "^7.17.0", 56 | "eslint-plugin-import": "^2.22.1", 57 | "jest": "^26.6.3", 58 | "ts-node": "^9.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/gitUtils.ts: -------------------------------------------------------------------------------- 1 | import child_process from 'child_process'; 2 | import os from 'os'; 3 | 4 | export function countFilesContainingPhraseInCommit({commitHash, filesGlobs, phrase, repositoryPath}: { repositoryPath: string, commitHash: string, filesGlobs: string[], phrase: string, }): Promise { 5 | return new Promise(resolve => { 6 | const gitGrep = child_process.spawn('git', ['grep', '-r', '--files-with-matches', `${phrase}`, `${commitHash}`, ...filesGlobs], { cwd: repositoryPath }); 7 | const bufferArray: Buffer[] = []; 8 | 9 | gitGrep.stdout.on('data', (buff: Buffer) => { 10 | bufferArray.push(buff); 11 | }); 12 | 13 | gitGrep.on('close', () => { 14 | const output = Buffer.concat(bufferArray).toString(); 15 | //each line is a file match 16 | const value = output ? output.trim().split(os.EOL).length : 0; 17 | resolve(value); 18 | }); 19 | }); 20 | } 21 | 22 | export async function listFilesInCommitWithPatterns({commitHash, fileRegexes, repositoryPath}: { repositoryPath: string, commitHash: string, fileRegexes: RegExp[], }): Promise { 23 | return new Promise(resolve => { 24 | const filesInCommit = child_process.spawn('git', ['ls-tree', `${commitHash}`, '-r', '--name-only'], { cwd: repositoryPath }); 25 | let outputContainer = ''; 26 | filesInCommit.stdout?.on('data', (data: Buffer) => { 27 | outputContainer = outputContainer.concat(data.toString()); 28 | }) 29 | filesInCommit.on('exit', () => { 30 | const result = outputContainer.split(os.EOL) 31 | .filter(file => fileRegexes.some(regex => regex.test(file))); 32 | resolve(result); 33 | }) 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const gitMetric = require('git-metric'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const handler = require('serve-handler'); 5 | const http = require('http'); 6 | const clone = require('git-clone'); 7 | 8 | const repoCloneUrl = 'https://github.com/microsoft/TypeScriptSamples.git'; 9 | const repoName = 'TypeScriptSamples'; 10 | 11 | const config = { 12 | repositoryPath: path.resolve('.', repoName), 13 | trackByFileExtension: { 14 | 'JS File Count': ['**.js', '**.jsx'], 15 | 'TS File Count': ['**.ts', '**.tsx'], 16 | }, 17 | trackByFileContent: { 18 | 'React Related': { 19 | globs: ['**.js', '**.jsx', '**.ts', '**.tsx'], 20 | phrase: 'react' 21 | }, 22 | 'Angular Related': { 23 | globs: ['**.js', '**.jsx', '**.ts', '**.tsx'], 24 | phrase: 'angular' 25 | }, 26 | }, 27 | commitsSince: '1-15-2010', //MM-DD-YYYY 28 | commitsUntil: '11-18-2021', //MM-DD-YYYY 29 | maxCommitsCount: 450, 30 | }; 31 | 32 | 33 | cloneRepo().then(() => 34 | gitMetric.run(config).then((data) => { 35 | fs.writeFileSync('./data.json', JSON.stringify(data, null, 2)); 36 | startServer(); 37 | })); 38 | 39 | 40 | function cloneRepo() { 41 | return new Promise(res => { 42 | console.log(`cloning repo ${repoCloneUrl}...`); 43 | clone(repoCloneUrl, path.resolve('.', repoName), () => { 44 | console.log(`done cloning repo ${repoCloneUrl}`); 45 | res(); 46 | }); 47 | }); 48 | }; 49 | 50 | function startServer() { 51 | const server = http.createServer(handler); 52 | 53 | server.listen(3000, () => { 54 | console.log('View result at http://localhost:3000'); 55 | }); 56 | } -------------------------------------------------------------------------------- /src/MeasurementService.ts: -------------------------------------------------------------------------------- 1 | import { CommitDetails, ProcessedProgramOptions } from '.'; 2 | import { listFilesInCommitWithPatterns, countFilesContainingPhraseInCommit } from './gitUtils'; 3 | import globToRegex from 'glob-to-regexp'; 4 | 5 | export type CommitMetrics = {[metricName: string]: number}; 6 | 7 | export interface CommitWithMetrics { 8 | commit: CommitDetails, 9 | metrics: CommitMetrics; 10 | } 11 | 12 | export class MeasurementService { 13 | constructor(private options: ProcessedProgramOptions) { 14 | } 15 | 16 | public async calculateMetricsForCommits(commits: CommitDetails[]): Promise { 17 | return await Promise.all(commits.map((commit) => this.addMetricsToCommitDetails(commit))); 18 | } 19 | 20 | private async getContentMetrics(commit: CommitDetails) { 21 | const metrics: CommitMetrics = {}; 22 | await Promise.all(Object.keys(this.options.trackByFileContent) 23 | .map(async metricName => { 24 | const metricDetails = this.options.trackByFileContent[metricName]; 25 | const matchingFileCount = await countFilesContainingPhraseInCommit({commitHash: commit.hash, filesGlobs: metricDetails.globs, phrase: metricDetails.phrase, repositoryPath: this.options.repositoryPath}) 26 | metrics[metricName] = matchingFileCount; 27 | })); 28 | 29 | return metrics; 30 | } 31 | 32 | private async getExtensionsMetrics(commit: CommitDetails) { 33 | const metrics: CommitMetrics = {}; 34 | await Promise.all(Object.keys(this.options.trackByFileExtension) 35 | .map(async metricName => { 36 | const metricFilesGlobs = this.options.trackByFileExtension[metricName]; 37 | const globsAsRegex = metricFilesGlobs.map(glob => globToRegex(glob)); 38 | const filesInCommitWithPattern = await listFilesInCommitWithPatterns({ commitHash: commit.hash, fileRegexes: globsAsRegex, repositoryPath: this.options.repositoryPath }); 39 | metrics[metricName] = filesInCommitWithPattern.length; 40 | })); 41 | 42 | return metrics; 43 | } 44 | 45 | private async addMetricsToCommitDetails(clone: CommitDetails): Promise { 46 | const [extensionsMetrics, contentMetrics] = await Promise.all([this.getExtensionsMetrics(clone), this.getContentMetrics(clone)]) 47 | 48 | return { commit: clone, metrics: { ...extensionsMetrics, ...contentMetrics } }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import gitlog from 'gitlog'; 2 | import { CommitWithMetrics, MeasurementService } from './MeasurementService'; 3 | import { buildFilesStringFromGlobs } from './utils'; 4 | import * as path from 'path'; 5 | import _ from 'lodash'; 6 | 7 | 8 | type TrackFileContenOptions = { 9 | [metricName: string]: { 10 | globs: string[]; 11 | phrase: string; 12 | } 13 | }; 14 | 15 | export type ProgramOptions = { 16 | repositoryPath: string; 17 | maxCommitsCount?: number; 18 | commitsSince?: string; //MM-DD-YYYY 19 | commitsUntil?: string; //MM-DD-YYYY 20 | trackByFileExtension?: { 21 | [metricName: string]: string[]; //map from the metric name to the globs that count it. e.g. `{'jsFiles': ['**/*.js', '**/*.jsx'], 'tsFiles': ['**/*.ts', '**/*.tsx'], }` 22 | } 23 | trackByFileContent?: TrackFileContenOptions; 24 | } 25 | 26 | export type ProcessedProgramOptions = ProgramOptions & { 27 | repositoryName: string, 28 | allTrackedFileGlobs: string[], 29 | trackByFileExtension: { 30 | [metricName: string]: string[]; 31 | } 32 | trackByFileContent: TrackFileContenOptions; 33 | } 34 | 35 | export interface CommitDetails { 36 | hash: string; 37 | subject: string; 38 | authorName: string; 39 | authorDate: string; 40 | authorEmail: string; 41 | status: string[]; 42 | files: string[]; 43 | } 44 | 45 | function processProgramOptions(options: ProgramOptions): ProcessedProgramOptions { 46 | const repositoryName = path.basename(options.repositoryPath); 47 | const trackByFileExtension = options.trackByFileExtension || {}; 48 | const trackByFileContent = options.trackByFileContent || {}; 49 | const allTrackedFileGlobs = _.flatten([...Object.values(trackByFileExtension), ...Object.values(trackByFileContent).map(({ globs }) => globs)]); 50 | 51 | return { 52 | ...options, 53 | repositoryName, 54 | trackByFileExtension, 55 | allTrackedFileGlobs, 56 | trackByFileContent 57 | }; 58 | } 59 | 60 | function getGitCommitLogs(options: ProcessedProgramOptions): CommitDetails[] { 61 | const filesString = buildFilesStringFromGlobs(options.allTrackedFileGlobs); 62 | const result = gitlog({ 63 | repo: options.repositoryPath, 64 | since: options.commitsSince, 65 | until: options.commitsUntil, 66 | number: options.maxCommitsCount, 67 | file: filesString, 68 | fields: ["hash", "subject", "authorName", "authorDate", "authorEmail"], 69 | }); 70 | return result as unknown as (Omit & {status: string[]})[]; //this hack bypasses a typing bug in gitlog 71 | } 72 | 73 | export async function run(options: ProgramOptions): Promise { 74 | try { 75 | const processedOptions = processProgramOptions(options) 76 | 77 | const commitsDetails = getGitCommitLogs(processedOptions); 78 | const strategy = new MeasurementService(processedOptions); 79 | return await strategy.calculateMetricsForCommits(commitsDetails); 80 | } catch (e) { 81 | console.error(e); 82 | throw e; 83 | } 84 | } 85 | 86 | process.on('unhandledRejection', error => { 87 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 88 | console.error('unhandledRejection', error && (error as any).message); 89 | }); 90 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Metric 2 | Git Metric is a tool for tracking file-based metrics in a git repository. Git Metric is ideal for projects with an ongoing incremental migration, for example: 3 | * Migrating from AngularJS to React 4 | * Migrating from JavaScript to TypeScript 5 | * Any incremental removal of an old library or framework 6 | 7 | Git Metric provides commit-based metrics, which can be easily displayed on a chart to provide meaningful insights. 8 | 9 | See below the output of the example usage, which analyzes the [TypeScriptSamples](https://github.com/microsoft/TypeScriptSamples) repository. The chart shows the number of JS, TS, Angular-related and React-related files for each commit in the repository. 10 | ![Chart example for TypeScriptSamples](./git-metric-example.png "Chart example for TypeScriptSamples") 11 | 12 | Git Metric is extremely fast, and can be used with projects with various sizes and ages. 13 | Please note: if you need to collect metrics which depends on more than just the files content (like code coverage or complitation warnings), this tool can't help you. 14 | 15 | 16 | ## How it works 17 | When used, Git Metric will: 18 | * Fetch the repository's commits the git history using [gitlog](https://www.npmjs.com/package/gitlog). 19 | * Calculate the metrics for each commit using [git ls-tree](https://git-scm.com/docs/git-ls-tree) and [git grep](https://git-scm.com/docs/git-grep). 20 | 21 | Git Metric is written in TypeScript, and supplies `d.ts` files for you convenience. 22 | 23 | ## Installation 24 | ```bash 25 | npm install git-metric 26 | ``` 27 | 28 | ## Usage 29 | ```js 30 | import gitMetric from 'gitMetric'; 31 | 32 | gitMetric.run({ 33 | repositoryPath: '/full/path/to/repo', 34 | trackByFileExtension: { // Tracks a metric by the file extension 35 | 'Extension Metric 1': ['glob1', 'glob2'], // Each extension metric secifies the globs to look for 36 | // For example: 37 | 'JS File Count': ['**.js', '**.jsx'], 38 | 'TS File Count': ['**.ts', '**.tsx'], 39 | }, 40 | trackByFileContent: { // Tracks a metric by the file's content 41 | 'Content Metric 1': { // Each file content metric specifies the file globs and the phrase to look for in each file: 42 | globs: ['glob10', 'glob20',], 43 | phrase: 'some_phrase', 44 | }, 45 | // For example: 46 | 'React Related': { 47 | globs: ['**.js', '**.jsx', '**.ts', '**.tsx'], 48 | phrase: 'react' 49 | }, 50 | }, 51 | commitsSince: '1-15-2010', //MM-DD-YYYY 52 | commitsUntil: '11-18-2021', //MM-DD-YYYY 53 | maxCommitsCount: 450, 54 | }).then(commitsWithMetrics => { 55 | // Resolves with details about each commit, along with its metric. See below. 56 | }) 57 | ``` 58 | 59 | `run()` resolves with an array of commits with metrics. Each element in the result contains two properties: 60 | 1. `metrics` - a map between a configured metric, and its value for this commit. A result for the configuration from the usage example will look like the following: 61 | ```js 62 | { 63 | 'Extension Metric 1': 23 64 | 'JS File Count': 174, 65 | 'TS File Count': 320, 66 | } 67 | ``` 68 | 2. `commit` - additional details about the commit: `hash`, `authorName`, `subject`, `authorDate`, `authorEmail`, `files` and `status`. The details are provided by [gitlog](https://www.npmjs.com/package/gitlog). 69 | 70 | 71 | *Psst*.. If you only want a one-time analyze of your project, you can just adjust the example code for your need. 72 | 73 | ### Running the Example 74 | 1. Clone the repo - `git clone https://github.com/omril321/git-metric.git` 75 | 2. `cd git-metric/example` 76 | 3. `npm install` 77 | 4. `npm start` 78 | 5. Open the link on the console to view the chart 79 | 80 | 81 | ## Contributing 82 | If you wish to contribute, please contact me at any way. I'd be happy to work with you :) 83 | -------------------------------------------------------------------------------- /test/e2e/utils.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import path from 'path'; 3 | import git from 'isomorphic-git'; 4 | import fse from 'fs-extra'; 5 | import _ from 'lodash'; 6 | 7 | 8 | const TMP_REPOS_PATH = path.resolve(os.tmpdir(), 'git-metric-e2e'); 9 | 10 | type CommitActions = { 11 | create: string[]; 12 | remove: string[]; 13 | modifyContent: string[]; 14 | setContent: { file: string, content: string }[]; 15 | rename: { from: string, to: string, modifyContent?: boolean }[]; 16 | message: string; 17 | } 18 | 19 | export class GitRepoForTests { 20 | public path?: string; 21 | private commitCount = 0; 22 | 23 | public async init() { 24 | this.path = path.resolve(TMP_REPOS_PATH, `${Date.now()}`); 25 | await git.init({fs: fse, dir: this.path}); 26 | return this.path; 27 | } 28 | 29 | public async executeCommits(fileOperations: Partial[]) { 30 | for (const ops of fileOperations) { 31 | await this.executeCommit(ops); 32 | } 33 | } 34 | 35 | private async executeCommit({ create = [], remove = [], rename = [], setContent = [], modifyContent = [], message = `Commit #${this.commitCount++} ${Date.now()}` }: Partial) { 36 | if (!this.path) { 37 | throw new Error('Git repo is not initiated') 38 | } 39 | 40 | await Promise.all(create.map((relativePath) => this.createNewFile(relativePath))); 41 | await Promise.all(remove.map((relativePath) => this.deleteExistingFile(relativePath))); 42 | await Promise.all(rename.filter(({ modifyContent }) => modifyContent).map(({ from }) => this.modifyExistingFile(from))); 43 | await Promise.all(rename.map(({ from, to }) => this.renameExistingFile(from, to))); 44 | await Promise.all(modifyContent.map((relativePath) => this.modifyExistingFile(relativePath))); 45 | await Promise.all(setContent.map(({content, file}) => this.setExistingFileContent(file, content))); 46 | 47 | const filesToAdd = _.flatten([create, modifyContent, rename.map(({ to }) => to), setContent.map(({ file }) => file)]) 48 | await Promise.all(filesToAdd.map((file) => git.add({ fs: fse, dir: this.path!, filepath: file }))); 49 | 50 | const filesToRemove = _.flatten([remove, rename.map(({ from }) => from)]); 51 | await Promise.all(filesToRemove.map(file => git.remove({fs: fse, dir: this.path!, filepath: file}))); 52 | 53 | await git.commit({ fs: fse, dir: this.path, message, author: { name: 'Fake test author' } }); 54 | } 55 | 56 | private mapToRelativePath(relativePath: string) { 57 | return path.resolve(this.path!, relativePath); 58 | } 59 | 60 | 61 | private async createNewFile(relativePath: string) { 62 | const path = this.mapToRelativePath(relativePath); 63 | if (fse.existsSync(path)) { 64 | throw new Error(`Attempted creating a new file at path that already exists: ${path}`); 65 | } 66 | await fse.createFile(path); 67 | await fse.appendFile(path, `created at: ${Date.now()} `); 68 | } 69 | 70 | private async deleteExistingFile(relativePath: string) { 71 | const path = this.mapToRelativePath(relativePath); 72 | if (!fse.existsSync(path)) { 73 | throw new Error(`Attempted deleting a file that doesn't exist: ${path}`); 74 | } 75 | await fse.remove(path); 76 | } 77 | 78 | private async renameExistingFile(relativePath: string, newRelativePath: string) { 79 | const path = this.mapToRelativePath(relativePath); 80 | const newPath = this.mapToRelativePath(newRelativePath); 81 | if (!fse.existsSync(path)) { 82 | throw new Error(`Attempted renaming a file that doesn't exist: ${path}`); 83 | } 84 | await fse.rename(path, newPath); 85 | } 86 | 87 | private async modifyExistingFile(relativePath: string) { 88 | const path = this.mapToRelativePath(relativePath); 89 | if (!fse.existsSync(path)) { 90 | throw new Error(`Attempted modifying a file that doesn't exist: ${path}`); 91 | } 92 | await fse.appendFile(path, `${(Date.now())} `); 93 | } 94 | 95 | private async setExistingFileContent(relativePath: string, content: string) { 96 | const path = this.mapToRelativePath(relativePath); 97 | if (!fse.existsSync(path)) { 98 | throw new Error(`Attempted setting content to a file that doesn't exist: ${path}`); 99 | } 100 | await fse.writeFile(path, content); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/82/_bnrh4d14ys5j0yvr19qjww40000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | preset: "ts-jest/presets/js-with-ts", 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | roots: [ 121 | "/test" 122 | ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: "node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /test/e2e/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { run } from '../../src'; 3 | import { GitRepoForTests } from './utils'; 4 | 5 | describe('e2e', () => { 6 | describe('file extension metric only', () => { 7 | it(`should count file extension metrics properly for a repo with a single commit`, async () => { 8 | const repo = new GitRepoForTests() 9 | await repo.init(); 10 | 11 | await repo.executeCommits( 12 | [ 13 | { create: ['file1.ts'] } 14 | ] 15 | ) 16 | const result = await run({ 17 | trackByFileExtension: { tsFilesForThisTest: ['**.ts'], txtFiles: ['**.txt'] }, 18 | repositoryPath: repo.path!, 19 | maxCommitsCount: 10, 20 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 21 | }) 22 | 23 | expect(result).toHaveLength(1); 24 | expect(result[0].metrics).toEqual({ tsFilesForThisTest: 1, txtFiles: 0 }); 25 | }); 26 | 27 | it(`should count file extension metrics properly for a repo with multiple commits of renaming and modifying files`, async () => { 28 | const repo = new GitRepoForTests() 29 | await repo.init(); 30 | 31 | await repo.executeCommits( 32 | [ 33 | { 34 | create: ['file1.tsx', 'file2.ts', 'file3.ts', 'file4.txt', 'file5.txt', 'file6.ts'] 35 | }, 36 | { 37 | modifyContent: ['file1.tsx'], 38 | rename: [ 39 | { from: 'file2.ts', to: 'file2.something' }, 40 | { from: 'file3.ts', to: 'file3_renamed.ts' }, 41 | ], 42 | }, 43 | { 44 | modifyContent: ['file4.txt'], 45 | rename: [ 46 | { from: 'file2.something', to: 'file2.ts', modifyContent: true }, 47 | { from: 'file3_renamed.ts', to: 'file3_renamed_again.ts' }, 48 | { from: 'file6.ts', to: 'file6.txt' }, 49 | ], 50 | } 51 | ] 52 | ) 53 | const result = await run({ 54 | trackByFileExtension: { 55 | tsFilesForThisTest: ['**.ts', '**.tsx'], 56 | txtFiles: ['**.txt'], 57 | noSuchExtension: ['unknown.bla'] 58 | }, 59 | repositoryPath: repo.path!, 60 | maxCommitsCount: 10, 61 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 62 | }); 63 | const resultMetrics = _.map(result, 'metrics'); 64 | 65 | expect(resultMetrics).toEqual([ //first here is latest commit 66 | { tsFilesForThisTest: 3, txtFiles: 3, noSuchExtension: 0 }, 67 | { tsFilesForThisTest: 3, txtFiles: 2, noSuchExtension: 0 }, 68 | { tsFilesForThisTest: 4, txtFiles: 2, noSuchExtension: 0 }, 69 | ]); 70 | }); 71 | 72 | it(`should count file extension metrics properly for a repo with multiple commits of creating, deleting and renaming files`, async () => { 73 | const repo = new GitRepoForTests() 74 | await repo.init(); 75 | 76 | await repo.executeCommits( 77 | [ 78 | { 79 | create: ['file1.ts', 'file2.ts', 'file3.tsx', 'file4.txt', 'file5.txt', 'file6.ts'] 80 | }, 81 | { 82 | remove: ['file1.ts'], 83 | rename: [ 84 | { from: 'file2.ts', to: 'file2.something', modifyContent: true }, 85 | { from: 'file3.tsx', to: 'file3_renamed.tsx' } 86 | ], 87 | }, 88 | { 89 | remove: ['file2.something', 'file3_renamed.tsx', 'file4.txt'], 90 | } 91 | ] 92 | ); 93 | 94 | const result = await run({ 95 | trackByFileExtension: { 96 | tsFilesForThisTest: ['**.ts', '**.tsx'], 97 | txtFiles: ['**.txt'], 98 | noSuchExtension: ['unknown.bla'], 99 | }, 100 | repositoryPath: repo.path!, 101 | maxCommitsCount: 10, 102 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 103 | }); 104 | const resultMetrics = _.map(result, 'metrics'); 105 | 106 | expect(resultMetrics).toEqual([ //first here is latest commit 107 | { tsFilesForThisTest: 1, txtFiles: 1, noSuchExtension: 0 }, 108 | { tsFilesForThisTest: 2, txtFiles: 2, noSuchExtension: 0 }, 109 | { tsFilesForThisTest: 4, txtFiles: 2, noSuchExtension: 0 }, 110 | ]); 111 | }); 112 | 113 | it('should count file extension metrics properly for a repository with folders hierarchy with a single commit', async () => { 114 | const repo = new GitRepoForTests() 115 | await repo.init(); 116 | 117 | await repo.executeCommits([{ create: ['f1/file1.ts', 'f2/file2.ts', 'f2/file3.ts', 't/t.ts', 't/txt.txt'] }]); 118 | 119 | const result = await run({ 120 | trackByFileExtension: { 121 | tsFilesForThisTest: ['**.ts', '**.tsx'], 122 | txtFiles: ['**.txt'], 123 | noSuchExtension: ['unknown.bla'], 124 | }, 125 | repositoryPath: repo.path!, 126 | maxCommitsCount: 10, 127 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 128 | }); 129 | const resultMetrics = _.map(result, 'metrics'); 130 | 131 | expect(resultMetrics).toEqual([ //first here is latest commit 132 | { tsFilesForThisTest: 4, txtFiles: 1, noSuchExtension: 0 }, 133 | ]); 134 | }) 135 | }); 136 | 137 | describe('file content metric only', () => { 138 | it('should count file content metrics for a repository with folders hierarchy when some files matching and some not', async () => { 139 | const repo = new GitRepoForTests() 140 | await repo.init(); 141 | 142 | await repo.executeCommits([ 143 | { create: ['f1/file1.ts', 'f2/file2.ts', 'f2/file3.tsx', 't/t.ts', 't/inner/txt1.txt', 't/txt2.txt'] }, 144 | { 145 | setContent: [ 146 | { file: 'f1/file1.ts', content: 'aaa' }, // match tsWithAaa 147 | { file: 'f2/file2.ts', content: 'zzz' }, 148 | { file: 'f2/file3.tsx', content: 'aaa' }, 149 | { file: 't/t.ts', content: 'aaa' }, // match tsWithAaa 150 | { file: 't/inner/txt1.txt', content: 'zzz' }, // match txtWithZzz 151 | { file: 't/txt2.txt', content: 'nope' } 152 | ] 153 | } 154 | ]); 155 | 156 | const result = await run({ 157 | trackByFileContent: { 158 | tsWithAaa: { 159 | globs: ['**.ts'], 160 | phrase: 'aaa', 161 | }, 162 | txtWithZzz: { 163 | globs: ['**.txt'], 164 | phrase: 'zzz', 165 | }, 166 | noSuchFiles: {globs: ['nothingHere'], phrase: ''}, 167 | }, 168 | repositoryPath: repo.path!, 169 | maxCommitsCount: 10, 170 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 171 | }); 172 | const resultMetrics = _.map(result, 'metrics'); 173 | 174 | expect(resultMetrics).toEqual([ //first here is latest commit 175 | { tsWithAaa: 2, txtWithZzz: 1, noSuchFiles: 0 }, 176 | { tsWithAaa: 0, txtWithZzz: 0, noSuchFiles: 0 }, 177 | ]); 178 | }); 179 | 180 | 181 | it('should count file content metrics for a repository with multiple commits with folders hierarchy when some files matching and some not', async () => { 182 | const repo = new GitRepoForTests() 183 | await repo.init(); 184 | 185 | await repo.executeCommits([ 186 | { create: ['file1.ts', 'file2.txt'] }, 187 | { 188 | setContent: [ 189 | { file: 'file1.ts', content: 'aaa' }, 190 | { file: 'file2.txt', content: 'aaa' }, 191 | ] 192 | }, 193 | { rename: [{ from: 'file1.ts', to: 'file1.txt' }] }, 194 | { create: ['file3.ts'], }, 195 | { setContent: [{ file: 'file3.ts', content: 'aaa' }] }, 196 | { remove: ['file3.ts'] } 197 | ]); 198 | 199 | const result = await run({ 200 | trackByFileContent: { 201 | tsWithAaa: { 202 | globs: ['**.ts'], 203 | phrase: 'aaa', 204 | }, 205 | }, 206 | repositoryPath: repo.path!, 207 | maxCommitsCount: 10, 208 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 209 | }); 210 | const resultMetrics = _.map(result, 'metrics'); 211 | 212 | expect(resultMetrics).toEqual([ //first here is latest commit 213 | { tsWithAaa: 0, }, 214 | { tsWithAaa: 1, }, 215 | { tsWithAaa: 0, }, 216 | { tsWithAaa: 0, }, 217 | { tsWithAaa: 1, }, 218 | { tsWithAaa: 0, }, 219 | ]); 220 | }); 221 | }) 222 | 223 | describe('mix of metric types', () => { 224 | it('should calculate all metric type for a repository with multiple commits', async () => { 225 | const repo = new GitRepoForTests() 226 | await repo.init(); 227 | 228 | await repo.executeCommits([ 229 | { create: ['f1.ts', 'f2.ts', 'f3.ts', 'f4.txt', 'f5.txt', 'f6.txt', 'other_file'] }, 230 | { 231 | setContent: [ 232 | { file: 'f1.ts', content: 'aaa' }, 233 | { file: 'f2.ts', content: 'zzz' }, 234 | { file: 'f3.ts', content: 'aaa' }, 235 | { file: 'f4.txt', content: 'aaa' }, 236 | { file: 'f5.txt', content: 'zzz' }, 237 | { file: 'f6.txt', content: 'nope' }, 238 | { file: 'other_file', content: 'aaa' } 239 | ] 240 | }, 241 | { 242 | rename: [ 243 | {from: 'f1.ts', to: 'f1.renamed', modifyContent: false}, 244 | {from: 'f2.ts', to: 'f2.renamed', modifyContent: true}, 245 | {from: 'f4.txt', to: 'f4.renamed.txt', modifyContent: false}, 246 | ], 247 | setContent: [{ file: 'f5.txt', content: 'nope' }] 248 | }, 249 | { 250 | create: ['file7.txt'] 251 | } 252 | ]); 253 | 254 | const result = await run({ 255 | trackByFileExtension: { 256 | tsFiles: ['**.ts', '**.tsx'], 257 | txtFiles: ['**.txt'], 258 | noSuchExtension: ['unknown.bla'], 259 | }, 260 | trackByFileContent: { 261 | tsWithAaa: { 262 | globs: ['**.ts'], 263 | phrase: 'aaa', 264 | }, 265 | txtWithZzz: { 266 | globs: ['**.txt'], 267 | phrase: 'zzz', 268 | }, 269 | noSuchFiles: {globs: ['nothingHere'], phrase: ''}, 270 | }, 271 | repositoryPath: repo.path!, 272 | maxCommitsCount: 10, 273 | commitsSince: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), //yesterday 274 | }); 275 | const resultMetrics = _.map(result, 'metrics'); 276 | 277 | expect(resultMetrics).toEqual([ //first here is latest commit 278 | { tsFiles: 1, txtFiles: 4, tsWithAaa: 1, txtWithZzz: 0, noSuchFiles: 0, noSuchExtension: 0, }, 279 | { tsFiles: 1, txtFiles: 3, tsWithAaa: 1, txtWithZzz: 0, noSuchFiles: 0, noSuchExtension: 0, }, 280 | { tsFiles: 3, txtFiles: 3, tsWithAaa: 2, txtWithZzz: 1, noSuchFiles: 0, noSuchExtension: 0, }, 281 | { tsFiles: 3, txtFiles: 3, tsWithAaa: 0, txtWithZzz: 0, noSuchFiles: 0, noSuchExtension: 0, }, 282 | ]); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@jest/types": { 8 | "version": "26.6.2", 9 | "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", 10 | "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", 11 | "requires": { 12 | "@types/istanbul-lib-coverage": "^2.0.0", 13 | "@types/istanbul-reports": "^3.0.0", 14 | "@types/node": "*", 15 | "@types/yargs": "^15.0.0", 16 | "chalk": "^4.0.0" 17 | } 18 | }, 19 | "@kwsites/file-exists": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", 22 | "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", 23 | "requires": { 24 | "debug": "^4.1.1" 25 | } 26 | }, 27 | "@kwsites/promise-deferred": { 28 | "version": "1.1.1", 29 | "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", 30 | "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" 31 | }, 32 | "@types/glob": { 33 | "version": "7.1.3", 34 | "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", 35 | "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", 36 | "requires": { 37 | "@types/minimatch": "*", 38 | "@types/node": "*" 39 | } 40 | }, 41 | "@types/glob-to-regexp": { 42 | "version": "0.4.0", 43 | "resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz", 44 | "integrity": "sha512-unszpTzAknG612PxqtoNUkLM0T3rIAXT8oE9Dyhhbl4eew91jLqcgJZOu5j7GztHg09R8LWCMocRU1ohDFY7Pw==" 45 | }, 46 | "@types/istanbul-lib-coverage": { 47 | "version": "2.0.3", 48 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", 49 | "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" 50 | }, 51 | "@types/istanbul-lib-report": { 52 | "version": "3.0.0", 53 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", 54 | "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", 55 | "requires": { 56 | "@types/istanbul-lib-coverage": "*" 57 | } 58 | }, 59 | "@types/istanbul-reports": { 60 | "version": "3.0.1", 61 | "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", 62 | "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", 63 | "requires": { 64 | "@types/istanbul-lib-report": "*" 65 | } 66 | }, 67 | "@types/lodash": { 68 | "version": "4.14.170", 69 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", 70 | "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==" 71 | }, 72 | "@types/minimatch": { 73 | "version": "3.0.4", 74 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", 75 | "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" 76 | }, 77 | "@types/node": { 78 | "version": "15.12.4", 79 | "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", 80 | "integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" 81 | }, 82 | "@types/yargs": { 83 | "version": "15.0.13", 84 | "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", 85 | "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", 86 | "requires": { 87 | "@types/yargs-parser": "*" 88 | } 89 | }, 90 | "@types/yargs-parser": { 91 | "version": "20.2.0", 92 | "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", 93 | "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" 94 | }, 95 | "ansi-styles": { 96 | "version": "4.3.0", 97 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 98 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 99 | "requires": { 100 | "color-convert": "^2.0.1" 101 | } 102 | }, 103 | "async-lock": { 104 | "version": "1.3.0", 105 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.0.tgz", 106 | "integrity": "sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg==" 107 | }, 108 | "at-least-node": { 109 | "version": "1.0.0", 110 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 111 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" 112 | }, 113 | "balanced-match": { 114 | "version": "1.0.0", 115 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 116 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 117 | }, 118 | "brace-expansion": { 119 | "version": "1.1.11", 120 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 121 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 122 | "requires": { 123 | "balanced-match": "^1.0.0", 124 | "concat-map": "0.0.1" 125 | } 126 | }, 127 | "braces": { 128 | "version": "3.0.2", 129 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 130 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 131 | "requires": { 132 | "fill-range": "^7.0.1" 133 | } 134 | }, 135 | "bs-logger": { 136 | "version": "0.2.6", 137 | "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", 138 | "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", 139 | "requires": { 140 | "fast-json-stable-stringify": "2.x" 141 | } 142 | }, 143 | "buffer-from": { 144 | "version": "1.1.1", 145 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 146 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 147 | }, 148 | "bytes": { 149 | "version": "3.0.0", 150 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 151 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 152 | }, 153 | "chalk": { 154 | "version": "4.1.1", 155 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", 156 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", 157 | "requires": { 158 | "ansi-styles": "^4.1.0", 159 | "supports-color": "^7.1.0" 160 | } 161 | }, 162 | "ci-info": { 163 | "version": "2.0.0", 164 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", 165 | "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" 166 | }, 167 | "clean-git-ref": { 168 | "version": "2.0.1", 169 | "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", 170 | "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" 171 | }, 172 | "color-convert": { 173 | "version": "2.0.1", 174 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 175 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 176 | "requires": { 177 | "color-name": "~1.1.4" 178 | } 179 | }, 180 | "color-name": { 181 | "version": "1.1.4", 182 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 183 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 184 | }, 185 | "concat-map": { 186 | "version": "0.0.1", 187 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 188 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 189 | }, 190 | "content-disposition": { 191 | "version": "0.5.2", 192 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 193 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 194 | }, 195 | "crc-32": { 196 | "version": "1.2.0", 197 | "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", 198 | "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", 199 | "requires": { 200 | "exit-on-epipe": "~1.0.1", 201 | "printj": "~1.1.0" 202 | } 203 | }, 204 | "debug": { 205 | "version": "4.3.1", 206 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 207 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 208 | "requires": { 209 | "ms": "2.1.2" 210 | } 211 | }, 212 | "decompress-response": { 213 | "version": "4.2.1", 214 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 215 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 216 | "requires": { 217 | "mimic-response": "^2.0.0" 218 | } 219 | }, 220 | "diff3": { 221 | "version": "0.0.3", 222 | "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", 223 | "integrity": "sha1-1OXDpM305f4SEatC5pP8tDIVgPw=" 224 | }, 225 | "exit-on-epipe": { 226 | "version": "1.0.1", 227 | "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", 228 | "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" 229 | }, 230 | "fast-json-stable-stringify": { 231 | "version": "2.1.0", 232 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 233 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 234 | }, 235 | "fast-url-parser": { 236 | "version": "1.1.3", 237 | "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", 238 | "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", 239 | "requires": { 240 | "punycode": "^1.3.2" 241 | } 242 | }, 243 | "fill-range": { 244 | "version": "7.0.1", 245 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 246 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 247 | "requires": { 248 | "to-regex-range": "^5.0.1" 249 | } 250 | }, 251 | "fs-extra": { 252 | "version": "9.1.0", 253 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", 254 | "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", 255 | "requires": { 256 | "at-least-node": "^1.0.0", 257 | "graceful-fs": "^4.2.0", 258 | "jsonfile": "^6.0.1", 259 | "universalify": "^2.0.0" 260 | } 261 | }, 262 | "fs.realpath": { 263 | "version": "1.0.0", 264 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 265 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 266 | }, 267 | "git-clone": { 268 | "version": "0.1.0", 269 | "resolved": "https://registry.npmjs.org/git-clone/-/git-clone-0.1.0.tgz", 270 | "integrity": "sha1-DXYWN3gJOu9/HDAjjyqe8/B6Lrk=" 271 | }, 272 | "git-metric": { 273 | "version": "0.1.7", 274 | "resolved": "https://registry.npmjs.org/git-metric/-/git-metric-0.1.7.tgz", 275 | "integrity": "sha512-6Jh82302In7rt1KaxlB/I6ZHSARiecnO/BGbsjYvXnpEeNwxRYYnG2X/EWfUbEyd/C9iwckRctBmmUj0osQz+g==", 276 | "requires": { 277 | "@types/glob": "^7.1.3", 278 | "@types/glob-to-regexp": "^0.4.0", 279 | "@types/lodash": "^4.14.165", 280 | "fs-extra": "^9.0.1", 281 | "gitlog": "^4.0.3", 282 | "glob": "^7.1.6", 283 | "glob-to-regexp": "^0.4.1", 284 | "isomorphic-git": "^1.8.0", 285 | "lodash": "^4.17.20", 286 | "simple-git": "^2.31.0", 287 | "ts-jest": "^26.4.4", 288 | "typescript": "^4.1.2" 289 | } 290 | }, 291 | "gitlog": { 292 | "version": "4.0.4", 293 | "resolved": "https://registry.npmjs.org/gitlog/-/gitlog-4.0.4.tgz", 294 | "integrity": "sha512-jeY2kO7CVyTa6cUM7ZD2ZxIyBkna1xvW2esV/3o8tbhiUneX1UBQCH4D9aMrHgGiohBjyXbuZogyjKXslnY5Yg==", 295 | "requires": { 296 | "debug": "^4.1.1", 297 | "tslib": "^1.14.1" 298 | } 299 | }, 300 | "glob": { 301 | "version": "7.1.7", 302 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 303 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 304 | "requires": { 305 | "fs.realpath": "^1.0.0", 306 | "inflight": "^1.0.4", 307 | "inherits": "2", 308 | "minimatch": "^3.0.4", 309 | "once": "^1.3.0", 310 | "path-is-absolute": "^1.0.0" 311 | } 312 | }, 313 | "glob-to-regexp": { 314 | "version": "0.4.1", 315 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 316 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 317 | }, 318 | "graceful-fs": { 319 | "version": "4.2.6", 320 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 321 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" 322 | }, 323 | "has-flag": { 324 | "version": "4.0.0", 325 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 326 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 327 | }, 328 | "ignore": { 329 | "version": "5.1.8", 330 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", 331 | "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" 332 | }, 333 | "inflight": { 334 | "version": "1.0.6", 335 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 336 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 337 | "requires": { 338 | "once": "^1.3.0", 339 | "wrappy": "1" 340 | } 341 | }, 342 | "inherits": { 343 | "version": "2.0.4", 344 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 345 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 346 | }, 347 | "is-ci": { 348 | "version": "2.0.0", 349 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", 350 | "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", 351 | "requires": { 352 | "ci-info": "^2.0.0" 353 | } 354 | }, 355 | "is-number": { 356 | "version": "7.0.0", 357 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 358 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 359 | }, 360 | "isomorphic-git": { 361 | "version": "1.8.3", 362 | "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.8.3.tgz", 363 | "integrity": "sha512-VFiX6Xf1o3w3HqVW/GOw/mCADQdhWeW5SqjkXpclG4Fyh1yr2/G9dkwINezQ5hsvyKpImCX7xJCGmpH+56WLzw==", 364 | "requires": { 365 | "async-lock": "^1.1.0", 366 | "clean-git-ref": "^2.0.1", 367 | "crc-32": "^1.2.0", 368 | "diff3": "0.0.3", 369 | "ignore": "^5.1.4", 370 | "minimisted": "^2.0.0", 371 | "pako": "^1.0.10", 372 | "pify": "^4.0.1", 373 | "readable-stream": "^3.4.0", 374 | "sha.js": "^2.4.9", 375 | "simple-get": "^3.0.2" 376 | } 377 | }, 378 | "jest-util": { 379 | "version": "26.6.2", 380 | "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", 381 | "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", 382 | "requires": { 383 | "@jest/types": "^26.6.2", 384 | "@types/node": "*", 385 | "chalk": "^4.0.0", 386 | "graceful-fs": "^4.2.4", 387 | "is-ci": "^2.0.0", 388 | "micromatch": "^4.0.2" 389 | } 390 | }, 391 | "json5": { 392 | "version": "2.2.0", 393 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", 394 | "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", 395 | "requires": { 396 | "minimist": "^1.2.5" 397 | } 398 | }, 399 | "jsonfile": { 400 | "version": "6.1.0", 401 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 402 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 403 | "requires": { 404 | "graceful-fs": "^4.1.6", 405 | "universalify": "^2.0.0" 406 | } 407 | }, 408 | "lodash": { 409 | "version": "4.17.21", 410 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 411 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 412 | }, 413 | "lru-cache": { 414 | "version": "6.0.0", 415 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 416 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 417 | "requires": { 418 | "yallist": "^4.0.0" 419 | } 420 | }, 421 | "make-error": { 422 | "version": "1.3.6", 423 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 424 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" 425 | }, 426 | "micromatch": { 427 | "version": "4.0.4", 428 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 429 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 430 | "requires": { 431 | "braces": "^3.0.1", 432 | "picomatch": "^2.2.3" 433 | } 434 | }, 435 | "mime-db": { 436 | "version": "1.33.0", 437 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 438 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 439 | }, 440 | "mime-types": { 441 | "version": "2.1.18", 442 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 443 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 444 | "requires": { 445 | "mime-db": "~1.33.0" 446 | } 447 | }, 448 | "mimic-response": { 449 | "version": "2.1.0", 450 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 451 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 452 | }, 453 | "minimatch": { 454 | "version": "3.0.4", 455 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 456 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 457 | "requires": { 458 | "brace-expansion": "^1.1.7" 459 | } 460 | }, 461 | "minimist": { 462 | "version": "1.2.5", 463 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 464 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 465 | }, 466 | "minimisted": { 467 | "version": "2.0.1", 468 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", 469 | "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", 470 | "requires": { 471 | "minimist": "^1.2.5" 472 | } 473 | }, 474 | "mkdirp": { 475 | "version": "1.0.4", 476 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 477 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 478 | }, 479 | "ms": { 480 | "version": "2.1.2", 481 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 482 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 483 | }, 484 | "once": { 485 | "version": "1.4.0", 486 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 487 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 488 | "requires": { 489 | "wrappy": "1" 490 | } 491 | }, 492 | "pako": { 493 | "version": "1.0.11", 494 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 495 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 496 | }, 497 | "path-is-absolute": { 498 | "version": "1.0.1", 499 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 500 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 501 | }, 502 | "path-is-inside": { 503 | "version": "1.0.2", 504 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 505 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" 506 | }, 507 | "path-to-regexp": { 508 | "version": "2.2.1", 509 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", 510 | "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" 511 | }, 512 | "picomatch": { 513 | "version": "2.3.0", 514 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 515 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" 516 | }, 517 | "pify": { 518 | "version": "4.0.1", 519 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 520 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" 521 | }, 522 | "printj": { 523 | "version": "1.1.2", 524 | "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", 525 | "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" 526 | }, 527 | "punycode": { 528 | "version": "1.4.1", 529 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 530 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 531 | }, 532 | "range-parser": { 533 | "version": "1.2.0", 534 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 535 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 536 | }, 537 | "readable-stream": { 538 | "version": "3.6.0", 539 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 540 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 541 | "requires": { 542 | "inherits": "^2.0.3", 543 | "string_decoder": "^1.1.1", 544 | "util-deprecate": "^1.0.1" 545 | } 546 | }, 547 | "safe-buffer": { 548 | "version": "5.2.1", 549 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 550 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 551 | }, 552 | "semver": { 553 | "version": "7.3.5", 554 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 555 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 556 | "requires": { 557 | "lru-cache": "^6.0.0" 558 | } 559 | }, 560 | "serve-handler": { 561 | "version": "6.1.3", 562 | "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz", 563 | "integrity": "sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==", 564 | "requires": { 565 | "bytes": "3.0.0", 566 | "content-disposition": "0.5.2", 567 | "fast-url-parser": "1.1.3", 568 | "mime-types": "2.1.18", 569 | "minimatch": "3.0.4", 570 | "path-is-inside": "1.0.2", 571 | "path-to-regexp": "2.2.1", 572 | "range-parser": "1.2.0" 573 | } 574 | }, 575 | "sha.js": { 576 | "version": "2.4.11", 577 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 578 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 579 | "requires": { 580 | "inherits": "^2.0.1", 581 | "safe-buffer": "^5.0.1" 582 | } 583 | }, 584 | "simple-concat": { 585 | "version": "1.0.1", 586 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 587 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 588 | }, 589 | "simple-get": { 590 | "version": "3.1.0", 591 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 592 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 593 | "requires": { 594 | "decompress-response": "^4.2.0", 595 | "once": "^1.3.1", 596 | "simple-concat": "^1.0.0" 597 | } 598 | }, 599 | "simple-git": { 600 | "version": "2.40.0", 601 | "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.40.0.tgz", 602 | "integrity": "sha512-7IO/eQwrN5kvS38TTu9ljhG9tx2nn0BTqZOmqpPpp51TvE44YIvLA6fETqEVA8w/SeEfPaVv6mk7Tsk9Jns+ag==", 603 | "requires": { 604 | "@kwsites/file-exists": "^1.1.1", 605 | "@kwsites/promise-deferred": "^1.1.1", 606 | "debug": "^4.3.1" 607 | } 608 | }, 609 | "string_decoder": { 610 | "version": "1.3.0", 611 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 612 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 613 | "requires": { 614 | "safe-buffer": "~5.2.0" 615 | } 616 | }, 617 | "supports-color": { 618 | "version": "7.2.0", 619 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 620 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 621 | "requires": { 622 | "has-flag": "^4.0.0" 623 | } 624 | }, 625 | "to-regex-range": { 626 | "version": "5.0.1", 627 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 628 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 629 | "requires": { 630 | "is-number": "^7.0.0" 631 | } 632 | }, 633 | "ts-jest": { 634 | "version": "26.5.6", 635 | "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", 636 | "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", 637 | "requires": { 638 | "bs-logger": "0.x", 639 | "buffer-from": "1.x", 640 | "fast-json-stable-stringify": "2.x", 641 | "jest-util": "^26.1.0", 642 | "json5": "2.x", 643 | "lodash": "4.x", 644 | "make-error": "1.x", 645 | "mkdirp": "1.x", 646 | "semver": "7.x", 647 | "yargs-parser": "20.x" 648 | } 649 | }, 650 | "tslib": { 651 | "version": "1.14.1", 652 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 653 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" 654 | }, 655 | "typescript": { 656 | "version": "4.3.4", 657 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", 658 | "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==" 659 | }, 660 | "universalify": { 661 | "version": "2.0.0", 662 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 663 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 664 | }, 665 | "util-deprecate": { 666 | "version": "1.0.2", 667 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 668 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 669 | }, 670 | "wrappy": { 671 | "version": "1.0.2", 672 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 673 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 674 | }, 675 | "yallist": { 676 | "version": "4.0.0", 677 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 678 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 679 | }, 680 | "yargs-parser": { 681 | "version": "20.2.9", 682 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 683 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" 684 | } 685 | } 686 | } 687 | --------------------------------------------------------------------------------