From 8c5209286b408adc19e65dbc174e1b3a1e41def2 Mon Sep 17 00:00:00 2001 From: Michael Mullins Date: Sun, 3 Oct 2021 10:14:27 +0400 Subject: [PATCH 1/2] feat: refactoring feat: secondary entry points support --- package-lock.json | 8 +- package.json | 1 + src/analyze.ts | 209 ----------------- src/index.ts | 3 +- src/lib/analyze.ts | 147 ++++++++++++ src/lib/index.ts | 1 + src/lib/models/entry-point.ts | 22 ++ src/lib/models/imports-model-ep.ts | 9 + src/lib/models/imports-model.ts | 48 ++++ src/lib/models/ng-package-json.ts | 15 ++ src/lib/models/package-json.ts | 28 +++ src/lib/models/package-reports.ts | 30 +++ src/lib/models/package.ts | 54 +++++ src/lib/models/report-deps-full.ts | 109 +++++++++ src/lib/models/report-deps-local.ts | 41 ++++ src/lib/models/report-imports.ts | 48 ++++ src/lib/types/analyze-input.interface.ts | 30 +++ src/lib/types/base-package.interface.ts | 9 + src/lib/types/issue-level.types.ts | 1 + src/lib/types/object.types.ts | 3 + src/lib/types/process-dependencies.types.ts | 17 ++ src/lib/types/process-imports.types.ts | 5 + src/lib/types/process-pkg-version.types.ts | 3 + src/lib/types/report-item.interface.ts | 6 + src/lib/utils/check-dependencies.ts | 236 ++++++++++++++++++++ src/lib/utils/check-imports.ts | 128 +++++++++++ src/lib/utils/check-pkg-version.ts | 30 +++ src/lib/utils/count-imports-hits.ts | 6 + src/lib/utils/ensure-unix-path.ts | 21 ++ src/lib/utils/get-absolute-path.ts | 5 + src/lib/utils/get-files-recursive.ts | 31 +++ src/lib/utils/get-imports-from-files.ts | 48 ++++ src/lib/utils/get-imports-model-ep.ts | 17 ++ src/lib/utils/get-secondary-points.ts | 12 + src/lib/utils/level-to-icon.ts | 3 + src/lib/utils/read-json-file.ts | 20 ++ src/lib/utils/resolve-base-package.ts | 38 ++++ src/lib/utils/resolve-file-path.ts | 10 + src/lib/utils/resolve-package.ts | 45 ++++ src/types.ts | 223 ------------------ src/utils/check-deps.ts | 55 ----- src/utils/check-imports.ts | 89 -------- 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 | 83 ------- src/utils/get-package-json.ts | 7 - src/utils/get-treat-icon.ts | 5 - tests/dummy-project/dummy/sub1/package.json | 5 + tests/dummy-project/dummy/sub2/package.json | 7 +- tests/index.spec.ts | 14 +- 51 files changed, 1302 insertions(+), 742 deletions(-) delete mode 100644 src/analyze.ts create mode 100644 src/lib/analyze.ts create mode 100644 src/lib/index.ts create mode 100644 src/lib/models/entry-point.ts create mode 100644 src/lib/models/imports-model-ep.ts create mode 100644 src/lib/models/imports-model.ts create mode 100644 src/lib/models/ng-package-json.ts create mode 100644 src/lib/models/package-json.ts create mode 100644 src/lib/models/package-reports.ts create mode 100644 src/lib/models/package.ts create mode 100644 src/lib/models/report-deps-full.ts create mode 100644 src/lib/models/report-deps-local.ts create mode 100644 src/lib/models/report-imports.ts create mode 100644 src/lib/types/analyze-input.interface.ts create mode 100644 src/lib/types/base-package.interface.ts create mode 100644 src/lib/types/issue-level.types.ts create mode 100644 src/lib/types/object.types.ts create mode 100644 src/lib/types/process-dependencies.types.ts create mode 100644 src/lib/types/process-imports.types.ts create mode 100644 src/lib/types/process-pkg-version.types.ts create mode 100644 src/lib/types/report-item.interface.ts create mode 100644 src/lib/utils/check-dependencies.ts create mode 100644 src/lib/utils/check-imports.ts create mode 100644 src/lib/utils/check-pkg-version.ts create mode 100644 src/lib/utils/count-imports-hits.ts create mode 100644 src/lib/utils/ensure-unix-path.ts create mode 100644 src/lib/utils/get-absolute-path.ts create mode 100644 src/lib/utils/get-files-recursive.ts create mode 100644 src/lib/utils/get-imports-from-files.ts create mode 100644 src/lib/utils/get-imports-model-ep.ts create mode 100644 src/lib/utils/get-secondary-points.ts create mode 100644 src/lib/utils/level-to-icon.ts create mode 100644 src/lib/utils/read-json-file.ts create mode 100644 src/lib/utils/resolve-base-package.ts create mode 100644 src/lib/utils/resolve-file-path.ts create mode 100644 src/lib/utils/resolve-package.ts delete mode 100644 src/types.ts delete mode 100644 src/utils/check-deps.ts delete mode 100644 src/utils/check-imports.ts delete mode 100644 src/utils/check-package-version.ts delete mode 100644 src/utils/count-import-hits.ts delete mode 100644 src/utils/get-files.ts delete mode 100644 src/utils/get-imports.ts delete mode 100644 src/utils/get-package-json.ts delete mode 100644 src/utils/get-treat-icon.ts diff --git a/package-lock.json b/package-lock.json index ceaacf1..0f2445a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@public-js/ng-pkg-keeper", - "version": "0.0.1", + "version": "1.0.0-beta.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -506,6 +506,12 @@ "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==", "dev": true }, + "@types/semver": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz", + "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "4.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.1.tgz", diff --git a/package.json b/package.json index 473cd9e..6915d27 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/chai": "^4.2.14", "@types/mocha": "^8.2.0", "@types/node": "^14.6.0", + "@types/semver": "^6.2.3", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", "chai": "^4.2.0", diff --git a/src/analyze.ts b/src/analyze.ts deleted file mode 100644 index 712f2ca..0000000 --- a/src/analyze.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { existsSync } from 'fs'; -import { resolve } from 'path'; - -import { - IAnalyzeInput, - IObject, - IObjectTypes, - IPackage, - IPackageInput, - IPackageJsonData, - packageImportsDefault, -} from './types'; -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'; - -/** - * Run the packages analysis - * @param {IAnalyzeInput} params - Analysis configuration - * @returns {IPackage[]} - */ -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.bannedImports || [], 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.warn('Warnings found. See the report above.'); - } else { - console.warn('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 (Object.keys(importsReport).length > 0) { - console.table(importsReport); - } - console.groupEnd(); -} diff --git a/src/index.ts b/src/index.ts index cdd09d4..765fa01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1 @@ -export { analyze } from './analyze'; -export { IAnalyzeInput, IPackageInput, IPackage } from './types'; +export { analyze } from './lib'; diff --git a/src/lib/analyze.ts b/src/lib/analyze.ts new file mode 100644 index 0000000..4d6970a --- /dev/null +++ b/src/lib/analyze.ts @@ -0,0 +1,147 @@ +import { Package } from './models/package'; +import { IAnalyzeInput } from './types/analyze-input.interface'; +import { checkLocalList, checkVersions } from './utils/check-dependencies'; +import { checkImports } from './utils/check-imports'; +import { checkPkgVersion } from './utils/check-pkg-version'; +import { countImportsHits } from './utils/count-imports-hits'; +import { getImportsFromFiles } from './utils/get-imports-from-files'; +import { getImportsModelEp } from './utils/get-imports-model-ep'; +import { resolveBasePackage } from './utils/resolve-base-package'; +import { resolvePackage } from './utils/resolve-package'; + +export function analyze(params: IAnalyzeInput): Package[] { + const requireRootJson: boolean = params.checkDeps === 'full' || Boolean(params.checkPackageVersion); + const { basePath, packageJson } = resolveBasePackage(params.rootPath, !requireRootJson, true); + const packages: Package[] = params.packagesPaths.map((packagePath) => resolvePackage(packagePath)); + + packages.forEach((pkg) => { + if (params.countHits || params.checkImports || params.checkDeps) { + pkg.importsModel = getImportsFromFiles( + pkg.packageFilesArr, + params.matchExt || [], + params.ignoreImports || [] + ); + pkg.secondaryEPsArr.forEach((entryPoint) => { + entryPoint.importsModel = getImportsModelEp(pkg.importsModel, entryPoint.basePath, []); + }); + pkg.primaryEP.importsModel = getImportsModelEp( + pkg.importsModel, + pkg.primaryEP.basePath, + pkg.secondaryEPsArr.map((entryPoint) => entryPoint.basePath) + ); + } + + if (params.countHits) { + const hitsReport: [string, number][] = countImportsHits( + pkg.importsModel.importsUnique, + pkg.importsModel.importsMatched + ); + pkg.reports.importsHits = new Map(hitsReport); + } + + if (params.checkImports) { + pkg.reports.importsCheck = checkImports( + pkg, + params.bannedImports || [], + params.treatImports || null + ); + } + + if (params.checkDeps === 'local') { + pkg.reports.depsLocalCheck = checkLocalList(pkg, params.treatDeps || null); + } else if (params.checkDeps === 'full' && packageJson?.version) { + pkg.reports.depsLocalCheck = checkLocalList(pkg, params.treatDeps || null); + pkg.reports.depsFullCheck = checkVersions(pkg, packageJson, params.treatDeps || null); + } + + if (params.checkPackageVersion && packageJson?.version) { + pkg.reports.versionCheck = checkPkgVersion( + pkg, + packageJson.version, + params.treatPackageVersion || null + ); + } + }); + + if (params.logToConsole) { + // const replacer = (key: unknown, value: unknown) => { + // if (value instanceof Map) { + // return Array.from(value.entries()); + // } else if (value instanceof Set) { + // return Array.from(value); + // } else { + // return value; + // } + // }; + // console.log(JSON.stringify(packages, replacer)); + + packages.forEach((pkg: Package) => { + console.log('\n----------------------------------------------------------------------'); + console.log('Package: ' + pkg.packageName); + console.log('Path: ' + pkg.basePath.replace(basePath, '')); + if (params.logStats) { + console.log('------------------------------'); + console.log('Total files : ' + pkg.packageFilesArr.length); + console.log('Matched files : ' + pkg.importsModel.filesMatched.length); + console.log('Total imports : ' + pkg.importsModel.importsTotal); + console.log('Matched imports: ' + pkg.importsModel.importsMatched.length); + console.log('Unique imports : ' + pkg.importsModel.importsUnique.length); + } + if (params.countHits) { + console.log('------------------------------'); + Array.from(pkg.reports.importsHits.entries()).map(([imp, hits]) => console.log(imp, '–', hits)); + } + if (pkg.reports.hasErrors || pkg.reports.hasWarnings) { + if (pkg.reports.versionCheck?.details) { + console.log('------------------------------'); + console.log(pkg.reports.versionCheck.details); + } + if (pkg.reports.importsCheck) { + const details = pkg.reports.importsCheck.reportDetails; + Object.entries(details).forEach(([issue, report]) => { + console.log('------------------------------'); + console.log(issue); + console.log(report.join('\n')); + }); + } + if (pkg.reports.depsLocalCheck) { + const details = pkg.reports.depsLocalCheck.reportDetails; + Object.entries(details).forEach(([issue, report]) => { + console.log('------------------------------'); + console.log(issue); + console.log(report.join('\n')); + }); + } + if (pkg.reports.depsFullCheck) { + const details = pkg.reports.depsFullCheck.reportDetails; + Object.entries(details).forEach(([issue, report]) => { + console.log('------------------------------'); + console.log(issue); + console.log(report.join('\n')); + }); + } + } + // console.log('----------------------------------------------------------------------'); + }); + } + + if (packages.some((pkg: Package) => pkg.reports.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: Package) => pkg.reports.hasWarnings)) { + if (params.logToConsole) { + console.warn('Warnings found. See the report above.'); + } else { + console.warn('Warnings found. To see the report pass \'logToConsole\' to parameters.'); + } + } + + return packages; +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..faab126 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +export { analyze } from './analyze'; diff --git a/src/lib/models/entry-point.ts b/src/lib/models/entry-point.ts new file mode 100644 index 0000000..57a1fb9 --- /dev/null +++ b/src/lib/models/entry-point.ts @@ -0,0 +1,22 @@ +import { ImportsModelEp } from './imports-model-ep'; +import { NgPackageJson } from './ng-package-json'; + +export interface IEntryPoint { + basePath: string; + moduleId: string; + isPrimary: boolean; + ngPackageJson: NgPackageJson | null; +} + +export class EntryPoint implements IEntryPoint { + basePath = ''; + moduleId = ''; + isPrimary = true; + ngPackageJson: NgPackageJson | null = null; + + importsModel: ImportsModelEp = new ImportsModelEp(); + + constructor(params: IEntryPoint) { + Object.assign(this, params); + } +} diff --git a/src/lib/models/imports-model-ep.ts b/src/lib/models/imports-model-ep.ts new file mode 100644 index 0000000..d5acd67 --- /dev/null +++ b/src/lib/models/imports-model-ep.ts @@ -0,0 +1,9 @@ +export interface IImportsModelEp { + files: string[]; + importsToFiles: Map; +} + +export class ImportsModelEp implements IImportsModelEp { + files: string[] = []; + importsToFiles: Map = new Map(); +} diff --git a/src/lib/models/imports-model.ts b/src/lib/models/imports-model.ts new file mode 100644 index 0000000..df066a9 --- /dev/null +++ b/src/lib/models/imports-model.ts @@ -0,0 +1,48 @@ +export interface IImportsModel { + filesMatched: string[]; + importsTotal: number; + importsMatched: string[]; + importsUnique: string[]; + // filesToImports: Map; + // importsToFiles: Map; +} + +export class ImportsModel implements IImportsModel { + filesMatched: string[] = []; + importsTotal = 0; + importsMatched: string[] = []; + importsUnique: string[] = []; + + private _filesToImports: Map = new Map(); + private _filesToImportsEnt: Array<[string, string[]]> = []; + // importsToFiles: Map = new Map(); + + get filesToImports(): Map { + return this._filesToImports; + } + + set filesToImports(data: Map) { + this._filesToImports = data; + this._filesToImportsEnt = Array.from(data.entries()); + } + + get filesToImportsEnt(): Array<[string, string[]]> { + return this._filesToImportsEnt; + } + + mapImportsToFiles(files: string[]): Map { + const importsToFiles: Map = new Map(); + const ftiEntries: Array<[string, string[]]> = this._filesToImportsEnt.filter( + ([file]: [string, unknown]) => files.includes(file) + ); + this.importsUnique.forEach((item: string) => { + const files = ftiEntries + .filter(([, imports]: [unknown, string[]]) => imports.includes(item)) + .map(([file]: [string, unknown]) => file); + if (files.length > 0) { + importsToFiles.set(item, files); + } + }); + return importsToFiles; + } +} diff --git a/src/lib/models/ng-package-json.ts b/src/lib/models/ng-package-json.ts new file mode 100644 index 0000000..5da75b1 --- /dev/null +++ b/src/lib/models/ng-package-json.ts @@ -0,0 +1,15 @@ +export interface INgPackageJson { + allowedNonPeerDependencies: string[]; + lib: { + entryFile?: string; + }; +} + +export class NgPackageJson implements INgPackageJson { + allowedNonPeerDependencies: string[] = []; + lib: { entryFile?: string } = {}; + + constructor(params: Partial) { + Object.assign(this, params); + } +} diff --git a/src/lib/models/package-json.ts b/src/lib/models/package-json.ts new file mode 100644 index 0000000..ebbf0dd --- /dev/null +++ b/src/lib/models/package-json.ts @@ -0,0 +1,28 @@ +import { TObject } from '../types/object.types'; +import { INgPackageJson, NgPackageJson } from './ng-package-json'; + +export interface IPackageJson { + name?: string; + version?: string; + dependencies: TObject; + devDependencies: TObject; + peerDependencies: TObject; + ngPackage?: INgPackageJson; +} + +export class PackageJson implements IPackageJson { + name: string | undefined; + version: string | undefined; + dependencies: TObject = {}; + devDependencies: TObject = {}; + peerDependencies: TObject = {}; + ngPackage: INgPackageJson | undefined; + + constructor(params: Partial) { + Object.assign(this, params); + } + + get ngPackageJson(): NgPackageJson | null { + return this.ngPackage ? new NgPackageJson(this.ngPackage) : null; + } +} diff --git a/src/lib/models/package-reports.ts b/src/lib/models/package-reports.ts new file mode 100644 index 0000000..266b7ec --- /dev/null +++ b/src/lib/models/package-reports.ts @@ -0,0 +1,30 @@ +import { IReportItem } from '../types/report-item.interface'; +import { ReportDepsFull } from './report-deps-full'; +import { ReportDepsLocal } from './report-deps-local'; +import { ReportImports } from './report-imports'; + +export class PackageReports { + importsHits: Map = new Map(); + versionCheck: IReportItem | undefined; + importsCheck: ReportImports | undefined; + depsLocalCheck: ReportDepsLocal | undefined; + depsFullCheck: ReportDepsFull | undefined; + + get hasErrors(): boolean { + return ( + this.versionCheck?.level === 'err' || + this.importsCheck?.hasErrors || + this.depsLocalCheck?.hasErrors || + this.depsFullCheck?.hasErrors + ) || false; + } + + get hasWarnings(): boolean { + return ( + this.versionCheck?.level === 'warn' || + this.importsCheck?.hasWarnings || + this.depsLocalCheck?.hasWarnings || + this.depsFullCheck?.hasWarnings + ) || false; + } +} diff --git a/src/lib/models/package.ts b/src/lib/models/package.ts new file mode 100644 index 0000000..755d6e0 --- /dev/null +++ b/src/lib/models/package.ts @@ -0,0 +1,54 @@ +import { EntryPoint } from './entry-point'; +import { ImportsModel } from './imports-model'; +import { PackageJson } from './package-json'; +import { PackageReports } from './package-reports'; + +export interface IPackage { + packageName: string; + basePath: string; + packageJson: PackageJson; + packageFiles?: Set; +} + +export class Package implements IPackage { + packageName = ''; + basePath = ''; + packageJson!: PackageJson; + packageFiles: Set = new Set(); + importsModel: ImportsModel = new ImportsModel(); + primaryEP!: EntryPoint; + secondaryEPs: Map = new Map(); + reports: PackageReports = new PackageReports(); + + private entryPointIds: Set = new Set(); + + constructor(params: IPackage) { + Object.assign(this, params); + } + + get packageFilesArr(): string[] { + return Array.from(this.packageFiles); + } + + get entryPointsArr(): EntryPoint[] { + return [this.primaryEP, ...this.secondaryEPs.values()].filter(Boolean); + } + + get entryPointIdsArr(): string[] { + return Array.from(this.entryPointIds); + } + + get secondaryEPsArr(): EntryPoint[] { + return Array.from(this.secondaryEPs.values()); + } + + set primaryEntryPoint(item: EntryPoint) { + this.primaryEP = item; + this.entryPointIds.add(item.moduleId); + } + + set secondaryEntryPoint(item: EntryPoint) { + this.secondaryEPs.set(item.moduleId, item); + this.entryPointIds.add(item.moduleId); + } +} diff --git a/src/lib/models/report-deps-full.ts b/src/lib/models/report-deps-full.ts new file mode 100644 index 0000000..1de6616 --- /dev/null +++ b/src/lib/models/report-deps-full.ts @@ -0,0 +1,109 @@ +import { TObject } from '../types/object.types'; +import { IReportItem } from '../types/report-item.interface'; + +export class ReportDepsFull { + eLocalRMismatchRootR: Map = new Map(); + eLocalRMismatchRootV: Map = new Map(); + eLocalVMismatchRootR: Map = new Map(); + eLocalVMismatchRootV: Map = new Map(); + eLocalVerInvalid: Map = new Map(); + eRootMismatchDevDep: Map = new Map(); + eRootNotListedDep: Map = new Map(); + eRootNotListedDevDep: Map = new Map(); + eRootNotListedPeerDep: Map = new Map(); + eRootVerInvalid: Map = new Map(); + + private reportTitles: TObject = { + eLocalRMismatchRootR: 'Ranges that don\'t intersect', + eLocalRMismatchRootV: 'Ranges that don\'t include versions', + eLocalVMismatchRootR: 'Ranges that don\'t include versions', + eLocalVMismatchRootV: 'Mismatching versions', + eLocalVerInvalid: 'Invalid versions in local json', + eRootMismatchDevDep: 'Dev dependencies listed as dependencies', + eRootNotListedDep: 'Dependencies not listed in root json', + eRootNotListedDevDep: 'Dev dependencies not listed in root json', + eRootNotListedPeerDep: 'Peer dependencies not listed in root json', + eRootVerInvalid: 'Invalid versions in root json', + }; + + get hasErrors(): boolean { + return ( + Array.from(this.eLocalRMismatchRootR.values()).some((item) => item.level === 'err') || + Array.from(this.eLocalRMismatchRootV.values()).some((item) => item.level === 'err') || + Array.from(this.eLocalVMismatchRootR.values()).some((item) => item.level === 'err') || + Array.from(this.eLocalVMismatchRootV.values()).some((item) => item.level === 'err') || + Array.from(this.eLocalVerInvalid.values()).some((item) => item.level === 'err') || + Array.from(this.eRootMismatchDevDep.values()).some((item) => item.level === 'err') || + Array.from(this.eRootNotListedDep.values()).some((item) => item.level === 'err') || + Array.from(this.eRootNotListedDevDep.values()).some((item) => item.level === 'err') || + Array.from(this.eRootNotListedPeerDep.values()).some((item) => item.level === 'err') || + Array.from(this.eRootVerInvalid.values()).some((item) => item.level === 'err') + ); + } + + get hasWarnings(): boolean { + return ( + Array.from(this.eLocalRMismatchRootR.values()).some((item) => item.level === 'warn') || + Array.from(this.eLocalRMismatchRootV.values()).some((item) => item.level === 'warn') || + Array.from(this.eLocalVMismatchRootR.values()).some((item) => item.level === 'warn') || + Array.from(this.eLocalVMismatchRootV.values()).some((item) => item.level === 'warn') || + Array.from(this.eLocalVerInvalid.values()).some((item) => item.level === 'warn') || + Array.from(this.eRootMismatchDevDep.values()).some((item) => item.level === 'warn') || + Array.from(this.eRootNotListedDep.values()).some((item) => item.level === 'warn') || + Array.from(this.eRootNotListedDevDep.values()).some((item) => item.level === 'warn') || + Array.from(this.eRootNotListedPeerDep.values()).some((item) => item.level === 'warn') || + Array.from(this.eRootVerInvalid.values()).some((item) => item.level === 'warn') + ); + } + + get reportDetails(): TObject { + const details = {} as TObject; + if (this.eLocalRMismatchRootR.size > 0) { + details[this.reportTitles.eLocalRMismatchRootR] = Array.from(this.eLocalRMismatchRootR.values()).map( + (item) => item.details + ); + } + if (this.eLocalRMismatchRootV.size > 0) { + details[this.reportTitles.eLocalRMismatchRootV] = Array.from(this.eLocalRMismatchRootV.values()).map( + (item) => item.details + ); + } + if (this.eLocalVMismatchRootR.size > 0) { + details[this.reportTitles.eLocalVMismatchRootR] = Array.from(this.eLocalVMismatchRootR.values()).map( + (item) => item.details + ); + } + if (this.eLocalVMismatchRootV.size > 0) { + details[this.reportTitles.eLocalVMismatchRootV] = Array.from(this.eLocalVMismatchRootV.values()).map( + (item) => item.details + ); + } + if (this.eLocalVerInvalid.size > 0) { + details[this.reportTitles.eLocalVerInvalid] = Array.from(this.eLocalVerInvalid.values()).map((item) => item.details); + } + if (this.eRootMismatchDevDep.size > 0) { + details[this.reportTitles.eRootMismatchDevDep] = Array.from(this.eRootMismatchDevDep.values()).map( + (item) => item.details + ); + } + if (this.eRootNotListedDep.size > 0) { + details[this.reportTitles.eRootNotListedDep] = Array.from(this.eRootNotListedDep.values()).map( + (item) => item.details + ); + } + if (this.eRootNotListedDevDep.size > 0) { + details[this.reportTitles.eRootNotListedDevDep] = Array.from(this.eRootNotListedDevDep.values()).map( + (item) => item.details + ); + } + if (this.eRootNotListedPeerDep.size > 0) { + details[this.reportTitles.eRootNotListedPeerDep] = Array.from(this.eRootNotListedPeerDep.values()).map( + (item) => item.details + ); + } + if (this.eRootVerInvalid.size > 0) { + details[this.reportTitles.eRootVerInvalid] = Array.from(this.eRootVerInvalid.values()).map((item) => item.details); + } + return details; + } +} diff --git a/src/lib/models/report-deps-local.ts b/src/lib/models/report-deps-local.ts new file mode 100644 index 0000000..6e7d6eb --- /dev/null +++ b/src/lib/models/report-deps-local.ts @@ -0,0 +1,41 @@ +import { TObject } from '../types/object.types'; +import { IReportItem } from '../types/report-item.interface'; + +export class ReportDepsLocal { + eLocalNotListed: Map = new Map(); + eLocalUnused: Map = new Map(); + + private reportTitles: TObject = { + eLocalNotListed: 'Not listed as dependencies', + eLocalUnused: 'Listed in json but unused', + }; + + get hasErrors(): boolean { + return ( + Array.from(this.eLocalNotListed.values()).some((item) => item.level === 'err') || + Array.from(this.eLocalUnused.values()).some((item) => item.level === 'err') + ); + } + + get hasWarnings(): boolean { + return ( + Array.from(this.eLocalNotListed.values()).some((item) => item.level === 'warn') || + Array.from(this.eLocalUnused.values()).some((item) => item.level === 'warn') + ); + } + + get reportDetails(): TObject { + const details = {} as TObject; + if (this.eLocalNotListed.size > 0) { + details[this.reportTitles.eLocalNotListed] = Array.from(this.eLocalNotListed.values()).map( + (item) => item.details + ); + } + if (this.eLocalUnused.size > 0) { + details[this.reportTitles.eLocalUnused] = Array.from(this.eLocalUnused.values()).map( + (item) => item.details + ); + } + return details; + } +} diff --git a/src/lib/models/report-imports.ts b/src/lib/models/report-imports.ts new file mode 100644 index 0000000..3b571c3 --- /dev/null +++ b/src/lib/models/report-imports.ts @@ -0,0 +1,48 @@ +import { TObject } from '../types/object.types'; +import { IReportItem } from '../types/report-item.interface'; + +export class ReportImports { + absoluteSame: Map = new Map(); + relativeExt: Map = new Map(); + banned: Map = new Map(); + + private reportTitles: TObject = { + absoluteSame: 'Absolute imports from the same package', + relativeExt: 'Relative external imports', + banned: 'Banned imports', + }; + + get hasErrors(): boolean { + return ( + Array.from(this.absoluteSame.values()).some((item) => item.level === 'err') || + Array.from(this.relativeExt.values()).some((item) => item.level === 'err') || + Array.from(this.banned.values()).some((item) => item.level === 'err') + ); + } + + get hasWarnings(): boolean { + return ( + Array.from(this.absoluteSame.values()).some((item) => item.level === 'warn') || + Array.from(this.relativeExt.values()).some((item) => item.level === 'warn') || + Array.from(this.banned.values()).some((item) => item.level === 'warn') + ); + } + + get reportDetails(): TObject { + const details = {} as TObject; + if (this.absoluteSame.size > 0) { + details[this.reportTitles.absoluteSame] = Array.from(this.absoluteSame.values()).map( + (item) => item.details + ); + } + if (this.relativeExt.size > 0) { + details[this.reportTitles.relativeExt] = Array.from(this.relativeExt.values()).map( + (item) => item.details + ); + } + if (this.banned.size > 0) { + details[this.reportTitles.banned] = Array.from(this.banned.values()).map((item) => item.details); + } + return details; + } +} diff --git a/src/lib/types/analyze-input.interface.ts b/src/lib/types/analyze-input.interface.ts new file mode 100644 index 0000000..ee9edd1 --- /dev/null +++ b/src/lib/types/analyze-input.interface.ts @@ -0,0 +1,30 @@ +import { TIssueLevel } from './issue-level.types'; +import { TProcessDeps } from './process-dependencies.types'; +import { TProcessImports } from './process-imports.types'; +import { TProcessPkgVersion } from './process-pkg-version.types'; + +type TCheckDeps = 'local' | 'full'; + +export interface IAnalyzeInput { + rootPath: string; + packagesPaths: string[]; + // If left unset, all the files containing '... from ...' will be analyzed + matchExt?: string[]; + ignoreImports?: string[]; + bannedImports?: string[]; + checkImports?: true; + // If falsy (or left unset), no imports will be reported + treatImports?: TIssueLevel | TProcessImports; + checkDeps?: TCheckDeps; + // If falsy (or left unset), no dependencies will be reported + treatDeps?: TIssueLevel | TProcessDeps; + checkPackageVersion?: true; + // If falsy (or left unset), no packages will be reported + treatPackageVersion?: TIssueLevel | TProcessPkgVersion; + // Useful to create pre-commit analysis report + logToConsole?: true; + logStats?: true; + countHits?: true; + // Useful for strict pre-commit rules + throwError?: true; +} diff --git a/src/lib/types/base-package.interface.ts b/src/lib/types/base-package.interface.ts new file mode 100644 index 0000000..5a07be3 --- /dev/null +++ b/src/lib/types/base-package.interface.ts @@ -0,0 +1,9 @@ +import { NgPackageJson } from '../models/ng-package-json'; +import { PackageJson } from '../models/package-json'; + +export interface IBasePackage { + basePath: string; + // packageJsonPath: string | null; + packageJson: PackageJson | null; + ngPackageJson: NgPackageJson | null; +} diff --git a/src/lib/types/issue-level.types.ts b/src/lib/types/issue-level.types.ts new file mode 100644 index 0000000..8547e25 --- /dev/null +++ b/src/lib/types/issue-level.types.ts @@ -0,0 +1 @@ +export type TIssueLevel = 'err' | 'warn' | null; diff --git a/src/lib/types/object.types.ts b/src/lib/types/object.types.ts new file mode 100644 index 0000000..893f508 --- /dev/null +++ b/src/lib/types/object.types.ts @@ -0,0 +1,3 @@ +export type TObjectTypes = number | string; + +export type TObject = Record; diff --git a/src/lib/types/process-dependencies.types.ts b/src/lib/types/process-dependencies.types.ts new file mode 100644 index 0000000..6a56948 --- /dev/null +++ b/src/lib/types/process-dependencies.types.ts @@ -0,0 +1,17 @@ +import { TIssueLevel } from './issue-level.types'; + +export type TDepsIssues = + | 'eLocalNotListed' + | 'eLocalUnused' + | 'eLocalVerInvalid' + | 'eRootVerInvalid' + | 'eRootNotListedDep' + | 'eRootMismatchDevDep' + | 'eRootNotListedDevDep' + | 'eRootNotListedPeerDep' + | 'eLocalVMismatchRootV' + | 'eLocalVMismatchRootR' + | 'eLocalRMismatchRootV' + | 'eLocalRMismatchRootR'; + +export type TProcessDeps = (pkgName: string, issueType: TDepsIssues, depName: string) => TIssueLevel; diff --git a/src/lib/types/process-imports.types.ts b/src/lib/types/process-imports.types.ts new file mode 100644 index 0000000..164f176 --- /dev/null +++ b/src/lib/types/process-imports.types.ts @@ -0,0 +1,5 @@ +import { TIssueLevel } from './issue-level.types'; + +export type TImportIssues = 'absSame' | 'relExt' | 'banned'; + +export type TProcessImports = (pkgName: string, issueType: TImportIssues, importPath: string) => TIssueLevel; diff --git a/src/lib/types/process-pkg-version.types.ts b/src/lib/types/process-pkg-version.types.ts new file mode 100644 index 0000000..0d0f5eb --- /dev/null +++ b/src/lib/types/process-pkg-version.types.ts @@ -0,0 +1,3 @@ +import { TIssueLevel } from './issue-level.types'; + +export type TProcessPkgVersion = (pkgName: string) => TIssueLevel; diff --git a/src/lib/types/report-item.interface.ts b/src/lib/types/report-item.interface.ts new file mode 100644 index 0000000..d9c24db --- /dev/null +++ b/src/lib/types/report-item.interface.ts @@ -0,0 +1,6 @@ +import { TIssueLevel } from './issue-level.types'; + +export interface IReportItem { + level: TIssueLevel; + details: string; +} diff --git a/src/lib/utils/check-dependencies.ts b/src/lib/utils/check-dependencies.ts new file mode 100644 index 0000000..db7657a --- /dev/null +++ b/src/lib/utils/check-dependencies.ts @@ -0,0 +1,236 @@ +import { eq, intersects, satisfies, valid, validRange } from 'semver'; +import { Package } from '../models/package'; +import { PackageJson } from '../models/package-json'; +import { ReportDepsFull } from '../models/report-deps-full'; +import { ReportDepsLocal } from '../models/report-deps-local'; +import { TIssueLevel } from '../types/issue-level.types'; +import { TObject } from '../types/object.types'; +import { TProcessDeps } from '../types/process-dependencies.types'; +import { IReportItem } from '../types/report-item.interface'; +import { levelToIcon } from './level-to-icon'; + +export function checkLocalList(pkg: Package, issueLevel: TIssueLevel | TProcessDeps): ReportDepsLocal { + const report: ReportDepsLocal = new ReportDepsLocal(); + + const allDependencies: TObject = { + ...(pkg.packageJson.peerDependencies || {}), + ...(pkg.packageJson.devDependencies || {}), + ...(pkg.packageJson.dependencies || {}), + }; + const packageDepsForUnused: Set = new Set(Object.keys(allDependencies)); + const packageDeps: string[] = Array.from(packageDepsForUnused); + + const absoluteImports = pkg.importsModel.importsUnique.filter( + (imp) => !imp.includes('./') && !imp.startsWith(pkg.packageName) + ); + + absoluteImports.forEach((item: string) => { + const depName = packageDeps.find((dep: string) => item === dep || item.startsWith(dep + '/')); + const isSamePackage = depName ? depName.includes(pkg.packageName) : false; + if (!depName && !isSamePackage) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eLocalNotListed', item) + : issueLevel; + if (level) { + report.eLocalNotListed.set(item, itemFormat1(level, item)); + } + } else if (depName) { + packageDepsForUnused.delete(depName); + } + }); + + Array.from(packageDepsForUnused) + .filter((item: string) => item !== 'tslib') + .forEach((item: string) => { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eLocalUnused', item) + : issueLevel; + if (level) { + report.eLocalUnused.set(item, itemFormat1(level, item)); + } + }); + + return report; +} + +export function checkVersions( + pkg: Package, + rootJson: PackageJson, + issueLevel: TIssueLevel | TProcessDeps +): ReportDepsFull { + const report: ReportDepsFull = new ReportDepsFull(); + + const pkgDeps: TObject = pkg.packageJson.dependencies || {}; + const pkgDevDeps: TObject = pkg.packageJson.devDependencies || {}; + const pkgPeerDeps: TObject = pkg.packageJson.peerDependencies || {}; + + const rootDeps: TObject = rootJson.dependencies || {}; + const rootDevDeps: TObject = rootJson.devDependencies || {}; + const rootPeerDeps: TObject = rootJson.peerDependencies || {}; + + Array.from(Object.entries(pkgDeps)).forEach(([depName, depVer]) => { + if (!rootDeps[depName]) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eRootNotListedDep', depName) + : issueLevel; + if (level) { + report.eRootNotListedDep.set(depName, itemFormat2(level, depName, depVer)); + } + return; + } + validateDepVersion(pkg.packageName, depName, depVer, rootDeps[depName], report, issueLevel); + }); + + Array.from(Object.entries(pkgDevDeps)).forEach(([depName, depVer]) => { + if (!rootDevDeps[depName] && !rootDeps[depName]) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eRootNotListedDevDep', depName) + : issueLevel; + if (level) { + report.eRootNotListedDevDep.set(depName, itemFormat2(level, depName, depVer)); + } + return; + } + if (!rootDevDeps[depName] && rootDeps[depName]) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eRootMismatchDevDep', depName) + : issueLevel; + if (level) { + report.eRootMismatchDevDep.set(depName, itemFormat2(level, depName, depVer)); + } + } + validateDepVersion( + pkg.packageName, + depName, + depVer, + rootDevDeps[depName] || rootDeps[depName], + report, + issueLevel + ); + }); + + Array.from(Object.entries(pkgPeerDeps)).forEach(([depName, depVer]) => { + if (!rootPeerDeps[depName] && !rootDeps[depName]) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkg.packageName, 'eRootNotListedPeerDep', depName) + : issueLevel; + if (level) { + report.eRootNotListedPeerDep.set(depName, itemFormat2(level, depName, depVer)); + } + return; + } + validateDepVersion( + pkg.packageName, + depName, + depVer, + rootPeerDeps[depName] || rootDeps[depName], + report, + issueLevel + ); + }); + + return report; +} + +function validateDepVersion( + pkgName: string, + depName: string, + depVer: string, + rootDepVer: string, + report: ReportDepsFull, + issueLevel: TIssueLevel | TProcessDeps +) { + const [rootIsVer, rootIsRange] = [valid(rootDepVer), validRange(rootDepVer)]; + if (!rootIsVer && !rootIsRange) { + if (rootDepVer.includes('file:')) { + return; + } + const level: TIssueLevel = + typeof issueLevel === 'function' ? issueLevel(pkgName, 'eRootVerInvalid', depName) : issueLevel; + if (level) { + report.eRootVerInvalid.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + const [localIsVer, localIsRange] = [valid(depVer), validRange(depVer)]; + if (!localIsVer && !localIsRange) { + const level: TIssueLevel = + typeof issueLevel === 'function' ? issueLevel(pkgName, 'eLocalVerInvalid', depName) : issueLevel; + if (level) { + report.eLocalVerInvalid.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + if (localIsVer && rootIsVer) { + if (!eq(depVer, rootDepVer)) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkgName, 'eLocalVMismatchRootV', depName) + : issueLevel; + if (level) { + report.eLocalVMismatchRootV.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + } else if (localIsVer && rootIsRange) { + if (!satisfies(depVer, rootDepVer)) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkgName, 'eLocalVMismatchRootR', depName) + : issueLevel; + if (level) { + report.eLocalVMismatchRootR.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + } else if (localIsRange && rootIsVer) { + if (!satisfies(rootDepVer, depVer)) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkgName, 'eLocalRMismatchRootV', depName) + : issueLevel; + if (level) { + report.eLocalRMismatchRootV.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + } else { + if (!intersects(depVer, rootDepVer)) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(pkgName, 'eLocalRMismatchRootR', depName) + : issueLevel; + if (level) { + report.eLocalRMismatchRootR.set(depName, itemFormat3(level, depName, depVer, rootDepVer)); + } + return; + } + } +} + +function itemFormat1(level: TIssueLevel, item: string): IReportItem { + return { + level, + details: levelToIcon(level) + item, + }; +} + +function itemFormat2(level: TIssueLevel, depName: string, depVer: string): IReportItem { + return { + level, + details: levelToIcon(level) + `${depName}, version: "${depVer}"`, + }; +} + +function itemFormat3(level: TIssueLevel, depName: string, depVer: string, rootDepVer: string): IReportItem { + return { + level, + details: levelToIcon(level) + `${depName}, version: "${depVer}", root version: "${rootDepVer}"`, + }; +} diff --git a/src/lib/utils/check-imports.ts b/src/lib/utils/check-imports.ts new file mode 100644 index 0000000..9ffe0b8 --- /dev/null +++ b/src/lib/utils/check-imports.ts @@ -0,0 +1,128 @@ +import { dirname, resolve } from 'path'; +import { EntryPoint } from '../models/entry-point'; +import { Package } from '../models/package'; +import { ReportImports } from '../models/report-imports'; +import { TIssueLevel } from '../types/issue-level.types'; +import { TProcessImports } from '../types/process-imports.types'; +import { IReportItem } from '../types/report-item.interface'; +import { levelToIcon } from './level-to-icon'; + +export function checkImports( + pkg: Package, + bannedImports: string[], + issueLevel: TIssueLevel | TProcessImports +): ReportImports { + const report: ReportImports = new ReportImports(); + const entryPoints: EntryPoint[] = pkg.entryPointsArr; + + const secondaryPointIds = pkg.secondaryEPsArr.map((ep) => ep.moduleId); + + // const primaryBasePath = pkg.primaryEP.basePath; + // const secondaryBasePaths = pkg.secondaryEPsArr.map((ep) => ep.basePath); + + entryPoints.forEach((ep: EntryPoint) => { + checkAbsoluteSame(ep, ep.isPrimary ? secondaryPointIds : [], pkg.basePath, report, issueLevel); + checkRelativeExt(ep, pkg.basePath, report, issueLevel); + if (bannedImports.length > 0) { + checkBanned(ep, bannedImports, pkg.basePath, report, issueLevel); + } + }); + + return report; +} + +function checkAbsoluteSame( + entryPoint: EntryPoint, + secondaryPointIds: string[], + pkgPath: string, + report: ReportImports, + issueLevel: TIssueLevel | TProcessImports +): void { + Array.from(entryPoint.importsModel.importsToFiles.entries()) + .filter(([imp]) => !imp.includes('./')) + .forEach(([imp, files]) => { + if ( + imp.startsWith(entryPoint.moduleId) && + !secondaryPointIds.some((skipped) => imp.startsWith(skipped)) + ) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(entryPoint.moduleId, 'absSame', imp) + : issueLevel; + if (level) { + report.absoluteSame.set(imp, itemFormat1(level, imp, files, pkgPath)); + } + } + }); +} + +function checkRelativeExt( + entryPoint: EntryPoint, + pkgPath: string, + report: ReportImports, + issueLevel: TIssueLevel | TProcessImports +): void { + Array.from(entryPoint.importsModel.importsToFiles.entries()) + .filter(([imp]) => imp.includes('./')) + .forEach(([imp, files]) => { + if (files.some((file) => !resolve(dirname(file), imp).startsWith(entryPoint.basePath))) { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(entryPoint.moduleId, 'relExt', imp) + : issueLevel; + if (level) { + report.relativeExt.set(imp, itemFormat2(level, imp, files, pkgPath, entryPoint.basePath)); + } + } + }); +} + +function checkBanned( + entryPoint: EntryPoint, + bannedImports: string[], + pkgPath: string, + report: ReportImports, + issueLevel: TIssueLevel | TProcessImports +): void { + Array.from(entryPoint.importsModel.importsToFiles.entries()) + .filter(([imp]) => bannedImports.some((banned) => banned.includes(imp))) + .forEach(([imp, files]) => { + const level: TIssueLevel = + typeof issueLevel === 'function' + ? issueLevel(entryPoint.moduleId, 'banned', imp) + : issueLevel; + if (level) { + report.banned.set(imp, itemFormat3(level, imp, files, pkgPath)); + } + }); +} + +function itemFormat1(level: TIssueLevel, imp: string, files: string[], pkgPath: string): IReportItem { + const inFiles = '\n Located in:\n ' + files.map((file) => file.replace(pkgPath, '')).join('\n '); + return { + level, + details: levelToIcon(level) + imp + inFiles, + }; +} + +function itemFormat2( + level: TIssueLevel, + imp: string, + files: string[], + pkgPath: string, + basePath: string +): IReportItem { + const inFiles = '\n Located in:\n ' + files.map((file) => file.replace(pkgPath, '')).join('\n '); + return { + level, + details: levelToIcon(level) + imp + '\n Resolved to:' + resolve(dirname(basePath), imp) + inFiles, + }; +} + +function itemFormat3(level: TIssueLevel, imp: string, files: string[], pkgPath: string): IReportItem { + const inFiles = '\n Located in:\n ' + files.map((file) => file.replace(pkgPath, '')).join('\n '); + return { + level, + details: levelToIcon(level) + imp + inFiles, + }; +} diff --git a/src/lib/utils/check-pkg-version.ts b/src/lib/utils/check-pkg-version.ts new file mode 100644 index 0000000..d08373e --- /dev/null +++ b/src/lib/utils/check-pkg-version.ts @@ -0,0 +1,30 @@ +import { eq, valid } from 'semver'; +import { Package } from '../models/package'; +import { TIssueLevel } from '../types/issue-level.types'; +import { TProcessPkgVersion } from '../types/process-pkg-version.types'; +import { IReportItem } from '../types/report-item.interface'; + +export function checkPkgVersion( + pkg: Package, + rootVersion: string, + issueLevel: TIssueLevel | TProcessPkgVersion +): IReportItem | undefined { + const level: TIssueLevel = typeof issueLevel === 'function' ? issueLevel(pkg.packageName) : issueLevel; + if (!level) { + return undefined; + } + if (!pkg.packageJson.version) { + return { level, details: 'No package version' }; + } else { + if (!valid(pkg.packageJson.version)) { + return { level, details: `Invalid package version: "${pkg.packageJson.version}"` }; + } + if (!eq(pkg.packageJson.version, rootVersion)) { + return { + level, + details: `Package version mismatch: "${pkg.packageJson.version}", root version: "${rootVersion}"`, + }; + } + } + return undefined; +} diff --git a/src/lib/utils/count-imports-hits.ts b/src/lib/utils/count-imports-hits.ts new file mode 100644 index 0000000..43c56eb --- /dev/null +++ b/src/lib/utils/count-imports-hits.ts @@ -0,0 +1,6 @@ +export function countImportsHits(importsUnique: string[], importsMatched: string[]): [string, number][] { + return importsUnique.map((imp: string) => [ + imp, + importsMatched.filter((im: string) => im === imp).length, + ]); +} diff --git a/src/lib/utils/ensure-unix-path.ts b/src/lib/utils/ensure-unix-path.ts new file mode 100644 index 0000000..9089d88 --- /dev/null +++ b/src/lib/utils/ensure-unix-path.ts @@ -0,0 +1,21 @@ +import { posix, win32 } from 'path'; + +const pathRegExp = new RegExp('\\' + win32.sep, 'g'); +const isWin32: boolean = process.platform === 'win32'; + +const ensureUnixPathCache: Map = new Map(); + +export function ensureUnixPath(path: string): string { + if (!isWin32) { + return path; + } + const cachedPath = ensureUnixPathCache.get(path); + if (cachedPath) { + return cachedPath; + } + // we use a regex instead of the character literal due to a bug in some versions of node.js + // the path separator needs to be preceded by an escape character + const normalizedPath = path.replace(pathRegExp, posix.sep); + ensureUnixPathCache.set(path, normalizedPath); + return normalizedPath; +} diff --git a/src/lib/utils/get-absolute-path.ts b/src/lib/utils/get-absolute-path.ts new file mode 100644 index 0000000..a619ac0 --- /dev/null +++ b/src/lib/utils/get-absolute-path.ts @@ -0,0 +1,5 @@ +import { isAbsolute, normalize, resolve } from 'path'; + +export function getAbsolutePath(path: string): string { + return isAbsolute(path) ? normalize(path) : resolve(path); +} diff --git a/src/lib/utils/get-files-recursive.ts b/src/lib/utils/get-files-recursive.ts new file mode 100644 index 0000000..ebd548a --- /dev/null +++ b/src/lib/utils/get-files-recursive.ts @@ -0,0 +1,31 @@ +import { Dirent, readdirSync } from 'fs'; +import { resolve } from 'path'; + +// export function getFilesRecursive(dirPath: string): string[] { +// const entries = readdirSync(dirPath, { withFileTypes: true }); +// return entries +// .map((entry: Dirent) => { +// const res = resolve(dirPath, entry.name); +// return entry.isDirectory() ? getFilesRecursive(res) : res; +// }) +// .flat() +// .sort(); +// } + +export function getFilesRecursive(dirPath: string): Set { + const filesSet: Set = new Set(); + getRecursive(dirPath, filesSet); + return filesSet; +} + +function getRecursive(dirPath: string, filesSet: Set) { + const entries = readdirSync(dirPath, { withFileTypes: true }); + entries.forEach((entry: Dirent) => { + const res = resolve(dirPath, entry.name); + if (entry.isDirectory()) { + getRecursive(res, filesSet); + } else { + filesSet.add(res); + } + }); +} diff --git a/src/lib/utils/get-imports-from-files.ts b/src/lib/utils/get-imports-from-files.ts new file mode 100644 index 0000000..b3224f8 --- /dev/null +++ b/src/lib/utils/get-imports-from-files.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'fs'; +import { extname } from 'path'; + +import { ImportsModel } from '../models/imports-model'; + +export function getImportsFromFiles( + packageFiles: string[], + matchExtensions: string[], + ignoreImports: string[] +): ImportsModel { + const model = new ImportsModel(); + + model.filesMatched = + matchExtensions.length > 0 + ? packageFiles.filter((file: string) => matchExtensions.includes(extname(file))) + : [...packageFiles]; + + const filesToImportsRaw = new Map(); + model.filesMatched.forEach((file: string) => { + const contents = readFileSync(file, 'utf8'); + const fileLines = contents.split(/\r?\n/).filter((line: string) => line.includes('from')); + const imports = fileLines + .map((line: string) => { + const match = line.match(/\s+?from\s+?['"](.+?)['"]/); + return match && match.length > 1 ? match[1] : ''; + }) + .filter(Boolean); + filesToImportsRaw.set(file, imports); + }); + model.importsTotal = Array.from(filesToImportsRaw.values()).flat().length; + + const filesToImports = ignoreImports.length > 0 ? new Map([]) : filesToImportsRaw; + if (ignoreImports.length > 0) { + Array.from(filesToImportsRaw.entries()).forEach(([file, imports]: [string, string[]]) => { + const matched = imports.filter( + (line: string) => !ignoreImports.some((ignored: string) => line.includes(ignored)) + ); + if (matched.length > 0) { + filesToImports.set(file, matched); + } + }); + } + model.filesToImports = filesToImports; + model.importsMatched = Array.from(filesToImports.values()).flat(); + model.importsUnique = [...new Set(model.importsMatched)].sort(); + + return model; +} diff --git a/src/lib/utils/get-imports-model-ep.ts b/src/lib/utils/get-imports-model-ep.ts new file mode 100644 index 0000000..ac914a7 --- /dev/null +++ b/src/lib/utils/get-imports-model-ep.ts @@ -0,0 +1,17 @@ +import { ImportsModel } from '../models/imports-model'; +import { ImportsModelEp } from '../models/imports-model-ep'; + +export function getImportsModelEp( + pkgImportsModel: ImportsModel, + basePath: string, + excludePaths: string[] +): ImportsModelEp { + const model = new ImportsModelEp(); + const matchedFiles: string[] = pkgImportsModel.filesMatched.filter((file) => file.startsWith(basePath)); + model.files = + excludePaths.length > 0 + ? matchedFiles.filter((file) => !excludePaths.some((path) => file.startsWith(path))) + : matchedFiles; + model.importsToFiles = pkgImportsModel.mapImportsToFiles(model.files); + return model; +} diff --git a/src/lib/utils/get-secondary-points.ts b/src/lib/utils/get-secondary-points.ts new file mode 100644 index 0000000..927eb99 --- /dev/null +++ b/src/lib/utils/get-secondary-points.ts @@ -0,0 +1,12 @@ +import { basename, dirname } from 'path'; + +export function getSecondaryPoints(basePath: string, packageFiles: string[]): string[] { + const directories: string[] = packageFiles + .filter((filePath) => { + const baseName = basename(filePath); + return baseName === 'package.json' || baseName === 'ng-package.json'; + }) + .map((filePath) => dirname(filePath)) + .filter((dirPath) => dirPath !== basePath); + return Array.from(new Set(directories)).sort(); +} diff --git a/src/lib/utils/level-to-icon.ts b/src/lib/utils/level-to-icon.ts new file mode 100644 index 0000000..e17dee7 --- /dev/null +++ b/src/lib/utils/level-to-icon.ts @@ -0,0 +1,3 @@ +import { TIssueLevel } from '../types/issue-level.types'; + +export const levelToIcon = (level: TIssueLevel): string => (level ? (level === 'err' ? '❌ ' : '⚠️ ') : ''); diff --git a/src/lib/utils/read-json-file.ts b/src/lib/utils/read-json-file.ts new file mode 100644 index 0000000..e73b3bf --- /dev/null +++ b/src/lib/utils/read-json-file.ts @@ -0,0 +1,20 @@ +import { existsSync, readFileSync } from 'fs'; +import { NgPackageJson } from '../models/ng-package-json'; + +import { PackageJson } from '../models/package-json'; +import { TObject } from '../types/object.types'; + +export function getPackageJson(path: string): PackageJson | null { + const file: TObject | null = readJsonFile(path); + return file ? new PackageJson(file) : null; +} + +export function getNgPackageJson(path: string): NgPackageJson | null { + const file: TObject | null = readJsonFile(path); + return file ? new NgPackageJson(file) : null; +} + +export function readJsonFile(path: string): TObject | null { + const file: string | null = existsSync(path) ? readFileSync(path, 'utf8') : null; + return file ? JSON.parse(file) : null; +} diff --git a/src/lib/utils/resolve-base-package.ts b/src/lib/utils/resolve-base-package.ts new file mode 100644 index 0000000..ff5d405 --- /dev/null +++ b/src/lib/utils/resolve-base-package.ts @@ -0,0 +1,38 @@ +import { Stats, statSync } from 'fs'; +import { dirname, join } from 'path'; + +import { PackageJson } from '../models/package-json'; +import { IBasePackage } from '../types/base-package.interface'; +import { getAbsolutePath } from './get-absolute-path'; +import { getNgPackageJson, getPackageJson } from './read-json-file'; + +export function resolveBasePackage( + inputPath: string, + isSecondary = false, + ignoreNgPackage = false +): IBasePackage { + const fullPath: string = getAbsolutePath(inputPath); + const pathStats: Stats = statSync(fullPath); + const basePath: string = pathStats.isDirectory() ? fullPath : dirname(fullPath); + const packageJsonPath: string = join(basePath, 'package.json'); + const packageJson: PackageJson | null = getPackageJson(packageJsonPath); + + if (!packageJson && !isSecondary) { + throw new Error(`Cannot discover package sources at ${inputPath} as 'package.json' was not found.`); + } + + const ngPackageJson = + packageJson?.ngPackageJson || + (pathStats.isDirectory() ? getNgPackageJson(join(basePath, 'ng-package.json')) : null); + + if (!ngPackageJson && !ignoreNgPackage) { + throw new Error(`Cannot discover package sources at ${inputPath}`); + } + + return { + basePath, + // packageJsonPath: packageJson ? packageJsonPath : null, + packageJson: packageJson || null, + ngPackageJson: ngPackageJson, + }; +} diff --git a/src/lib/utils/resolve-file-path.ts b/src/lib/utils/resolve-file-path.ts new file mode 100644 index 0000000..6a4489e --- /dev/null +++ b/src/lib/utils/resolve-file-path.ts @@ -0,0 +1,10 @@ +import { isAbsolute, join, normalize } from 'path'; + +export function resolveFilePath(filePath: string, fileDir?: string): string | null { + const normalizedFilePath: string = normalize(filePath); + return isAbsolute(normalizedFilePath) + ? normalizedFilePath + : fileDir + ? join(normalize(fileDir), normalizedFilePath) + : null; +} diff --git a/src/lib/utils/resolve-package.ts b/src/lib/utils/resolve-package.ts new file mode 100644 index 0000000..e910461 --- /dev/null +++ b/src/lib/utils/resolve-package.ts @@ -0,0 +1,45 @@ +import { relative } from 'path'; + +import { EntryPoint } from '../models/entry-point'; +import { Package } from '../models/package'; +import { IBasePackage } from '../types/base-package.interface'; +import { ensureUnixPath } from './ensure-unix-path'; +import { getAbsolutePath } from './get-absolute-path'; +import { getFilesRecursive } from './get-files-recursive'; +import { getSecondaryPoints } from './get-secondary-points'; +import { resolveBasePackage } from './resolve-base-package'; + +export function resolvePackage(inputPath: string): Package { + const fullPath: string = getAbsolutePath(inputPath); + + const primaryPackage: IBasePackage = resolveBasePackage(fullPath); + const primaryEP: EntryPoint = new EntryPoint({ + basePath: primaryPackage.basePath, + moduleId: primaryPackage.packageJson?.name || '', + isPrimary: true, + ngPackageJson: primaryPackage.ngPackageJson, + }); + + const pkg: Package = new Package({ + packageName: primaryEP.moduleId, + basePath: primaryPackage.basePath, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + packageJson: primaryPackage.packageJson!, + packageFiles: getFilesRecursive(primaryEP.basePath), + }); + pkg.primaryEntryPoint = primaryEP; + + const secondaryPaths: string[] = getSecondaryPoints(primaryEP.basePath, pkg.packageFilesArr); + secondaryPaths.forEach((secondaryPath) => { + const basePackage: IBasePackage = resolveBasePackage(secondaryPath, true); + const relativeSourcePath = relative(primaryEP.basePath, basePackage.basePath); + pkg.secondaryEntryPoint = new EntryPoint({ + basePath: basePackage.basePath, + moduleId: ensureUnixPath(`${primaryEP.moduleId}/${relativeSourcePath}`), + isPrimary: false, + ngPackageJson: basePackage.ngPackageJson, + }); + }); + + return pkg; +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index fbd9dfc..0000000 --- a/src/types.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * @typedef IPackageInput - * @type {object} - */ -export interface IPackageInput { - /** - * @type {string} - Package name - */ - name: string; - /** - * @type {string} - Absolute package path - */ - path: string; - /** - * @type {string} - Absolute path to package.json file - * If left unset, it must be available at 'path/package.json' - */ - packageJson?: string; -} - -export interface IPackage extends IPackageInput { - packageJson: string; - jsonData: IPackageJsonData; - imports: IPackageImports; - importsReport: TImportsReports; - versionReport: string; - time: number; - hasErrors?: true; - hasWarnings?: true; -} - -export interface IPackageImports { - filesTotal: number; - filesMatched: number; - importsTotal: number; - importsMatched: string[]; - importsUnique: string[]; - matchedMap: Map; - packageJsonFiles: string[]; - subpackages: { - names: string[]; - paths: string[]; - nameToPath: Map; - pathToName: Map; - }; -} - -export const packageImportsDefault: IPackageImports = { - filesTotal: 0, - filesMatched: 0, - importsTotal: 0, - importsMatched: [], - importsUnique: [], - matchedMap: new Map(), - packageJsonFiles: [], - subpackages: { - names: [], - paths: [], - nameToPath: new Map(), - pathToName: new Map(), - }, -}; - -/** - * @typedef IAnalyzeInput - * @type {object} - */ -export interface IAnalyzeInput { - /** - * @type {IPackageInput[]} - List of packages to analyze - */ - packages: ReadonlyArray; - /** - * @type {string[]} - File extensions to analyze - * If left unset, all the files containing 'import {...} from' will be analyzed - */ - matchExt?: string[]; - /** - * @type {string[]} - Imports to exclude from the analysis - */ - ignoreImports?: string[]; - /** - * @type {string[]} - Imports to be reported as banned - */ - bannedImports?: string[]; - /** - * @type {string} - An absolute path to the root project package.json file - * Required if checkDeps of checkPackageVersion is passed - */ - packageJson?: string; - /** - * @type {boolean} - Count import hits; useful for statistic - */ - countHits?: true; - /** - * @type {boolean} - Check and report imports - */ - checkImports?: true; - /** - * @type {(TTreatTypes|TTreatCallbackImport)} - Report level for failed import checks - * If left unset or null, no imports will be reported - */ - treatImports?: TTreatTypes | TTreatCallbackImport; - /** - * @type {TCheckDepsTypes} - Check and report dependencies - * Requires packageJson to be provided - */ - checkDeps?: TCheckDepsTypes; - /** - * @type {(TTreatTypes|TTreatCallbackDep)} - Report level for failed dependency checks - * If left unset or null, no dependencies will be reported - */ - treatDeps?: TTreatTypes | TTreatCallbackDep; - /** - * @type {boolean} - Check and report package versions - * Requires packageJson to be provided - */ - checkPackageVersion?: true; - /** - * @type {(TTreatTypes|TTreatCallbackVersion)} - Report level for failed version checks - * If left unset or null, no packages will be reported - */ - treatPackageVersion?: TTreatTypes | TTreatCallbackVersion; - /** - * @type {boolean} - Log report to console - * Useful to create pre-commit analysis report - */ - logToConsole?: true; - /** - * @type {boolean} - Throw an error after the analysis if any errors occurred - * Useful for strict pre-commit rules - */ - throwError?: true; -} - -export interface IPackageJsonData { - version?: string; - dependencies?: { - [name: string]: string; - }; - devDependencies?: { - [name: string]: string; - }; - peerDependencies?: { - [name: string]: string; - }; - ngPackage?: unknown; -} - -export type IObjectTypes = number | string; - -export interface IObject { - [key: string]: T; -} - -export type TImportsReport = Map; -export type TImportsReports = Map; - -/** - * @callback TTreatCallbackImport - * @param {string} pkgName - The package name in which an issue occurred - * @param {TImportTypes} importType - Issue type - * @param {string} importPath - Import that triggered an issue - * @returns {TTreatTypes} - */ -export type TTreatCallbackImport = ( - pkgName: string, - importType: TImportTypes, - importPath: string -) => TTreatTypes; - -/** - * @callback TTreatCallbackDep - * @param {string} pkgName - The package name in which an issue occurred - * @param {TDepsTypes} depType - Issue type - * @param {string} depName - Dependency name that triggered an issue - * @returns {TTreatTypes} - */ -export type TTreatCallbackDep = (pkgName: string, depType: TDepsTypes, depName: string) => TTreatTypes; - -/** - * @callback TTreatCallbackVersion - * @param {string} pkgName - The package name in which an issue occurred - * @returns {TTreatTypes} - */ -export type TTreatCallbackVersion = (pkgName: string) => TTreatTypes; - -/** - * @typedef TTreatTypes - * @type {(string|null)} - * Report levels: - * err - error, - * warn - warning, - * null - none - */ -export type TTreatTypes = 'err' | 'warn' | null; - -/** - * @typedef TImportTypes - * @type {string} - * Import issue types: - * absSame - absolute import from the same package, - * relExt - relative import from an external package, - * banned - banned import - */ -type TImportTypes = 'absSame' | 'relExt' | 'banned'; - -/** - * @typedef TCheckDepsTypes - * Dependency check types: - * local - check only local (per-package) package.json files, - * full - check local & root package.json files for issues - */ -export type TCheckDepsTypes = 'local' | 'full'; - -/** - * @typedef TDepsTypes - * @type {string} - * Dependency issue types: - * local - local package issue, - * root - root package.json issue - */ -type TDepsTypes = 'local' | 'root'; diff --git a/src/utils/check-deps.ts b/src/utils/check-deps.ts deleted file mode 100644 index c2556af..0000000 --- a/src/utils/check-deps.ts +++ /dev/null @@ -1,55 +0,0 @@ -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 pkgDeps: { [name: string]: string } = pkg.jsonData.dependencies || {}; - const pkgDevDeps: { [name: string]: string } = pkg.jsonData.devDependencies || {}; - const pkgPeerDeps: { [name: string]: string } = pkg.jsonData.peerDependencies || {}; - - const allDependencies: { [name: string]: string } = { ...pkgDeps, ...pkgDevDeps, ...pkgPeerDeps }; - const packageDepsForUnused: Set = new Set(Object.keys(allDependencies)); - const packageDeps: string[] = Array.from(packageDepsForUnused); - - const absoluteImports: string[] = pkg.imports.importsUnique.filter( - (item: string) => !item.includes('./') - ); - - const tempReport = new Map([]); - absoluteImports.forEach((item: string) => { - const depName = packageDeps.find((dep: string) => item === dep || item.startsWith(dep + '/')); - const subPackage = pkg.imports.subpackages.names.find((name: string) => item.includes(name)); - if (!depName && !subPackage) { - 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 }); - } else if (depName) { - packageDepsForUnused.delete(depName); - } - }); - - Array.from(packageDepsForUnused) - .filter((item: string) => item !== 'tslib') - .forEach((item: string) => { - const treat: TTreatTypes = - typeof treatAs === 'function' ? treatAs(pkg.name, 'local', item) : treatAs; - tempReport.set(item, { - data: getTreatIcon(treat) + 'Listed in local package.json, unused', - 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 deleted file mode 100644 index 21f09b9..0000000 --- a/src/utils/check-imports.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { dirname, resolve } from 'path'; - -import { IObjectTypes, IPackage, TImportsReport, TTreatCallbackImport, TTreatTypes } from '../types'; -import { getTreatIcon } from './get-treat-icon'; - -export function checkImports( - pkg: IPackage, - packages: IPackage[], - bannedImports: string[], - 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)) - .filter((item: string) => { - if (pkg.imports.subpackages.paths.length === 0) { - return true; - } - - const subName = pkg.imports.subpackages.names.find((name) => item.startsWith(name)); - if (!subName) { - return false; - } - const subPath = pkg.imports.subpackages.nameToPath.get(subName); - const filesWithImport = pkg.imports.matchedMap.get(item) || []; - if (!subPath) { - return false; - } - return filesWithImport.some((filePath: string) => filePath.startsWith(subPath)); - }); - - 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 filesWithImport = pkg.imports.matchedMap.get(item) || []; - filesWithImport.forEach((file: string) => { - const importFrom: string = resolve(dirname(file), item); - const otherSubpackages: string[] = pkg.imports.subpackages.paths.filter( - (subPath: string) => !file.includes(subPath) - ); - if ( - [...otherPackages, ...otherSubpackages].some((otherPkg: string) => - importFrom.includes(otherPkg) - ) - ) { - const treat: TTreatTypes = - typeof treatAs === 'function' ? treatAs(pkg.name, 'relExt', item) : treatAs; - tempReport.set(item, { data: getTreatIcon(treat) + 'External relative import', treat }); - } - }); - }); - } - - if (bannedImports.length > 0) { - pkg.imports.importsUnique.forEach((item: string) => { - if (bannedImports.some((banned: string) => item.includes(banned))) { - const treat: TTreatTypes = - typeof treatAs === 'function' ? treatAs(pkg.name, 'banned', item) : treatAs; - tempReport.set(item, { data: getTreatIcon(treat) + 'Banned 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 deleted file mode 100644 index a63ce4a..0000000 --- a/src/utils/check-package-version.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index ad0135b..0000000 --- a/src/utils/count-import-hits.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 1223b38..0000000 --- a/src/utils/get-files.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 09f02b9..0000000 --- a/src/utils/get-imports.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { readFileSync } from 'fs'; -import { basename, dirname, extname } from 'path'; - -import { IPackage, IPackageImports } from '../types'; -import { getFiles } from './get-files'; -import { getPackageJson } from './get-package-json'; - -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; - - /** file, imports */ - const allImports = new Map([]); - matchedFiles.forEach((file: string) => { - const contents = readFileSync(file, 'utf8'); - const fileLines = contents.split(/\r?\n/).filter((line: string) => line.includes(' from \'')); - const imports = fileLines - .map((line: string) => { - const match = line.match(/from '(.+?)'/); - return match && match.length > 1 ? match[1] : ''; - }) - .filter(Boolean); - allImports.set(file, imports); - }); - - /** file, imports */ - const matchedRev = - ignoreImports.length > 0 ? new Map([]) : new Map(allImports.entries()); - if (ignoreImports.length > 0) { - Array.from(allImports.entries()).forEach(([file, imports]: [string, string[]]) => { - const matched = imports.filter((line: string) => - ignoreImports.some((ignored: string) => line.includes(ignored)) - ); - if (matched.length > 0) { - matchedRev.set(file, matched); - } - }); - } - - const matchedImports = Array.from(matchedRev.values()).flat(); - const uniqueImports = [...new Set(matchedImports)].sort(); - - const revEntries = Array.from(matchedRev.entries()); - /** import, files */ - const matchedMap = new Map([]); - uniqueImports.forEach((item: string) => { - const files = revEntries - .filter(([, imports]: [string, string[]]) => imports.includes(item)) - .map(([file]: [string, string[]]) => file); - matchedMap.set(item, files); - }); - - const packageJsonFiles = allFiles.filter((file: string) => basename(file) === 'package.json'); - const subNames: string[] = []; - const nameToPath = new Map([]); - const pathToName = new Map([]); - const subPaths: string[] = packageJsonFiles - .filter((path: string) => Boolean(getPackageJson(path).ngPackage)) - .map((path: string) => dirname(path)); - subPaths.forEach((path: string) => { - const subName = pkg.name + path.replace(pkg.path, ''); - subNames.push(subName); - nameToPath.set(subName, path); - pathToName.set(path, subName); - }); - - return { - filesTotal: allFiles.length, - filesMatched: matchedFiles.length, - importsTotal: Array.from(allImports.values()).flat().length, - importsMatched: matchedImports, - importsUnique: uniqueImports, - matchedMap: matchedMap, - packageJsonFiles: allFiles.filter((file: string) => basename(file) === 'package.json'), - subpackages: { - names: subNames, - paths: subPaths, - nameToPath, - pathToName, - }, - }; -} diff --git a/src/utils/get-package-json.ts b/src/utils/get-package-json.ts deleted file mode 100644 index c9fc17b..0000000 --- a/src/utils/get-package-json.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 9a3d1a8..0000000 --- a/src/utils/get-treat-icon.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TTreatTypes } from '../types'; - -export function getTreatIcon(treat: TTreatTypes): string { - return treat ? (treat === 'err' ? '❌ ' : '⚠️ ') : ''; -} diff --git a/tests/dummy-project/dummy/sub1/package.json b/tests/dummy-project/dummy/sub1/package.json index c9626c7..430bd2f 100644 --- a/tests/dummy-project/dummy/sub1/package.json +++ b/tests/dummy-project/dummy/sub1/package.json @@ -3,5 +3,10 @@ "version": "1.0.0", "peerDependencies": { "c": "*" + }, + "ngPackage": { + "lib": { + "entryFile": "public_api.ts" + } } } diff --git a/tests/dummy-project/dummy/sub2/package.json b/tests/dummy-project/dummy/sub2/package.json index 681c69e..93fb875 100644 --- a/tests/dummy-project/dummy/sub2/package.json +++ b/tests/dummy-project/dummy/sub2/package.json @@ -1,4 +1,9 @@ { "name": "@dummy/sub2", - "version": "1.0.0" + "version": "1.0.0", + "ngPackage": { + "lib": { + "entryFile": "public_api.ts" + } + } } diff --git a/tests/index.spec.ts b/tests/index.spec.ts index 592954f..3d85c29 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -3,26 +3,24 @@ import { readFileSync } from 'fs'; import 'mocha'; import { resolve } from 'path'; -import { analyze, IPackageInput } from '../src'; +import { analyze } 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 packagesPaths = projectsList.map((project: string) => resolve(__dirname, './dummy-project/' + project)); const analysis = analyze({ - packages, + rootPath: resolve(__dirname, './dummy-project'), + packagesPaths, matchExt: ['.ts'], - packageJson: resolve(__dirname, './dummy-project/package.json'), - countHits: true, checkImports: true, checkDeps: 'local', checkPackageVersion: true, logToConsole: true, + logStats: true, + countHits: true, // throwError: true, }); From eca027492a8fb6292ea33b0b85d9ca96ad2ea516 Mon Sep 17 00:00:00 2001 From: Michael Mullins Date: Sun, 3 Oct 2021 10:15:20 +0400 Subject: [PATCH 2/2] chore: bump v1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6915d27..910711c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@public-js/ng-pkg-keeper", - "version": "1.0.0-beta.3", + "version": "1.0.0", "description": "Add description", "scripts": { "build": "npm run clean:dist && tsc",