x-fidelity 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.czrc +3 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +34 -0
- package/.releaserc +11 -0
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/commitlint.config.js +3 -0
- package/dist/core/cli.js +37 -0
- package/dist/core/engine.js +67 -0
- package/dist/core/engine.test.js +79 -0
- package/dist/facts/repoDependencyFacts.js +105 -0
- package/dist/facts/repoDependencyFacts.test.js +149 -0
- package/dist/facts/repoFilesystemFacts.js +91 -0
- package/dist/index.js +35 -0
- package/dist/operators/currentDependencies.js +58 -0
- package/dist/operators/currentDependencies.test.js +43 -0
- package/dist/operators/directoryStructureMatches.js +63 -0
- package/dist/operators/directoryStructureMatches.test.js +61 -0
- package/dist/operators/fileContains.js +17 -0
- package/dist/operators/fileContains.test.js +25 -0
- package/dist/operators/index.js +11 -0
- package/dist/rules/index.js +61 -0
- package/dist/rules/noDatabases-rule.json +21 -0
- package/dist/rules/standardDirectoryStructure-rule.json +25 -0
- package/dist/rules/supportedCoreDependencies-rule.json +29 -0
- package/dist/typeDefs.js +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/xfidelity +35 -0
- package/jest.config.js +6 -0
- package/package.json +69 -0
- package/src/core/cli.ts +42 -0
- package/src/core/engine.test.ts +82 -0
- package/src/core/engine.ts +68 -0
- package/src/facts/repoDependencyFacts.test.ts +166 -0
- package/src/facts/repoDependencyFacts.ts +88 -0
- package/src/facts/repoFilesystemFacts.ts +80 -0
- package/src/index.ts +25 -0
- package/src/operators/currentDependencies.test.ts +45 -0
- package/src/operators/currentDependencies.ts +40 -0
- package/src/operators/directoryStructureMatches.test.ts +47 -0
- package/src/operators/directoryStructureMatches.ts +43 -0
- package/src/operators/fileContains.test.ts +27 -0
- package/src/operators/fileContains.ts +19 -0
- package/src/operators/index.ts +12 -0
- package/src/rules/index.ts +29 -0
- package/src/rules/noDatabases-rule.json +21 -0
- package/src/rules/standardDirectoryStructure-rule.json +25 -0
- package/src/rules/supportedCoreDependencies-rule.json +29 -0
- package/src/typeDefs.ts +40 -0
- package/src/utils/logger.ts +20 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x-fidelity",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "cli for opinionated framework adherence checks",
|
|
5
|
+
"main": "dist/xfidelity",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xfidelity": "./dist/xfidelity"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rimraf dist/ && tsc && yarn copy-files",
|
|
11
|
+
"copy-files": "cp src/rules/*.json dist/rules/ && cp dist/index.js dist/xfidelity",
|
|
12
|
+
"start": "jest --watch",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"commit": "git-cz",
|
|
15
|
+
"release": "semantic-release",
|
|
16
|
+
"test-bin-install": "yarn build && yarn global bin && yarn global add file:$PWD"
|
|
17
|
+
},
|
|
18
|
+
"repository": "git@github.com:zotoio/x-fidelity.git",
|
|
19
|
+
"author": "wyvern8 <io@zoto.io>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0",
|
|
23
|
+
"yarn": ">=1.22.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@jest/globals": "^29.7.0",
|
|
27
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
28
|
+
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
29
|
+
"@semantic-release/git": "^10.0.1",
|
|
30
|
+
"@semantic-release/github": "^10.0.6",
|
|
31
|
+
"@semantic-release/npm": "^12.0.1",
|
|
32
|
+
"@semantic-release/release-notes-generator": "^14.0.0",
|
|
33
|
+
"@types/jest": "^29.5.12",
|
|
34
|
+
"@types/lodash": "^4.17.4",
|
|
35
|
+
"@types/node": "^20.14.2",
|
|
36
|
+
"@types/parse-json": "^4.0.2",
|
|
37
|
+
"@types/semver": "^7.5.8",
|
|
38
|
+
"@types/winston": "^2.4.4",
|
|
39
|
+
"commitizen": "^4.3.0",
|
|
40
|
+
"conventional-changelog-cli": "^5.0.0",
|
|
41
|
+
"conventional-recommended-bump": "^10.0.0",
|
|
42
|
+
"cz-conventional-changelog": "^3.3.0",
|
|
43
|
+
"jest": "^29.7.0",
|
|
44
|
+
"rimraf": "^5.0.7",
|
|
45
|
+
"semantic-release": "^24.0.0",
|
|
46
|
+
"ts-jest": "^29.1.4",
|
|
47
|
+
"ts-node": "^10.4.0",
|
|
48
|
+
"typescript": "^5.4.5"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@yarnpkg/lockfile": "^1.1.0",
|
|
52
|
+
"axios": "^1.7.2",
|
|
53
|
+
"commander": "^12.1.0",
|
|
54
|
+
"dotenv": "^16.4.5",
|
|
55
|
+
"format-json": "^1.0.3",
|
|
56
|
+
"json-rules-engine": "^6.5.0",
|
|
57
|
+
"lodash": "^4.17.21",
|
|
58
|
+
"nodemon": "^3.0.2",
|
|
59
|
+
"semver": "^7.6.2",
|
|
60
|
+
"winston": "^3.13.0",
|
|
61
|
+
"yarn": "^1.22.21"
|
|
62
|
+
},
|
|
63
|
+
"config": {
|
|
64
|
+
"commitizen": {
|
|
65
|
+
"path": "./node_modules/cz-conventional-changelog"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
69
|
+
}
|
package/src/core/cli.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
|
|
4
|
+
const banner = (`
|
|
5
|
+
=====================================
|
|
6
|
+
__ __ ________ ______
|
|
7
|
+
| ## | ## | ######## \\######
|
|
8
|
+
\\##\\/ ## ______ | ##__ | ##
|
|
9
|
+
>## ## | \\| ## \\ | ##
|
|
10
|
+
/ ####\\ \\######| ###### | ##
|
|
11
|
+
| ## \\##\\ | ## _| ##_
|
|
12
|
+
| ## | ## | ## | ## \\
|
|
13
|
+
\\## \\## \\## \\######
|
|
14
|
+
|
|
15
|
+
-------------------------------------
|
|
16
|
+
${new Date().toString()}`);
|
|
17
|
+
|
|
18
|
+
console.log(banner);
|
|
19
|
+
logger.info([banner]);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.option("-d, --dir <directory>", "The repo checkout directory")
|
|
23
|
+
.option("-c, --configUrl <url>", "The URL used to fetch config");
|
|
24
|
+
|
|
25
|
+
program.parse();
|
|
26
|
+
|
|
27
|
+
const options = program.opts();
|
|
28
|
+
|
|
29
|
+
// print help if no arguments are passed
|
|
30
|
+
if (program.options.length === 0) program.help();
|
|
31
|
+
|
|
32
|
+
if (!options.dir) {
|
|
33
|
+
console.error("Repo directory is required. Please specify the directory using the -d or --dir option.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`Analysis of: ${process.env.PWD}/${options.dir}`);
|
|
38
|
+
logger.info(`Analysis of: ${process.env.PWD}/${options.dir}`);
|
|
39
|
+
console.log('=====================================');
|
|
40
|
+
logger.info('=====================================');
|
|
41
|
+
|
|
42
|
+
export { options };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { analyzeCodebase } from './engine';
|
|
2
|
+
import { Engine } from 'json-rules-engine';
|
|
3
|
+
import { collectRepoFileData, collectStandardDirectoryStructure } from '../facts/repoFilesystemFacts';
|
|
4
|
+
import { getDependencyVersionFacts, collectMinimumDependencyVersions } from '../facts/repoDependencyFacts';
|
|
5
|
+
import { loadRules } from '../rules';
|
|
6
|
+
import { operators } from '../operators';
|
|
7
|
+
import { logger } from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
jest.mock('json-rules-engine');
|
|
10
|
+
jest.mock('../facts/repoFilesystemFacts');
|
|
11
|
+
jest.mock('../facts/repoDependencyFacts');
|
|
12
|
+
jest.mock('../rules');
|
|
13
|
+
jest.mock('../operators');
|
|
14
|
+
jest.mock('../utils/logger');
|
|
15
|
+
|
|
16
|
+
describe('analyzeCodebase', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should analyze the codebase and return results', async () => {
|
|
22
|
+
const mockFileData = [{ filePath: 'src/index.ts', fileContent: 'console.log("Hello, world!");' }];
|
|
23
|
+
const mockDependencyData = [{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' }];
|
|
24
|
+
const mockStandardStructure = { src: { core: null, utils: null, operators: null, rules: null, facts: null } };
|
|
25
|
+
const mockRules = [{ name: 'mockRule', conditions: { all: [] }, event: { type: 'mockEvent' } }];
|
|
26
|
+
|
|
27
|
+
(collectRepoFileData as jest.Mock).mockResolvedValue(mockFileData);
|
|
28
|
+
(getDependencyVersionFacts as jest.Mock).mockResolvedValue(mockDependencyData);
|
|
29
|
+
(collectMinimumDependencyVersions as jest.Mock).mockResolvedValue({});
|
|
30
|
+
(collectStandardDirectoryStructure as jest.Mock).mockResolvedValue(mockStandardStructure);
|
|
31
|
+
(loadRules as jest.Mock).mockResolvedValue(mockRules);
|
|
32
|
+
|
|
33
|
+
const engineRunMock = jest.fn().mockResolvedValue({ failureResults: [] });
|
|
34
|
+
(Engine as jest.Mock).mockImplementation(() => ({
|
|
35
|
+
addOperator: jest.fn(),
|
|
36
|
+
addRule: jest.fn(),
|
|
37
|
+
on: jest.fn(),
|
|
38
|
+
run: engineRunMock
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const results = await analyzeCodebase('mockRepoPath');
|
|
42
|
+
|
|
43
|
+
expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath');
|
|
44
|
+
expect(getDependencyVersionFacts).toHaveBeenCalled();
|
|
45
|
+
expect(collectMinimumDependencyVersions).toHaveBeenCalled();
|
|
46
|
+
expect(collectStandardDirectoryStructure).toHaveBeenCalled();
|
|
47
|
+
expect(loadRules).toHaveBeenCalled();
|
|
48
|
+
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
|
49
|
+
expect(results).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle errors during analysis', async () => {
|
|
53
|
+
const mockFileData = [{ filePath: 'src/index.ts', fileContent: 'console.log("Hello, world!");' }];
|
|
54
|
+
const mockDependencyData = [{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' }];
|
|
55
|
+
const mockStandardStructure = { src: { core: null, utils: null, operators: null, rules: null, facts: null } };
|
|
56
|
+
const mockRules = [{ name: 'mockRule', conditions: { all: [] }, event: { type: 'mockEvent' } }];
|
|
57
|
+
|
|
58
|
+
(collectRepoFileData as jest.Mock).mockResolvedValue(mockFileData);
|
|
59
|
+
(getDependencyVersionFacts as jest.Mock).mockResolvedValue(mockDependencyData);
|
|
60
|
+
(collectMinimumDependencyVersions as jest.Mock).mockResolvedValue({});
|
|
61
|
+
(collectStandardDirectoryStructure as jest.Mock).mockResolvedValue(mockStandardStructure);
|
|
62
|
+
(loadRules as jest.Mock).mockResolvedValue(mockRules);
|
|
63
|
+
|
|
64
|
+
const engineRunMock = jest.fn().mockRejectedValue(new Error('mock error'));
|
|
65
|
+
(Engine as jest.Mock).mockImplementation(() => ({
|
|
66
|
+
addOperator: jest.fn(),
|
|
67
|
+
addRule: jest.fn(),
|
|
68
|
+
on: jest.fn(),
|
|
69
|
+
run: engineRunMock
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const results = await analyzeCodebase('mockRepoPath');
|
|
73
|
+
|
|
74
|
+
expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath');
|
|
75
|
+
expect(getDependencyVersionFacts).toHaveBeenCalled();
|
|
76
|
+
expect(collectMinimumDependencyVersions).toHaveBeenCalled();
|
|
77
|
+
expect(collectStandardDirectoryStructure).toHaveBeenCalled();
|
|
78
|
+
expect(loadRules).toHaveBeenCalled();
|
|
79
|
+
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
|
80
|
+
expect(results).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import { Engine,RuleProperties } from 'json-rules-engine';
|
|
3
|
+
import { FileData, collectRepoFileData, collectStandardDirectoryStructure } from '../facts/repoFilesystemFacts';
|
|
4
|
+
import { loadRules } from '../rules';
|
|
5
|
+
import { operators } from '../operators';
|
|
6
|
+
import { ScanResult, RuleFailure } from '../typeDefs';
|
|
7
|
+
import { getDependencyVersionFacts, collectMinimumDependencyVersions } from
|
|
8
|
+
'../facts/repoDependencyFacts';
|
|
9
|
+
|
|
10
|
+
async function analyzeCodebase(repoPath: string, configUrl?: string): Promise<any[]> {
|
|
11
|
+
const installedDependencyVersions = await getDependencyVersionFacts();
|
|
12
|
+
const fileData: FileData[] = await collectRepoFileData(repoPath);
|
|
13
|
+
const minimumDependencyVersions = await collectMinimumDependencyVersions(configUrl);
|
|
14
|
+
const standardStructure = await collectStandardDirectoryStructure(configUrl);
|
|
15
|
+
|
|
16
|
+
const engine = new Engine([], { replaceFactsInEventParams: true });
|
|
17
|
+
|
|
18
|
+
// Add operators to engine
|
|
19
|
+
operators.map((operator) => engine.addOperator(operator.name, operator.fn));
|
|
20
|
+
|
|
21
|
+
// Add rules to engine
|
|
22
|
+
const rules: RuleProperties[] = await loadRules();
|
|
23
|
+
|
|
24
|
+
rules.map((rule) => {
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
engine.addRule(rule)
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
console.error(`Error loading rule: ${rule?.name}`);
|
|
30
|
+
console.error(e.message);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
engine.on('failure', function(event, almanac, ruleResult) {
|
|
35
|
+
//console.log(event)
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Run the engine for each file's data
|
|
39
|
+
let results: ScanResult[] = [];
|
|
40
|
+
for (const file of fileData) {
|
|
41
|
+
logger.info(`running engine for ${file.filePath}`);
|
|
42
|
+
|
|
43
|
+
const facts = {fileData: file, dependencyData: {installedDependencyVersions, minimumDependencyVersions},
|
|
44
|
+
standardStructure};
|
|
45
|
+
let fileFailures: RuleFailure[] = [];
|
|
46
|
+
|
|
47
|
+
await engine.run(facts)
|
|
48
|
+
.then(({failureResults}) => {
|
|
49
|
+
failureResults.map((result) => {
|
|
50
|
+
fileFailures.push({
|
|
51
|
+
ruleFailure: result?.name,
|
|
52
|
+
details: result?.event?.params
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}).catch(e => logger.error(e));
|
|
56
|
+
|
|
57
|
+
if (fileFailures.length > 0) {
|
|
58
|
+
results.push({ filePath: file.filePath, errors: fileFailures});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.info(`${fileData.length} files analyzed. ${results.length} files with errors.`)
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { analyzeCodebase };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { collectMinimumDependencyVersions, collectLocalDependencies, getDependencyVersionFacts, findPropertiesInTree } from './repoDependencyFacts';
|
|
3
|
+
import { logger } from '../utils/logger';
|
|
4
|
+
import _ from 'lodash';
|
|
5
|
+
|
|
6
|
+
jest.mock('child_process', () => ({
|
|
7
|
+
execSync: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock('../utils/logger', () => ({
|
|
11
|
+
logger: {
|
|
12
|
+
error: jest.fn(),
|
|
13
|
+
debug: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('collectMinimumDependencyVersions', () => {
|
|
18
|
+
it('should return the correct minimum dependency versions', async () => {
|
|
19
|
+
const result = await collectMinimumDependencyVersions();
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
commander: '^2.0.0',
|
|
22
|
+
nodemon: '^3.9.0'
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('collectLocalDependencies', () => {
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return parsed JSON result when execSync succeeds', () => {
|
|
33
|
+
const mockResult = JSON.stringify({ dependencies: {} });
|
|
34
|
+
(execSync as jest.Mock).mockReturnValue(Buffer.from(mockResult));
|
|
35
|
+
|
|
36
|
+
const result = collectLocalDependencies();
|
|
37
|
+
expect(result).toEqual({ dependencies: {} });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should log error and return empty object when execSync fails', () => {
|
|
41
|
+
(execSync as jest.Mock).mockImplementation(() => {
|
|
42
|
+
throw new Error('mock error');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = collectLocalDependencies();
|
|
46
|
+
expect(logger.error).toHaveBeenCalledWith('exec error: Error: mock error');
|
|
47
|
+
//expect(console.error).toHaveBeenCalledWith('exec error: Error: mock error');
|
|
48
|
+
expect(result).toEqual({});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle non-JSON response gracefully', () => {
|
|
52
|
+
(execSync as jest.Mock).mockReturnValue(Buffer.from('invalid json'));
|
|
53
|
+
|
|
54
|
+
const result = collectLocalDependencies();
|
|
55
|
+
expect(result).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('getDependencyVersionFacts', () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
jest.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return installed dependency versions correctly', async () => {
|
|
65
|
+
const mockLocalDependencies = { dependencies: { commander: { version: '2.0.0' }, nodemon: { version: '3.9.0' } } };
|
|
66
|
+
const mockMinimumVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
67
|
+
|
|
68
|
+
(execSync as jest.Mock).mockReturnValue(Buffer.from(JSON.stringify(mockLocalDependencies)));
|
|
69
|
+
|
|
70
|
+
const result = await getDependencyVersionFacts();
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
|
|
73
|
+
{ dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return an empty array if no dependencies match minimum versions', async () => {
|
|
78
|
+
const mockLocalDependencies = { dependencies: { someOtherDep: { version: '1.0.0' } } };
|
|
79
|
+
const mockMinimumVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
80
|
+
|
|
81
|
+
(execSync as jest.Mock).mockReturnValue(Buffer.from(JSON.stringify(mockLocalDependencies)));
|
|
82
|
+
|
|
83
|
+
const result = await getDependencyVersionFacts();
|
|
84
|
+
expect(result).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('findPropertiesInTree', () => {
|
|
89
|
+
it('should find properties in the tree correctly', () => {
|
|
90
|
+
const depGraph = {
|
|
91
|
+
commander: { version: '2.0.0' },
|
|
92
|
+
nodemon: { version: '3.9.0' }
|
|
93
|
+
};
|
|
94
|
+
const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
95
|
+
|
|
96
|
+
const result = findPropertiesInTree(depGraph, minVersions);
|
|
97
|
+
expect(result).toEqual([
|
|
98
|
+
{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
|
|
99
|
+
{ dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return an empty array if no properties match', () => {
|
|
104
|
+
const depGraph = {
|
|
105
|
+
someOtherDep: { version: '1.0.0' }
|
|
106
|
+
};
|
|
107
|
+
const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
108
|
+
|
|
109
|
+
const result = findPropertiesInTree(depGraph, minVersions);
|
|
110
|
+
expect(result).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle nested dependencies correctly', () => {
|
|
114
|
+
const depGraph = {
|
|
115
|
+
commander: { version: '2.0.0',
|
|
116
|
+
dependencies: {
|
|
117
|
+
nodemon: { version: '3.9.0' }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
122
|
+
|
|
123
|
+
const result = findPropertiesInTree(depGraph, minVersions);
|
|
124
|
+
expect(result).toEqual([
|
|
125
|
+
{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
|
|
126
|
+
{ dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should not add non-matching dependencies to results', () => {
|
|
131
|
+
const depGraph = {
|
|
132
|
+
someOtherDep: { version: '1.0.0' }
|
|
133
|
+
};
|
|
134
|
+
const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
|
|
135
|
+
|
|
136
|
+
const result = findPropertiesInTree(depGraph, minVersions);
|
|
137
|
+
expect(result).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should handle complex mixed structures', () => {
|
|
141
|
+
const depGraph = {
|
|
142
|
+
commander: {
|
|
143
|
+
version: '2.0.0',
|
|
144
|
+
dependencies: {
|
|
145
|
+
nodemon: {
|
|
146
|
+
version: '3.9.0',
|
|
147
|
+
dependencies: {
|
|
148
|
+
lodash: {
|
|
149
|
+
version: '4.17.21'
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0', lodash: '^4.17.20' };
|
|
157
|
+
|
|
158
|
+
const result = findPropertiesInTree(depGraph, minVersions);
|
|
159
|
+
//console.log(result);
|
|
160
|
+
expect(result).toEqual([
|
|
161
|
+
{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
|
|
162
|
+
{ dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' },
|
|
163
|
+
{ dep: 'lodash', ver: '4.17.21', min: '^4.17.20' }
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { LocalDependencies, MinimumDepVersions, VersionData } from '../typeDefs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collects the minimum dependency versions.
|
|
9
|
+
* @returns The minimum dependency versions.
|
|
10
|
+
*/
|
|
11
|
+
export async function collectMinimumDependencyVersions(configUrl?: string) {
|
|
12
|
+
if (configUrl) {
|
|
13
|
+
try {
|
|
14
|
+
const response = await axios.get(configUrl);
|
|
15
|
+
return response.data.minimumDependencyVersions;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
logger.error(`Error fetching minimum dependency versions from configUrl: ${error}`);
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
return {
|
|
22
|
+
commander: '^2.0.0',
|
|
23
|
+
nodemon: '^3.9.0'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collects the local dependencies.
|
|
30
|
+
* @returns The local dependencies.
|
|
31
|
+
*/
|
|
32
|
+
export function collectLocalDependencies(): LocalDependencies {
|
|
33
|
+
let result: LocalDependencies = {};
|
|
34
|
+
try {
|
|
35
|
+
let stdout = execSync('npm ls -a --json');
|
|
36
|
+
result = JSON.parse(stdout.toString());
|
|
37
|
+
} catch (e) {
|
|
38
|
+
logger.error(`exec error: ${e}`);
|
|
39
|
+
//console.error(`exec error: ${e}`);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets the installed dependency versions.
|
|
46
|
+
* @returns The installed dependency versions.
|
|
47
|
+
*/
|
|
48
|
+
export async function getDependencyVersionFacts() {
|
|
49
|
+
const localDependencies = await collectLocalDependencies();
|
|
50
|
+
const minimumDependencyVersions = await collectMinimumDependencyVersions();
|
|
51
|
+
|
|
52
|
+
//console.log(localDependencies);
|
|
53
|
+
|
|
54
|
+
const installedDependencyVersions = findPropertiesInTree(localDependencies, minimumDependencyVersions);
|
|
55
|
+
return installedDependencyVersions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively search for properties in a tree of objects.
|
|
60
|
+
* @param depGraph - The object to search.
|
|
61
|
+
* @param minVersions - The minimum dependency versions to search for.
|
|
62
|
+
* @returns An array of results.
|
|
63
|
+
*/
|
|
64
|
+
export function findPropertiesInTree(depGraph: LocalDependencies, minVersions: MinimumDepVersions): VersionData[] {
|
|
65
|
+
let results: VersionData[] = [];
|
|
66
|
+
|
|
67
|
+
logger.debug(`depGraph: ${depGraph}`);
|
|
68
|
+
|
|
69
|
+
function walk(depGraph: LocalDependencies) {
|
|
70
|
+
if (_.isObject(depGraph) && !_.isArray(depGraph)) {
|
|
71
|
+
for (let depName in depGraph) {
|
|
72
|
+
if (Object.keys(minVersions).includes(depName)) {
|
|
73
|
+
results.push({ dep: depName, ver: depGraph[depName].version, min: minVersions[depName] });
|
|
74
|
+
}
|
|
75
|
+
if (_.isObject(depGraph[depName])) {
|
|
76
|
+
walk(depGraph[depName] as unknown as LocalDependencies);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (_.isArray(depGraph)) {
|
|
80
|
+
for (let item of depGraph) {
|
|
81
|
+
walk(item);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
walk(depGraph);
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
interface FileData {
|
|
7
|
+
fileName: string;
|
|
8
|
+
filePath: string;
|
|
9
|
+
fileContent: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function parseFile(filePath: string): Promise<FileData> {
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
fileName: path.basename(filePath),
|
|
15
|
+
filePath,
|
|
16
|
+
fileContent: fs.readFileSync(filePath, 'utf8')
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function collectRepoFileData(repoPath: string): Promise<FileData[]> {
|
|
21
|
+
const filesData: FileData[] = [];
|
|
22
|
+
|
|
23
|
+
logger.debug(`collectingRepoFileData from: ${repoPath}`);
|
|
24
|
+
const files = fs.readdirSync(repoPath);
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const filePath = path.join(repoPath, file);
|
|
28
|
+
if (!ignored(filePath)) {
|
|
29
|
+
const stats = await fs.promises.lstat(filePath);
|
|
30
|
+
if (stats.isDirectory()) {
|
|
31
|
+
const dirFilesData = await collectRepoFileData(filePath);
|
|
32
|
+
filesData.push(...dirFilesData);
|
|
33
|
+
} else {
|
|
34
|
+
const fileData = await parseFile(filePath);
|
|
35
|
+
filesData.push(fileData);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return filesData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ignored(file: string){
|
|
43
|
+
return file.startsWith('.')
|
|
44
|
+
|| file.includes('node_modules')
|
|
45
|
+
|| file.includes('/.')
|
|
46
|
+
|| file.endsWith('-rule.json')
|
|
47
|
+
|| file.includes('/dist/')
|
|
48
|
+
|| file.includes('/lcov')
|
|
49
|
+
|| file.startsWith('dist')
|
|
50
|
+
|| file.endsWith('md')
|
|
51
|
+
|| file.endsWith('.log')
|
|
52
|
+
|| file.includes('LICENSE');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const standardStructure = {
|
|
56
|
+
"src": {
|
|
57
|
+
"core": null,
|
|
58
|
+
"utils": null,
|
|
59
|
+
"operators": null,
|
|
60
|
+
"rules": null,
|
|
61
|
+
"facts": null,
|
|
62
|
+
"FAIL": null
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function collectStandardDirectoryStructure(configUrl?: string) {
|
|
67
|
+
if (configUrl) {
|
|
68
|
+
try {
|
|
69
|
+
const response = await axios.get(configUrl);
|
|
70
|
+
return response.data.standardStructure;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(`Error fetching standard structure from configUrl: ${error}`);
|
|
73
|
+
return standardStructure;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
return standardStructure;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { collectRepoFileData, FileData, collectStandardDirectoryStructure }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { logger } from './utils/logger';
|
|
3
|
+
let json = require('format-json');
|
|
4
|
+
import { options } from "./core/cli";
|
|
5
|
+
import { analyzeCodebase } from "./core/engine";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//console.log(`analyzing repo at path: [${options.dir}]`);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
(async () => {
|
|
12
|
+
let results = await analyzeCodebase(options.dir, options.configUrl);
|
|
13
|
+
if (results.length > 0) {
|
|
14
|
+
//console.log('WARNING: lo-fi attributes detected in codebase!');
|
|
15
|
+
//console.log(results);
|
|
16
|
+
} else {
|
|
17
|
+
//console.log('hi-fi codebase detected!');
|
|
18
|
+
}
|
|
19
|
+
logger.info(results);
|
|
20
|
+
console.log(JSON.stringify(results));
|
|
21
|
+
//console.log(`opinionated codebase analysis completed with ${results.length} failed checks.`);
|
|
22
|
+
})().catch((e) => {console.log(e)});
|
|
23
|
+
} catch(e) {
|
|
24
|
+
console.log(e)
|
|
25
|
+
}
|