diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a15a117..fb576ac 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,2 +1,13 @@ #!/usr/bin/env node -export * from './lib'; + +(async function run() { + console.warn(`This package (@herodevs/hd-tracker) has been deprecated`); + const messageLines = [ + '', + 'It is now available as part of the HeroDevs CLI (@herodevs/cli) via:', + 'npx @herodevs/cli tracker init', + 'and', + 'npx @herodevs/cli tracker run', + ]; + console.log(messageLines.join('\n')); +})(); diff --git a/packages/cli/src/lib/argv.ts b/packages/cli/src/lib/argv.ts deleted file mode 100644 index 56cd33e..0000000 --- a/packages/cli/src/lib/argv.ts +++ /dev/null @@ -1,21 +0,0 @@ -import yargs = require('yargs'); -import { ChartConfig } from './models/chart-config'; -import { hideBin } from 'yargs/helpers'; - -interface RawArgs { - root?: string; - config?: string; - init?: boolean; - chart?: ChartConfig -} - -export interface Args extends RawArgs { - chart: ChartConfig -} - -const parsedArgv: RawArgs = yargs(hideBin(global.process.argv)).argv as RawArgs; - -// set a default chart config instance from cli options -parsedArgv.chart = new ChartConfig(parsedArgv.chart); - -export const argv = parsedArgv as Args; diff --git a/packages/cli/src/lib/default-config.ts b/packages/cli/src/lib/default-config.ts deleted file mode 100644 index 3227c02..0000000 --- a/packages/cli/src/lib/default-config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Config } from './models/config'; - -const defaultConfig: Config = { - categories: { - legacy: { - fileTypes: ['js', 'ts', 'html', 'css', 'scss', 'less'], - includes: ['./legacy'], - jsTsPairs: 'js', - }, - modern: { - fileTypes: ['ts', 'html', 'css', 'scss', 'less'], - includes: ['./modern'], - jsTsPairs: 'ts', - }, - }, - ignorePatterns: ['node_modules'], - outputDir: 'hd-tracker', -}; - -export default defaultConfig; diff --git a/packages/cli/src/lib/index.ts b/packages/cli/src/lib/index.ts deleted file mode 100644 index f6fbd31..0000000 --- a/packages/cli/src/lib/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { resolve } from 'path'; -import { processConfig } from './process-config'; -import { initialize } from './initialize'; -import { createDataVizIn, getData, getTheRootDirectory, readConfig, saveResults } from './util'; -import { argv } from './argv'; - - -(async function run() { - - const localRootDir = getTheRootDirectory(global.process.cwd()); - - if (argv.init) { - initialize(localRootDir); - return; - } - - const rootDirectory = argv.root ? resolve(argv.root) : localRootDir; - const config = readConfig(localRootDir, argv.config); - - const results = await processConfig(config, rootDirectory); - - saveResults(localRootDir, config.outputDir, results); - - - const allData = getData(localRootDir, config.outputDir); - - const parentDir = resolve(localRootDir, config.outputDir); - - await createDataVizIn(parentDir, allData); -}()); diff --git a/packages/cli/src/lib/initialize.ts b/packages/cli/src/lib/initialize.ts deleted file mode 100644 index f330218..0000000 --- a/packages/cli/src/lib/initialize.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { join } from 'path'; -import { existsSync, writeFileSync, mkdirSync } from 'fs'; - -import defaultConfig from './default-config'; - -export function initialize(rootDir: string) { - const output = JSON.stringify(defaultConfig, null, 2); - const dir = join(rootDir, 'hd-tracker'); - if (!existsSync(dir)) { - mkdirSync(dir); - } - writeFileSync(join(dir, 'config.json'), output); -} diff --git a/packages/cli/src/lib/models/aggregate-result.ts b/packages/cli/src/lib/models/aggregate-result.ts deleted file mode 100644 index c66c49f..0000000 --- a/packages/cli/src/lib/models/aggregate-result.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TotalResult } from './total-result'; - -export interface AggregateResult extends TotalResult { - fileType: string; -} diff --git a/packages/cli/src/lib/models/category-result.ts b/packages/cli/src/lib/models/category-result.ts deleted file mode 100644 index 33bec2e..0000000 --- a/packages/cli/src/lib/models/category-result.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AggregateResult } from './aggregate-result'; -import { TotalResult } from './total-result'; - -export interface CategoryResult { - name: string; - totals: TotalResult; - fileTypes: AggregateResult[]; -} diff --git a/packages/cli/src/lib/models/category.ts b/packages/cli/src/lib/models/category.ts deleted file mode 100644 index 6ea7638..0000000 --- a/packages/cli/src/lib/models/category.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface Category extends CategoryDefinition { - name: string; -} - -export interface CategoryDefinition { - fileTypes: string[]; - includes: string[]; - excludes?: string[]; - jsTsPairs?: 'js' | 'ts' | 'ignore'; -} diff --git a/packages/cli/src/lib/models/chart-config.spec.ts b/packages/cli/src/lib/models/chart-config.spec.ts deleted file mode 100644 index 684998e..0000000 --- a/packages/cli/src/lib/models/chart-config.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { argv } from '../argv'; -import { ChartConfig } from './chart-config'; - - -describe('ChartConfig', () => { - it('should be default chart configuration on argv', () => { - expect(argv.chart).toStrictEqual(new ChartConfig()); - }); - - const fakeCliOptions = { - perCategoryAndFileType: true, - perCategoryTotals: false, - width: 42, - height: 42, - bg: 'negro', - title: 'Grine-DING', - xAxisLabel: 'tomorrows', - yAxisLabel: 'weather', - overwrite: false, - outFile: 'flibbidy-giblets.png', - } as ChartConfig; - - const defaultInstance = new ChartConfig(); - - // loop over all public keys to set key differently - Object.keys(fakeCliOptions).forEach((keyToTest) => { - - const instantiatedViaCli = new ChartConfig({ [keyToTest]: fakeCliOptions[keyToTest] } as any); - - it(`should override default '${keyToTest}: ${defaultInstance[keyToTest]}' value with '${keyToTest}: ${instantiatedViaCli[keyToTest]}'`, () => { - expect(defaultInstance[keyToTest]).not.toEqual(instantiatedViaCli[keyToTest]); - }); - - // loop over all remaining keys after having set one key different - Object.keys(fakeCliOptions).forEach((otherKey) => { - if (otherKey === keyToTest) { return; } - - // testing differing keys; values should match - it(`should be default '${otherKey}: ${defaultInstance[otherKey]}' value with '${otherKey}: ${instantiatedViaCli[otherKey]}'`, () => { - expect(defaultInstance[otherKey]).toEqual(instantiatedViaCli[otherKey]); - }); - }); - }) - -}); - diff --git a/packages/cli/src/lib/models/chart-config.ts b/packages/cli/src/lib/models/chart-config.ts deleted file mode 100644 index c5ec109..0000000 --- a/packages/cli/src/lib/models/chart-config.ts +++ /dev/null @@ -1,86 +0,0 @@ -export class ChartConfig { - private _perCategoryAndFileType = false; - private _perCategoryTotals = true; - private _width = 1200; - private _height = 800; - private _bg = 'white'; - private _title = 'Migration Progress LOC'; - private _xAxisLabel = 'Date'; - private _yAxisLabel = 'Totals'; - private _overwrite = true; - private _outFile = 'viz.png'; - - get perCategoryAndFileType(): boolean { - // if false, use false; false !== undefined - return this._cliOptions.perCategoryAndFileType !== undefined ? - this._cliOptions.perCategoryAndFileType : - this._perCategoryAndFileType; - } - - get perCategoryTotals(): boolean { - // if false, use false; false !== undefined - return this._cliOptions.perCategoryTotals !== undefined ? - this._cliOptions.perCategoryTotals: - this._perCategoryTotals; - } - - get width(): number { - // if set and non-numeric - if (this._cliOptions.width !== undefined && isNaN(Number(this._cliOptions.width))) { - throw Error('--chart.width must be a number'); - } - // if unset or numeric - return this._cliOptions.width !== undefined ? - this._cliOptions.width: - this._width; - } - - get height(): number { - // if set and non-numeric - if (this._cliOptions.height !== undefined && isNaN(Number(this._cliOptions.height))) { - throw Error('--chart.height must be a number'); - } - // if unset or numeric - return this._cliOptions.height !== undefined ? - this._cliOptions.height: - this._height; - } - - get bg(): string { - return this._cliOptions.bg ? - this._cliOptions.bg: - this._bg; - } - - get title(): string { - return this._cliOptions.title ? - this._cliOptions.title: - this._title; - } - - get xAxisLabel(): string { - return this._cliOptions.xAxisLabel ? - this._cliOptions.xAxisLabel: - this._xAxisLabel; - } - - get yAxisLabel(): string { - return this._cliOptions.yAxisLabel ? - this._cliOptions.yAxisLabel: - this._yAxisLabel; - } - - get overwrite(): boolean { - return this._cliOptions.overwrite !== undefined ? - this._cliOptions.overwrite: - this._overwrite; - } - - get outFile(): string { - return this._cliOptions.outFile ? - this._cliOptions.outFile: - this._outFile; - } - - constructor(private _cliOptions: ChartConfig = {} as ChartConfig) { } -} diff --git a/packages/cli/src/lib/models/config.ts b/packages/cli/src/lib/models/config.ts deleted file mode 100644 index e107c9a..0000000 --- a/packages/cli/src/lib/models/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CategoryDefinition } from './category'; - -export interface Config { - categories: { [key: string]: CategoryDefinition }; - ignorePatterns?: string[]; - outputDir: string; -} diff --git a/packages/cli/src/lib/models/file-result.ts b/packages/cli/src/lib/models/file-result.ts deleted file mode 100644 index e7cfbc4..0000000 --- a/packages/cli/src/lib/models/file-result.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Result } from './result'; - -export interface FileResult extends Result { - fileType: string; - path: string; -} diff --git a/packages/cli/src/lib/models/process-result.ts b/packages/cli/src/lib/models/process-result.ts deleted file mode 100644 index 5750d60..0000000 --- a/packages/cli/src/lib/models/process-result.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CategoryResult } from './category-result'; - -export interface ProcessResult { - timestamp: string; - hash: string; - categories: CategoryResult[]; -} diff --git a/packages/cli/src/lib/models/result.ts b/packages/cli/src/lib/models/result.ts deleted file mode 100644 index fcc7661..0000000 --- a/packages/cli/src/lib/models/result.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface Result { - total: number; - source: number; - comment: number; - single: number; - block: number; - mixed: number; - empty: number; - todo: number; - blockEmpty: number; -} diff --git a/packages/cli/src/lib/models/total-result.ts b/packages/cli/src/lib/models/total-result.ts deleted file mode 100644 index dfbfb07..0000000 --- a/packages/cli/src/lib/models/total-result.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Result } from './result'; - -export interface TotalResult extends Result { - fileCount: number; -} diff --git a/packages/cli/src/lib/models/viz-dataset.ts b/packages/cli/src/lib/models/viz-dataset.ts deleted file mode 100644 index 16100d8..0000000 --- a/packages/cli/src/lib/models/viz-dataset.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type VizDataset = { - label: string; - data: number[]; -}; diff --git a/packages/cli/src/lib/models/viz-labels-datasets.ts b/packages/cli/src/lib/models/viz-labels-datasets.ts deleted file mode 100644 index b7055da..0000000 --- a/packages/cli/src/lib/models/viz-labels-datasets.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { VizDataset } from './viz-dataset' - -export type VizLabelsDatasets = { - labels: string[], - datasets: VizDataset[], -} diff --git a/packages/cli/src/lib/process-category.ts b/packages/cli/src/lib/process-category.ts deleted file mode 100644 index 23bb7e8..0000000 --- a/packages/cli/src/lib/process-category.ts +++ /dev/null @@ -1,190 +0,0 @@ -import * as sloc from 'sloc'; - -import { Category } from './models/category'; -import { FileResult } from './models/file-result'; -import { lstatSync, readdirSync, readFileSync } from 'fs'; -import { join, extname } from 'path'; -import { CategoryResult } from './models/category-result'; -import { TotalResult } from './models/total-result'; -import { AggregateResult } from './models/aggregate-result'; - -export function processCategory( - rootDirectory: string, - category: Category, - ignorePatterns: string[] -): CategoryResult { - console.log(`Processing "${category.name}"...`); - const allFiles = category.includes.reduce((acc, include) => { - return [...acc, ...findAllFilesInDirectory(join(rootDirectory, include))]; - }, [] as string[]); - - const includedFiles = findIncludedFiles(category, ignorePatterns, allFiles); - - console.log(` ${includedFiles.length} files...`); - const results = includedFiles.map(getFileStats); - - const resultMap = aggregateResults(results); - - const aggregatedResults = Object.values(resultMap).map((val) => val); - - const totals = aggregatedResults.reduce( - (totals, curr) => ({ - fileCount: totals.fileCount + curr.fileCount, - total: totals.total + curr.total, - source: totals.source + curr.source, - comment: totals.comment + curr.comment, - single: totals.single + curr.single, - block: totals.block + curr.block, - mixed: totals.mixed + curr.mixed, - empty: totals.empty + curr.empty, - todo: totals.todo + curr.todo, - blockEmpty: totals.blockEmpty + curr.blockEmpty, - }), - { - fileCount: 0, - total: 0, - source: 0, - comment: 0, - single: 0, - block: 0, - mixed: 0, - empty: 0, - todo: 0, - blockEmpty: 0, - } as TotalResult - ); - const final = { - name: category.name, - totals: totals, - fileTypes: aggregatedResults, - }; - - console.log(` ${final.totals.total} total lines`); - - return final; -} - -function aggregateResults(results: FileResult[]): { - [key: string]: AggregateResult; -} { - return results.reduce((acc, result) => { - const fileTypeResults = acc[result.fileType]; - if (!fileTypeResults) { - acc[result.fileType] = { - fileType: result.fileType, - fileCount: 1, - total: result.total, - source: result.source, - comment: result.comment, - single: result.single, - block: result.block, - mixed: result.mixed, - empty: result.empty, - todo: result.todo, - blockEmpty: result.blockEmpty, - }; - } else { - acc[result.fileType] = { - fileType: result.fileType, - fileCount: fileTypeResults.fileCount + 1, - total: fileTypeResults.total + result.total, - source: fileTypeResults.source + result.source, - comment: fileTypeResults.comment + result.comment, - single: fileTypeResults.single + result.single, - block: fileTypeResults.block + result.block, - mixed: fileTypeResults.mixed + result.mixed, - empty: fileTypeResults.empty + result.empty, - todo: fileTypeResults.todo + result.todo, - blockEmpty: fileTypeResults.blockEmpty + result.blockEmpty, - }; - } - return acc; - }, {} as { [key: string]: AggregateResult }); -} - -function getFileStats(file: string): FileResult { - const contents = readFileSync(file).toString('utf-8'); - const fileType = getFileExt(file); - const stats = sloc(contents, fileType); - return { - path: file, - fileType: fileType, - ...stats, - }; -} - -function findIncludedFiles( - category: Category, - ignorePatterns: string[], - allFiles: string[] -): string[] { - return allFiles - .filter((file) => { - const ext = getFileExt(file); - let shouldBeIncluded = !!category.fileTypes.find( - (fileType) => fileType === ext - ); - if (shouldBeIncluded) { - ignorePatterns?.forEach((ignorePattern) => { - if (file.indexOf(ignorePattern) !== -1) { - shouldBeIncluded = false; - } - }); - } - if (shouldBeIncluded) { - category.excludes?.forEach((exclude) => { - if (file.indexOf(exclude) !== -1) { - shouldBeIncluded = false; - } - }); - } - return shouldBeIncluded; - }) - .filter((file, _index, files) => { - if (category.jsTsPairs === 'ignore' || category.jsTsPairs === undefined) { - return true; - } - const fileExtToKeep = category.jsTsPairs; - const ext = getFileExt(file); - const fileExtToDiscard = fileExtToKeep === 'js' ? 'ts' : 'js'; - - // if it is the extension to keep - // or if it is not the one to discard, we keep those files - if (fileExtToKeep === ext || fileExtToDiscard !== ext) { - return true; - } - - // get the counterpart's extension - const counterpartExt = ext === 'js' ? 'ts' : 'js'; - const parts = file.split('.'); - parts[parts.length - 1] = counterpartExt; - - const counterpartExists = - files.filter((f) => f === parts.join('.')).length !== 0; - - if (counterpartExists) { - return false; - } - - return true; - }); -} - -function findAllFilesInDirectory(directory: string): string[] { - const results = readdirSync(directory); - const subfiles = results - .filter((result) => lstatSync(join(directory, result)).isDirectory()) - .reduce((acc, subdir) => { - const files = findAllFilesInDirectory(join(directory, subdir)); - return [...acc, ...files]; - }, [] as string[]); - - const files = results - .filter((result) => lstatSync(join(directory, result)).isFile()) - .map((fileName) => join(directory, fileName)); - return [...files, ...subfiles]; -} - -function getFileExt(file: string): string { - return extname(file).replace(/\./g, ''); -} diff --git a/packages/cli/src/lib/process-config.ts b/packages/cli/src/lib/process-config.ts deleted file mode 100644 index f2268ed..0000000 --- a/packages/cli/src/lib/process-config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Config } from './models/config'; -import { ProcessResult } from './models/process-result'; -import { processCategory } from './process-category'; -import { getGitCommit } from './util'; - -export async function processConfig( - config: Config, - rootDirectory: string -): Promise { - console.log(`Starting...`); - const categoryResults = Object.entries(config.categories).map( - ([name, category]) => - processCategory( - rootDirectory, - { ...category, name }, - config.ignorePatterns || [] - ) - ); - const commit = await getGitCommit(); - return { - timestamp: commit.timestamp, - hash: commit.hash, - categories: categoryResults, - }; -} diff --git a/packages/cli/src/lib/tracker-chart.spec.ts b/packages/cli/src/lib/tracker-chart.spec.ts deleted file mode 100644 index 886c617..0000000 --- a/packages/cli/src/lib/tracker-chart.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { argv } from './argv'; -import { ChartConfig } from './models/chart-config'; -import { TrackerChart } from './tracker-chart'; - - -describe('TrackerChart', () => { - - describe('ctor', () => { - let chart; - beforeEach(() => { - chart = new TrackerChart(argv.chart, [], 'foo') - }); - - it('should have default _config', () => { - expect((chart as any)._config).toStrictEqual(new ChartConfig()); - }); - - it('should have empty _allProcessResults', () => { - expect((chart as any)._allProcessResults).toStrictEqual([]); - }); - - it('should have foo _dateFormat', () => { - expect((chart as any)._dateFormat).toStrictEqual('foo'); - }); - }); - - describe('private methods', () => { - let spyChart: TrackerChart; - let ogGetDataAndLabels; - let getDataAndLabelsSpy: jest.SpyInstance; - let getTotalsPerCategorySpy: jest.SpyInstance; - let getTotalsPerFileTypePerCategorySpy: jest.SpyInstance; - let generateGraphImageFileSpy: jest.SpyInstance; - const noop = () => { undefined }; - beforeEach(() => { - spyChart = new TrackerChart(argv.chart, [1] as any, 'foo'); - ogGetDataAndLabels = (spyChart as any).getDataAndLabels; - getDataAndLabelsSpy = jest.spyOn((spyChart as any), 'getDataAndLabels').mockImplementation(noop); - getTotalsPerCategorySpy = jest.spyOn((spyChart as any), 'getTotalsPerCategory').mockImplementation(noop); - getTotalsPerFileTypePerCategorySpy = jest.spyOn((spyChart as any), 'getTotalsPerFileTypePerCategory').mockImplementation(noop); - generateGraphImageFileSpy = jest.spyOn((spyChart as any), 'generateGraphImageFile').mockImplementation(noop); - - }); - - afterEach(() => { - getDataAndLabelsSpy.mockReset(); - getTotalsPerCategorySpy.mockReset(); - getTotalsPerFileTypePerCategorySpy.mockReset(); - generateGraphImageFileSpy.mockReset(); - }); - - it('calls getDataAndLabels inside writeTo', () => { - spyChart.writeTo(''); - expect(getDataAndLabelsSpy).toHaveBeenCalledWith([1], 'total'); - }); - - it('calls generateGraphImageFileSpy inside writeTo', () => { - spyChart.writeTo(''); - expect(generateGraphImageFileSpy).toHaveBeenCalledWith('', undefined); - }); - - it('calls getTotalsPerCategory inside getDataLabels', () => { - (spyChart as any).getDataAndLabels = ogGetDataAndLabels; - (spyChart as any).getDataAndLabels([1], 'total'); - expect(getTotalsPerCategorySpy).toHaveBeenCalledWith([1], 'total'); - (spyChart as any).getDataAndLabels = getDataAndLabelsSpy; - }); - - it('calls getTotalsPerFileTypePerCategory inside getDataLabels', () => { - (spyChart as any)._config._perCategoryTotals = false; - (spyChart as any)._config._perCategoryAndFileType = true; - (spyChart as any).getDataAndLabels = ogGetDataAndLabels; - (spyChart as any).getDataAndLabels([1], 'total'); - expect(getTotalsPerFileTypePerCategorySpy).toHaveBeenCalledWith([1], 'total'); - (spyChart as any).getDataAndLabels = getDataAndLabelsSpy; - }); - - it('calls getTotalsPerCategory inside getDataLabels by default', () => { - (spyChart as any)._config._perCategoryTotals = undefined; - (spyChart as any)._config._perCategoryAndFileType = undefined; - (spyChart as any).getDataAndLabels = ogGetDataAndLabels; - (spyChart as any).getDataAndLabels([1], 'total'); - expect(getTotalsPerCategorySpy).toHaveBeenCalledWith([1], 'total'); - (spyChart as any).getDataAndLabels = getDataAndLabelsSpy; - }); - - }); - -}); diff --git a/packages/cli/src/lib/tracker-chart.ts b/packages/cli/src/lib/tracker-chart.ts deleted file mode 100644 index 6fd40ee..0000000 --- a/packages/cli/src/lib/tracker-chart.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { join } from 'path'; -import { AggregateResult } from './models/aggregate-result'; -import { ChartConfig } from './models/chart-config'; -import { ProcessResult } from './models/process-result'; -import { VizDataset } from './models/viz-dataset'; -import { VizLabelsDatasets } from './models/viz-labels-datasets'; -import { existsSync, rmSync, writeFileSync } from 'fs'; -import { format, parse } from 'date-fns'; -import { ChartJSNodeCanvas } from 'chartjs-node-canvas'; -import * as ChartDataLabels from 'chartjs-plugin-datalabels'; -import * as autocolors from 'chartjs-plugin-autocolors'; -import { ChartConfiguration } from 'chart.js'; - -export class TrackerChart { - - constructor(private _config: ChartConfig, private _allProcessResults: ProcessResult[], private _dateFormat: string) { } - - private getDataAndLabels(allJsonData, propName: string): VizLabelsDatasets { - if (this._config.perCategoryTotals) { - return this.getTotalsPerCategory(allJsonData, propName); - } - - if (this._config.perCategoryAndFileType) { - return this.getTotalsPerFileTypePerCategory(allJsonData, propName); - } - - return this.getTotalsPerCategory(allJsonData, propName); - } - - private getTotalsPerFileTypePerCategory(allJsonData, propName: string): VizLabelsDatasets { - const runs = { }; - const allTypes = { }; - allJsonData.forEach((jsonData: ProcessResult, i) => { - runs[jsonData.hash] = jsonData.timestamp; - jsonData.categories.forEach((category) => { - category.fileTypes.forEach((t) => { - const label = `${category.name}: ${t.fileType}`; - if (!allTypes[label]) { - allTypes[label] = { - label, - data: [] - } - } - allTypes[label].data[i] = t[propName]; - }); - }); - }); - - const labels = Object.values(runs) as string[]; - const datasets = Object.values(allTypes).map((t: VizDataset) => { - return { - ...t, - fill: false, - tension: .1 - } - }) as VizDataset[]; - - return { - labels, - datasets, - }; - } - - private getTotalsPerCategory(allJsonData, propName: string): VizLabelsDatasets { - const runs = { }; - const allTypes = { }; - allJsonData.forEach((jsonData: ProcessResult, i) => { - runs[jsonData.hash] = jsonData.timestamp; - jsonData.categories.forEach((category) => { - const label = category.name; - if (!allTypes[label]) { - allTypes[label] = { - label, - data: [] - }; - } - - allTypes[label].data[i] = category.totals[propName]; - }); - }); - - const labels = Object.values(runs) as string[]; - const datasets = Object.values(allTypes).map((t: VizDataset) => { - return { - ...t, - fill: false, - tension: .1 - } - }) as VizDataset[]; - - return { - labels, - datasets, - }; - } - - private async generateGraphImageFile(parentDirectory: string, vizData: VizLabelsDatasets): Promise { - const outFile = join(parentDirectory, this._config.outFile); - - if (this._config.overwrite && existsSync(outFile)) { rmSync(outFile); } - - const dateFmt = this._dateFormat; - const configuration = { - type: 'line', - data: vizData, - options: { - elements: { - point:{ - radius: 0 - } - }, - plugins: { - title: { - display: true, - text: this._config.title - }, - autocolors: { - enabled: true, - mode: 'data', - }, - scales: { - x: { - type: 'timeseries', - time: { - minUnit: 'week', - }, - parsing: false, - title: { - display: true, - text: this._config.xAxisLabel - }, - ticks: { - source: 'data', - callback: function(val) { - return format( - parse(this.getLabelForValue(val), dateFmt, new Date()), - 'yyyy-MM-dd' - ); - } - } - }, - y: { - title: { - display: true, - text: this._config.yAxisLabel - } - } - } - } - }, - }; - - try { - const pieChart = new ChartJSNodeCanvas({ - width: this._config.width, - height: this._config.height, - backgroundColour: this._config.bg, - plugins: { - modern: [ChartDataLabels, autocolors], - } - }); - - const result = await pieChart.renderToBuffer(configuration as ChartConfiguration); - - writeFileSync(outFile, result); - } catch (exc) { - console.error(exc); - } - } - - writeTo(parentDirectory: string, graphablePropertyName: keyof AggregateResult = 'total'): Promise { - - const vizData = this.getDataAndLabels(this._allProcessResults, graphablePropertyName); - - return this.generateGraphImageFile(parentDirectory, vizData); - } -} diff --git a/packages/cli/src/lib/util.spec.ts b/packages/cli/src/lib/util.spec.ts deleted file mode 100644 index 7679eaf..0000000 --- a/packages/cli/src/lib/util.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { trace } from 'console'; -import { argv } from './argv'; -import { ChartConfig } from './models/chart-config'; -import { ProcessResult } from './models/process-result'; -import { TrackerChart } from './tracker-chart'; -import { - createDataVizIn, - getGitCommit, - getTheRootDirectory, - readConfig, - saveResults, -} from './util'; -import * as fs from 'fs'; - -jest.mock('fs', () => { - return { - existsSync: jest.fn().mockImplementation((p) => { - switch (p) { - case '/a/package.json': - return true; - case '/custom/dir/something.json': - return true; - case '/x/z/data.json': - return true; - case '/x/z/viz.png': - return true; - default: - return false; - } - }), - readFileSync: jest.fn().mockImplementation((p) => { - switch (p) { - case '/hd-tracker/data.json': - return JSON.stringify({ - prop: 'value', - }); - case '/custom/dir/something.json': - return JSON.stringify({ - customProp: 'custom value', - }); - case '/x/z/data.json': - return JSON.stringify([]); - } - }), - rmSync: jest.fn(), - writeFileSync: jest.fn(), - }; -}); - -jest.mock('git-last-commit', () => { - return { - getLastCommit: (f) => { - f(null, { - hash: 'abc123', - committedOn: '1437988060', - }); - }, - }; -}); - -describe('util', () => { - beforeEach(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (fs.writeFileSync as any).mockReset(); - }); - - describe('getTheRootDirectory', () => { - it('should get the project root directory when it is the root directory', () => { - const result = getTheRootDirectory('/a'); - expect(result).toEqual('/a'); - }); - - it('should get the project root directory from a child directory', () => { - const result = getTheRootDirectory('/a/b/c'); - expect(result).toEqual('/a'); - }); - }); - - describe('readConfig', () => { - it('should read the config file', () => { - const result = readConfig('/'); - expect(result).toEqual({ prop: 'value' }); - }); - - it('should read a custom config file', () => { - const result = readConfig('/', '/custom/dir/something.json'); - expect(result).toEqual({ customProp: 'custom value' }); - }); - }); - - describe('saveResults', () => { - let localRootDir: string; - let outputDir: string; - let results: ProcessResult; - - beforeEach(() => { - localRootDir = '/x/'; - outputDir = 'z'; - results = { - timestamp: 'now', - hash: 'abc123', - categories: [], - }; - }); - - it('should write out the data to an existing data.json', () => { - saveResults(localRootDir, outputDir, results); - expect(fs.writeFileSync).toHaveBeenCalledTimes(1); - expect(fs.writeFileSync).toHaveBeenCalledWith( - '/x/z/data.json', - JSON.stringify([results], null, 2) - ); - }); - - it('should write out the data to a non-existing data.json', () => { - saveResults(localRootDir, 'empty', results); - expect(fs.writeFileSync).toHaveBeenCalledTimes(1); - expect(fs.writeFileSync).toHaveBeenCalledWith( - '/x/empty/data.json', - JSON.stringify([results], null, 2) - ); - }); - }); - - describe('getGitCommit', () => { - it('should get the hash from the current commit', async () => { - const result = await getGitCommit(); - expect(result.hash).toEqual('abc123'); - }); - - it('should get a formatted date from the current commit', async () => { - const result = await getGitCommit(); - expect(result.timestamp).toMatch( - /\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{3}/ - ); - }); - }); - - describe('createDataVizIn', () => { - - const writeToSpy = jest.spyOn(TrackerChart.prototype, 'writeTo'); - - beforeEach(() => { - writeToSpy.mockReset(); - }); - - it('should use default outFile', async () => { - await createDataVizIn('', [] as any, 'total'); - expect(writeToSpy).toHaveBeenCalled(); - }) - }) -}); diff --git a/packages/cli/src/lib/util.ts b/packages/cli/src/lib/util.ts deleted file mode 100644 index 7824dbe..0000000 --- a/packages/cli/src/lib/util.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { resolve, join } from 'path'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { format } from 'date-fns'; -import { Commit, getLastCommit } from 'git-last-commit'; -import { Config } from './models/config'; -import { ProcessResult } from './models/process-result'; -import { AggregateResult } from './models/aggregate-result'; -import { argv } from './argv'; -import { TrackerChart } from './tracker-chart'; - -const DATE_FORMAT = 'yyyy-MM-dd-HH-mm-ss-SSS'; - -/** - * - * - * - * internal alphabetized helper functions -> - * - */ - -function formatDate(date: Date): string { - return format(date, DATE_FORMAT); -} - -function getDataFilePath( - localRootDir: string, - outputDir: string, -) { - return resolve(join(localRootDir, outputDir, 'data.json')); -} - -function getGitDate(date: string): Date { - return new Date(+date * 1000); -} - -function getLastCommitAsPromise(): Promise { - return new Promise((resolve, reject) => { - getLastCommit((err, commit) => { - if (err) { - reject(err); - } - resolve(commit); - }); - }); -} - -/** - * - * - * - * exported alphabetized util functions -> - * - */ - -export async function createDataVizIn( - parentDirectory: string, - allJsonData: ProcessResult[], - graphablePropertyName: keyof AggregateResult = 'total' -): Promise { - const chart = new TrackerChart(argv.chart, allJsonData, DATE_FORMAT); - return chart.writeTo(parentDirectory, graphablePropertyName); -} - -export function getData( - localRootDir: string, - outputDir: string -) { - const outputPath = getDataFilePath(localRootDir, outputDir); - let contents = ''; - if (existsSync(outputPath)) { - contents = readFileSync(outputPath).toString('utf-8'); - } - return contents === '' ? [] : JSON.parse(contents); -} - -export async function getGitCommit(): Promise<{ - hash: string; - timestamp: string; -}> { - const commit = await getLastCommitAsPromise(); - return { - hash: commit.hash, - timestamp: formatDate(getGitDate(commit.committedOn)), - }; -} - -export function getTheRootDirectory(directory: string): string { - if (existsSync(join(directory, 'package.json'))) { - return directory; - } - return getTheRootDirectory(resolve(join(directory, '..'))); -} - -export function readConfig( - rootDirectory: string, - optionsPath?: string -): Config { - const path = - optionsPath && existsSync(join(rootDirectory, optionsPath)) - ? join(rootDirectory, optionsPath) - : join(rootDirectory, 'hd-tracker', 'data.json'); - - const contents = readFileSync(path).toString('utf-8'); - - return JSON.parse(contents); -} - -export function saveResults( - localRootDir: string, - outputDir: string, - results: ProcessResult -): void { - console.log('Outputting file'); - const output: ProcessResult[] = getData(localRootDir, outputDir); - if (!Array.isArray(output)) { - console.error('Invalid output file format'); - } - output.push(results); - const outputPath = getDataFilePath(localRootDir, outputDir); - const outputText = JSON.stringify(output, null, 2); - writeFileSync(outputPath, outputText); - console.log(`Output written to: ${outputPath}`); -}