From 7e59e6cb1ce17305efe31e60c6374d8783ab5046 Mon Sep 17 00:00:00 2001 From: Michael Mullins Date: Sat, 18 Sep 2021 15:24:58 +0400 Subject: [PATCH] feat(project): init with 1.0.0-beta.1 --- package.json | 2 +- src/analyze.ts | 204 +++++++++++++++++++++++++++++ src/index.ts | 2 + src/utils/check-deps.ts | 39 ++++++ src/utils/check-imports.ts | 53 ++++++++ src/utils/check-package-version.ts | 35 +++++ src/utils/count-import-hits.ts | 12 ++ src/utils/get-files.ts | 12 ++ src/utils/get-imports.ts | 41 ++++++ src/utils/get-package-json.ts | 7 + src/utils/get-treat-icon.ts | 5 + tests/index.spec.ts | 30 +++++ 12 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 src/analyze.ts create mode 100644 src/index.ts create mode 100644 src/utils/check-deps.ts create mode 100644 src/utils/check-imports.ts create mode 100644 src/utils/check-package-version.ts create mode 100644 src/utils/count-import-hits.ts create mode 100644 src/utils/get-files.ts create mode 100644 src/utils/get-imports.ts create mode 100644 src/utils/get-package-json.ts create mode 100644 src/utils/get-treat-icon.ts create mode 100644 tests/index.spec.ts diff --git a/package.json b/package.json index e818043..9d0a977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@public-js/ng-pkg-keeper", - "version": "0.0.1", + "version": "1.0.0-beta.1", "description": "Add description", "scripts": { "build": "npm run clean:dist && tsc", diff --git a/src/analyze.ts b/src/analyze.ts new file mode 100644 index 0000000..8d06064 --- /dev/null +++ b/src/analyze.ts @@ -0,0 +1,204 @@ +import { existsSync } from 'fs'; +import { resolve } from 'path'; + +import { checkLocal } from './utils/check-deps'; +import { checkImports } from './utils/check-imports'; +import { checkPackageVersion } from './utils/check-package-version'; +import { countImportHits } from './utils/count-import-hits'; +import { getImports } from './utils/get-imports'; +import { getPackageJson } from './utils/get-package-json'; +import { + IAnalyzeInput, + IObject, + IObjectTypes, + IPackage, + IPackageInput, + IPackageJsonData, + packageImportsDefault, +} from './types'; + +export function analyze(params: IAnalyzeInput): IPackage[] { + const timeStart = Date.now(); + analysisPreChecks(params); + + const rootJson: IPackageJsonData = + params.packageJson && (params.checkDeps === 'full' || params.checkPackageVersion) + ? getPackageJson(params.packageJson) + : {}; + + const packages: IPackage[] = []; + params.packages.forEach((pkgIn: IPackageInput) => { + const start = Date.now(); + const packageJson = packagePreChecks(pkgIn); + packages.push({ + name: pkgIn.name, + path: pkgIn.path, + packageJson, + jsonData: {}, + imports: packageImportsDefault, + importsReport: new Map([]), + versionReport: '', + time: Date.now() - start, + }); + }); + + packages.forEach((pkg: IPackage) => { + const start = Date.now(); + + if (params.checkDeps === 'full' || params.checkPackageVersion) { + pkg.jsonData = getPackageJson(pkg.packageJson); + } + if (params.countHits || params.checkImports || params.checkDeps) { + pkg.imports = getImports(pkg, params.matchExt || [], params.ignoreImports || []); + pkg.imports.importsUnique.forEach((item: string) => { + pkg.importsReport.set(item, {}); + }); + } + + if (params.countHits) { + const data = countImportHits(pkg.imports); + Array.from(data.entries()).forEach(([key, report]: [string, IObjectTypes]) => { + const item = pkg.importsReport.get(key) || {}; + item.Hits = report; + pkg.importsReport.set(key, item); + }); + } + + if (params.checkImports) { + const { + report: data, + hasErrors, + hasWarnings, + } = checkImports(pkg, packages, params.treatImports || null); + Array.from(data.entries()).forEach(([key, report]: [string, IObjectTypes]) => { + const item = pkg.importsReport.get(key) || {}; + item.Imports = report; + pkg.importsReport.set(key, item); + }); + if (hasErrors) { + pkg.hasErrors = true; + } + if (hasWarnings) { + pkg.hasWarnings = true; + } + } + + if (params.checkDeps) { + const { report: data, hasErrors, hasWarnings } = checkLocal(pkg, params.treatDeps || null); + Array.from(data.entries()).forEach(([key, report]: [string, IObjectTypes]) => { + const item = pkg.importsReport.get(key) || {}; + item.Dependencies = report; + pkg.importsReport.set(key, item); + }); + if (hasErrors) { + pkg.hasErrors = true; + } + if (hasWarnings) { + pkg.hasWarnings = true; + } + } + + if (params.checkPackageVersion) { + const { + report: data, + hasErrors, + hasWarnings, + } = checkPackageVersion(pkg, rootJson, params.treatPackageVersion || null); + pkg.versionReport = data; + if (hasErrors) { + pkg.hasErrors = true; + } + if (hasWarnings) { + pkg.hasWarnings = true; + } + } + + if (params.logToConsole) { + packageReportLog(pkg, params, start); + } + }); + + if (params.logToConsole) { + const totalReport: IObject = { + 'Packages analyzed': packages.length, + 'Total time spent (s)': (Date.now() - timeStart) / 1000, + }; + console.table(totalReport); + } + + if (packages.some((pkg: IPackage) => pkg.hasErrors)) { + if (params.throwError && params.logToConsole) { + throw new Error('Errors found. See the report above.'); + } else if (params.logToConsole) { + console.error('Errors found. See the report above.'); + } else if (params.throwError) { + throw new Error('Errors found. To see the report pass \'logToConsole\' to parameters.'); + } else { + console.error('Errors found. To see the report pass \'logToConsole\' to parameters.'); + } + } else if (packages.some((pkg: IPackage) => pkg.hasWarnings)) { + if (params.logToConsole) { + console.error('Warnings found. See the report above.'); + } else { + console.error('Warnings found. To see the report pass \'logToConsole\' to parameters.'); + } + } + + return packages; +} + +function analysisPreChecks(params: IAnalyzeInput): void { + if (params.packageJson && !existsSync(params.packageJson)) { + throw new Error(`The provided package.json path (${params.packageJson}) does not exist`); + } + if (params.checkDeps === 'full' && !params.packageJson) { + throw new Error('Can not fully check dependencies without package.json'); + } + if (params.checkPackageVersion && !params.packageJson) { + throw new Error('Can not check versions without package.json'); + } +} + +function packagePreChecks(pkgIn: IPackageInput): string { + if (!existsSync(pkgIn.path)) { + throw new Error('The provided package path does not exist: ' + pkgIn.path); + } + const packageJson = pkgIn?.packageJson || resolve(pkgIn.path + '/package.json'); + if (!existsSync(packageJson)) { + throw new Error('The provided package.json path does not exist: ' + packageJson); + } + return packageJson; +} + +function packageReportLog(pkg: IPackage, params: IAnalyzeInput, timeStart: number): void { + const packageReport: IObject = { + Package: pkg.name, + Path: pkg.path, + 'Version info': pkg.versionReport, + }; + if (params.countHits || params.checkImports || params.checkDeps) { + packageReport['Total files'] = pkg.imports.filesTotal; + packageReport['Matched files'] = pkg.imports.filesMatched; + packageReport['Total imports'] = pkg.imports.importsTotal; + packageReport['Matched imports'] = pkg.imports.importsMatched.length; + packageReport['Unique imports'] = pkg.imports.importsUnique.length; + } + const importsReport: IObject = {}; + if (pkg.importsReport.size > 0) { + Array.from(pkg.importsReport.entries()) + .filter(([, reports]: [string, IObject]) => Object.keys(reports).length > 0) + .forEach(([key, reports]: [string, IObject]) => { + importsReport[key] = reports; + }); + } + + pkg.time += Date.now() - timeStart; + packageReport['Time spent (s)'] = pkg.time / 1000; + + console.group(); + console.table(packageReport); + if (pkg.importsReport.size > 0) { + console.table(importsReport); + } + console.groupEnd(); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cdd09d4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export { analyze } from './analyze'; +export { IAnalyzeInput, IPackageInput, IPackage } from './types'; diff --git a/src/utils/check-deps.ts b/src/utils/check-deps.ts new file mode 100644 index 0000000..5cb875b --- /dev/null +++ b/src/utils/check-deps.ts @@ -0,0 +1,39 @@ +import { IPackage, TImportsReport, TTreatCallbackDep, TTreatTypes } from '../types'; +import { getTreatIcon } from './get-treat-icon'; + +export function checkLocal( + pkg: IPackage, + treatAs: TTreatTypes | TTreatCallbackDep +): { report: TImportsReport; hasErrors: boolean; hasWarnings: boolean } { + const allDependencies: { [name: string]: string } = { + ...(pkg.jsonData.dependencies || {}), + ...(pkg.jsonData.devDependencies || {}), + ...(pkg.jsonData.peerDependencies || {}), + }; + const packageDeps: string[] = Array.from(new Set(Object.keys(allDependencies))); + + const absoluteImports: string[] = pkg.imports.importsUnique.filter( + (item: string) => !item.includes('./') + ); + + const tempReport = new Map([]); + absoluteImports.forEach((item: string) => { + if (!packageDeps.some((dep: string) => item.startsWith(dep))) { + const treat: TTreatTypes = + typeof treatAs === 'function' ? treatAs(pkg.name, 'local', item) : treatAs; + tempReport.set(item, { data: getTreatIcon(treat) + 'Not listed in local package.json', treat }); + } + }); + + const reportEntries = Array.from(tempReport.entries()); + const report: TImportsReport = new Map([]); + reportEntries.forEach(([key, item]: [string, { data: string; treat: TTreatTypes }]) => { + report.set(key, item.data); + }); + + return { + report, + hasErrors: reportEntries.some(([, item]) => item.treat === 'err'), + hasWarnings: reportEntries.some(([, item]) => item.treat === 'warn'), + }; +} diff --git a/src/utils/check-imports.ts b/src/utils/check-imports.ts new file mode 100644 index 0000000..a95a990 --- /dev/null +++ b/src/utils/check-imports.ts @@ -0,0 +1,53 @@ +import { resolve } from 'path'; + +import { IObjectTypes, IPackage, TImportsReport, TTreatCallbackImport, TTreatTypes } from '../types'; +import { getTreatIcon } from './get-treat-icon'; + +export function checkImports( + pkg: IPackage, + packages: IPackage[], + treatAs: TTreatTypes | TTreatCallbackImport +): { report: TImportsReport; hasErrors: boolean; hasWarnings: boolean } { + const tempReport = new Map([]); + + const selfImports: string[] = pkg.imports.importsUnique.filter((item: string) => + item.startsWith(pkg.name) + ); + + selfImports.forEach((item: string) => { + const treat: TTreatTypes = + typeof treatAs === 'function' ? treatAs(pkg.name, 'absSame', item) : treatAs; + tempReport.set(item, { data: getTreatIcon(treat) + 'Absolute import from the same package', treat }); + }); + + const relativeImports: string[] = pkg.imports.importsUnique.filter((item: string) => + item.includes('../') + ); + + if (relativeImports.length > 0) { + const otherPackages: string[] = packages + .map((pkg: IPackage) => pkg.path) + .filter((path: string) => path !== pkg.path); + + relativeImports.forEach((item: string) => { + const path: string = resolve(pkg.path + '/' + item); + if (otherPackages.some((otherPkg: string) => path.includes(otherPkg))) { + const treat: TTreatTypes = + typeof treatAs === 'function' ? treatAs(pkg.name, 'relExt', item) : treatAs; + tempReport.set(item, { data: getTreatIcon(treat) + 'External relative import', treat }); + } + }); + } + + const reportEntries = Array.from(tempReport.entries()); + const report: TImportsReport = new Map([]); + reportEntries.forEach(([key, item]: [string, { data: IObjectTypes; treat: TTreatTypes }]) => { + report.set(key, item.data); + }); + + return { + report, + hasErrors: reportEntries.some(([, item]) => item.treat === 'err'), + hasWarnings: reportEntries.some(([, item]) => item.treat === 'warn'), + }; +} diff --git a/src/utils/check-package-version.ts b/src/utils/check-package-version.ts new file mode 100644 index 0000000..a63ce4a --- /dev/null +++ b/src/utils/check-package-version.ts @@ -0,0 +1,35 @@ +import { IPackage, IPackageJsonData, TTreatCallbackVersion, TTreatTypes } from '../types'; +import { getTreatIcon } from './get-treat-icon'; + +export function checkPackageVersion( + pkg: IPackage, + rootJson: IPackageJsonData, + treatAs: TTreatTypes | TTreatCallbackVersion +): { report: string; hasErrors: boolean; hasWarnings: boolean } { + if (!pkg.jsonData.version) { + const treat: TTreatTypes = typeof treatAs === 'function' ? treatAs(pkg.name) : treatAs; + return { + report: getTreatIcon(treat) + 'Package version is not set', + hasErrors: treat === 'err', + hasWarnings: treat === 'warn', + }; + } + if (pkg.jsonData.version !== rootJson.version) { + const treat: TTreatTypes = typeof treatAs === 'function' ? treatAs(pkg.name) : treatAs; + return { + report: + pkg.jsonData.version + + ', ' + + getTreatIcon(treat) + + 'does not match project version: ' + + rootJson.version, + hasErrors: treat === 'err', + hasWarnings: treat === 'warn', + }; + } + return { + report: pkg.jsonData.version, + hasErrors: false, + hasWarnings: false, + }; +} diff --git a/src/utils/count-import-hits.ts b/src/utils/count-import-hits.ts new file mode 100644 index 0000000..ad0135b --- /dev/null +++ b/src/utils/count-import-hits.ts @@ -0,0 +1,12 @@ +import { IPackageImports, TImportsReport } from '../types'; + +export function countImportHits(imports: IPackageImports): TImportsReport { + const report: TImportsReport = new Map([]); + + imports.importsUnique.forEach((item: string) => { + const hits = imports.importsMatched.filter((imp: string) => imp === item); + report.set(item, hits.length); + }); + + return report; +} diff --git a/src/utils/get-files.ts b/src/utils/get-files.ts new file mode 100644 index 0000000..1223b38 --- /dev/null +++ b/src/utils/get-files.ts @@ -0,0 +1,12 @@ +import { Dirent, readdirSync } from 'fs'; +import { resolve } from 'path'; + +export function getFiles(dir: string): string[] { + const entries = readdirSync(dir, { withFileTypes: true }); + return entries + .map((entry: Dirent) => { + const res = resolve(dir, entry.name); + return entry.isDirectory() ? getFiles(res) : res; + }) + .flat(); +} diff --git a/src/utils/get-imports.ts b/src/utils/get-imports.ts new file mode 100644 index 0000000..479adf5 --- /dev/null +++ b/src/utils/get-imports.ts @@ -0,0 +1,41 @@ +import { readFileSync } from 'fs'; +import { extname } from 'path'; + +import { getFiles } from './get-files'; +import { IPackage, IPackageImports } from '../types'; + +export function getImports(pkg: IPackage, matchExt: string[], ignoreImports: string[]): IPackageImports { + const allFiles: string[] = getFiles(pkg.path); + const matchedFiles: string[] = + matchExt.length > 0 ? allFiles.filter((file: string) => matchExt.includes(extname(file))) : allFiles; + + const allImports = matchedFiles + .map((file: string) => { + const contents = readFileSync(file, 'utf8'); + const fileLines = contents.split(/\r?\n/).filter((line: string) => line.includes(' from \'')); + return fileLines + .map((line: string) => { + const match = line.match(/from '(.+?)'/); + return match && match.length > 1 ? match[1] : ''; + }) + .filter(Boolean); + }) + .flat(); + + const matchedImports = + ignoreImports.length > 0 + ? allImports.filter((line: string) => + ignoreImports.some((ignored: string) => line.includes(ignored)) + ) + : allImports; + + const uniqueImports = [...new Set(matchedImports)].sort(); + + return { + filesTotal: allFiles.length, + filesMatched: matchedFiles.length, + importsTotal: allImports.length, + importsMatched: matchedImports, + importsUnique: uniqueImports, + }; +} diff --git a/src/utils/get-package-json.ts b/src/utils/get-package-json.ts new file mode 100644 index 0000000..c9fc17b --- /dev/null +++ b/src/utils/get-package-json.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'fs'; + +import { IPackageJsonData } from '../types'; + +export function getPackageJson(path: string): IPackageJsonData { + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/src/utils/get-treat-icon.ts b/src/utils/get-treat-icon.ts new file mode 100644 index 0000000..9a3d1a8 --- /dev/null +++ b/src/utils/get-treat-icon.ts @@ -0,0 +1,5 @@ +import { TTreatTypes } from '../types'; + +export function getTreatIcon(treat: TTreatTypes): string { + return treat ? (treat === 'err' ? '❌ ' : '⚠️ ') : ''; +} diff --git a/tests/index.spec.ts b/tests/index.spec.ts new file mode 100644 index 0000000..592954f --- /dev/null +++ b/tests/index.spec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import { readFileSync } from 'fs'; +import 'mocha'; +import { resolve } from 'path'; + +import { analyze, IPackageInput } from '../src'; + +describe('passing', () => { + const listPath: string = resolve(__dirname, './dummy-project/projects-list.json'); + const projectsList: string[] = JSON.parse(readFileSync(listPath, 'utf8')); + + const packages: IPackageInput[] = projectsList.map((project: string) => ({ + name: '@' + project, + path: resolve(__dirname, './dummy-project/' + project), + })); + + const analysis = analyze({ + packages, + matchExt: ['.ts'], + packageJson: resolve(__dirname, './dummy-project/package.json'), + countHits: true, + checkImports: true, + checkDeps: 'local', + checkPackageVersion: true, + logToConsole: true, + // throwError: true, + }); + + it('pass', () => expect(analysis).to.not.be.null); +});