diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ebd33c2..0f2862a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -39,7 +39,7 @@ jobs: run: npm run build - name: Test & coverage - run: npm run cover:ng-catcher + run: npm run cover:mocha - name: Report to Codacy uses: codacy/codacy-coverage-reporter-action@master with: diff --git a/package.json b/package.json index 9d0a977..7fe239b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@public-js/ng-pkg-keeper", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "description": "Add description", "scripts": { "build": "npm run clean:dist && tsc", diff --git a/src/analyze.ts b/src/analyze.ts index 8d06064..d529a80 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -138,9 +138,9 @@ export function analyze(params: IAnalyzeInput): IPackage[] { } } else if (packages.some((pkg: IPackage) => pkg.hasWarnings)) { if (params.logToConsole) { - console.error('Warnings found. See the report above.'); + console.warn('Warnings found. See the report above.'); } else { - console.error('Warnings found. To see the report pass \'logToConsole\' to parameters.'); + console.warn('Warnings found. To see the report pass \'logToConsole\' to parameters.'); } } @@ -197,7 +197,7 @@ function packageReportLog(pkg: IPackage, params: IAnalyzeInput, timeStart: numbe console.group(); console.table(packageReport); - if (pkg.importsReport.size > 0) { + if (Object.keys(importsReport).length > 0) { console.table(importsReport); } console.groupEnd(); diff --git a/src/types.ts b/src/types.ts index 39edb5e..a85edab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,14 @@ export interface IPackageImports { importsTotal: number; importsMatched: string[]; importsUnique: string[]; + matchedMap: Map; + packageJsonFiles: string[]; + subpackages: { + names: string[]; + paths: string[]; + nameToPath: Map; + pathToName: Map; + }; } export const packageImportsDefault: IPackageImports = { @@ -29,6 +37,14 @@ export const packageImportsDefault: IPackageImports = { importsTotal: 0, importsMatched: [], importsUnique: [], + matchedMap: new Map(), + packageJsonFiles: [], + subpackages: { + names: [], + paths: [], + nameToPath: new Map(), + pathToName: new Map(), + }, }; export interface IAnalyzeInput { @@ -58,6 +74,7 @@ export interface IPackageJsonData { peerDependencies?: { [name: string]: string; }; + ngPackage?: unknown; } export type IObjectTypes = number | string; diff --git a/src/utils/check-deps.ts b/src/utils/check-deps.ts index 5cb875b..e3febd7 100644 --- a/src/utils/check-deps.ts +++ b/src/utils/check-deps.ts @@ -5,12 +5,13 @@ 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 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('./') @@ -18,13 +19,25 @@ export function checkLocal( const tempReport = new Map([]); absoluteImports.forEach((item: string) => { - if (!packageDeps.some((dep: string) => item.startsWith(dep))) { + 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 }]) => { diff --git a/src/utils/check-imports.ts b/src/utils/check-imports.ts index a95a990..944be42 100644 --- a/src/utils/check-imports.ts +++ b/src/utils/check-imports.ts @@ -1,4 +1,4 @@ -import { resolve } from 'path'; +import { dirname, resolve } from 'path'; import { IObjectTypes, IPackage, TImportsReport, TTreatCallbackImport, TTreatTypes } from '../types'; import { getTreatIcon } from './get-treat-icon'; @@ -10,9 +10,24 @@ export function checkImports( ): { report: TImportsReport; hasErrors: boolean; hasWarnings: boolean } { const tempReport = new Map([]); - const selfImports: string[] = pkg.imports.importsUnique.filter((item: string) => - item.startsWith(pkg.name) - ); + 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 = @@ -30,12 +45,22 @@ export function checkImports( .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 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 }); + } + }); }); } diff --git a/src/utils/get-imports.ts b/src/utils/get-imports.ts index 479adf5..09f02b9 100644 --- a/src/utils/get-imports.ts +++ b/src/utils/get-imports.ts @@ -1,41 +1,83 @@ import { readFileSync } from 'fs'; -import { extname } from 'path'; +import { basename, dirname, extname } from 'path'; -import { getFiles } from './get-files'; 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; - 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) => + /** 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)) - ) - : allImports; + ); + 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: allImports.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, + }, }; }