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.
Files changed (53) hide show
  1. package/.czrc +3 -0
  2. package/.github/dependabot.yml +11 -0
  3. package/.github/workflows/ci.yml +28 -0
  4. package/.github/workflows/release.yml +34 -0
  5. package/.releaserc +11 -0
  6. package/CHANGELOG.md +20 -0
  7. package/LICENSE +21 -0
  8. package/README.md +81 -0
  9. package/commitlint.config.js +3 -0
  10. package/dist/core/cli.js +37 -0
  11. package/dist/core/engine.js +67 -0
  12. package/dist/core/engine.test.js +79 -0
  13. package/dist/facts/repoDependencyFacts.js +105 -0
  14. package/dist/facts/repoDependencyFacts.test.js +149 -0
  15. package/dist/facts/repoFilesystemFacts.js +91 -0
  16. package/dist/index.js +35 -0
  17. package/dist/operators/currentDependencies.js +58 -0
  18. package/dist/operators/currentDependencies.test.js +43 -0
  19. package/dist/operators/directoryStructureMatches.js +63 -0
  20. package/dist/operators/directoryStructureMatches.test.js +61 -0
  21. package/dist/operators/fileContains.js +17 -0
  22. package/dist/operators/fileContains.test.js +25 -0
  23. package/dist/operators/index.js +11 -0
  24. package/dist/rules/index.js +61 -0
  25. package/dist/rules/noDatabases-rule.json +21 -0
  26. package/dist/rules/standardDirectoryStructure-rule.json +25 -0
  27. package/dist/rules/supportedCoreDependencies-rule.json +29 -0
  28. package/dist/typeDefs.js +2 -0
  29. package/dist/utils/logger.js +14 -0
  30. package/dist/xfidelity +35 -0
  31. package/jest.config.js +6 -0
  32. package/package.json +69 -0
  33. package/src/core/cli.ts +42 -0
  34. package/src/core/engine.test.ts +82 -0
  35. package/src/core/engine.ts +68 -0
  36. package/src/facts/repoDependencyFacts.test.ts +166 -0
  37. package/src/facts/repoDependencyFacts.ts +88 -0
  38. package/src/facts/repoFilesystemFacts.ts +80 -0
  39. package/src/index.ts +25 -0
  40. package/src/operators/currentDependencies.test.ts +45 -0
  41. package/src/operators/currentDependencies.ts +40 -0
  42. package/src/operators/directoryStructureMatches.test.ts +47 -0
  43. package/src/operators/directoryStructureMatches.ts +43 -0
  44. package/src/operators/fileContains.test.ts +27 -0
  45. package/src/operators/fileContains.ts +19 -0
  46. package/src/operators/index.ts +12 -0
  47. package/src/rules/index.ts +29 -0
  48. package/src/rules/noDatabases-rule.json +21 -0
  49. package/src/rules/standardDirectoryStructure-rule.json +25 -0
  50. package/src/rules/supportedCoreDependencies-rule.json +29 -0
  51. package/src/typeDefs.ts +40 -0
  52. package/src/utils/logger.ts +20 -0
  53. package/tsconfig.json +9 -0
package/.czrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "path": "cz-conventional-changelog"
3
+ }
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "npm" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - master
7
+ push:
8
+ branches:
9
+ - master
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v2
18
+
19
+ - name: Set up Node.js
20
+ uses: actions/setup-node@v2
21
+ with:
22
+ node-version: '20'
23
+
24
+ - name: Install dependencies
25
+ run: yarn install
26
+
27
+ - name: Run tests
28
+ run: yarn test
@@ -0,0 +1,34 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v2
15
+
16
+ - name: Set up Node.js
17
+ uses: actions/setup-node@v2
18
+ with:
19
+ node-version: '20'
20
+
21
+ - name: Install dependencies
22
+ run: yarn install
23
+
24
+ - name: Run Build
25
+ run: yarn build
26
+
27
+ - name: Run tests
28
+ run: yarn test
29
+
30
+ - name: Run semantic-release
31
+ run: npx semantic-release
32
+ env:
33
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
34
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.releaserc ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "branches": ["master"],
3
+ "plugins": [
4
+ "@semantic-release/commit-analyzer",
5
+ "@semantic-release/release-notes-generator",
6
+ "@semantic-release/changelog",
7
+ "@semantic-release/npm",
8
+ "@semantic-release/github",
9
+ "@semantic-release/git"
10
+ ]
11
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # 1.0.0 (2024-06-15)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **access:** cd ([0016364](https://github.com/zotoio/x-fidelity/commit/0016364e94ca5175f46c76487363970f4378ada0))
7
+ * **ascii:** readme fix ([86a4448](https://github.com/zotoio/x-fidelity/commit/86a444809f502ac60aad7076fb09b3674a32750a))
8
+ * **ci:** branch name ([858a3cd](https://github.com/zotoio/x-fidelity/commit/858a3cd1f0f0290fed88ce70806e96eff65ecd69))
9
+ * **ci:** node version ([8da6d00](https://github.com/zotoio/x-fidelity/commit/8da6d004eb73109daaad9c867a4aaec35a30a17f))
10
+ * **config:** release ([ccf103c](https://github.com/zotoio/x-fidelity/commit/ccf103cfafdedd1c4bbf17ed2829bcc5392c7ab8))
11
+ * **deps:** missing ([367c43e](https://github.com/zotoio/x-fidelity/commit/367c43e356b50560edfcace2b212c685d4a59033))
12
+ * **pipeline:** test ([ee4a098](https://github.com/zotoio/x-fidelity/commit/ee4a0988ddf7f235fde6a454f86905b6c15ea6cf))
13
+ * **release bin:** local install and cd ([750ab7a](https://github.com/zotoio/x-fidelity/commit/750ab7a7a71163709fde331ca17c8b17895223f2))
14
+ * **test:** merge ([603ebc5](https://github.com/zotoio/x-fidelity/commit/603ebc58c930da0b2d8bdb465a457a02cfdbb28a))
15
+ * **trigger:** cd ([c039356](https://github.com/zotoio/x-fidelity/commit/c039356ecd2d64724411b9333adb7078ab17a2b8))
16
+
17
+
18
+ ### Features
19
+
20
+ * **pipelines:** setup gh actions ([ceca83a](https://github.com/zotoio/x-fidelity/commit/ceca83a2d0ea604dfb93a71d5a85fef4ffd8c6e7))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 zoto.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # x-fidelity
2
+
3
+ cli for opinionated framework adherence checks
4
+
5
+ ```
6
+ =====================================
7
+ __ __ ________ ______
8
+ | ## | ## | ######## \######
9
+ \##\/ ## ______ | ##__ | ##
10
+ >## ## | \| ## \ | ##
11
+ / ####\ \######| ###### | ##
12
+ | ## \##\ | ## _| ##_
13
+ | ## | ## | ## | ## \
14
+ \## \## \## \######
15
+
16
+ -------------------------------------
17
+ ```
18
+
19
+ ## What is x-fidelity?
20
+
21
+ x-fidelity is a CLI tool designed to enforce adherence to a set of opinionated framework rules within a codebase. It checks for various conditions such as directory structure, dependency versions, and the presence of certain strings in files.
22
+
23
+ ## Features
24
+
25
+ - Ensures the directory structure matches a predefined standard.
26
+ - Verifies that dependencies are up-to-date according to a specified minimum version.
27
+ - Checks for the presence or absence of specific strings in files.
28
+ - Configurable via a remote JSON configuration file.
29
+
30
+ ## Installation
31
+
32
+ To install x-fidelity, clone the repository and install the dependencies using yarn:
33
+
34
+ ```sh
35
+ yarn global add x-fidelity
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ To run x-fidelity, use the following command:
41
+
42
+ ```sh
43
+ xfidelity --dir <directory> [--configUrl <url>]
44
+ ```
45
+
46
+ - `--dir <directory>`: The directory of the repository to analyze.
47
+ - `--configUrl <url>`: (Optional) The URL to fetch the configuration from.
48
+
49
+ Example:
50
+
51
+ ```sh
52
+ xfidelity --dir my-repo --configUrl https://example.com/config.json
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ The configuration file should be a JSON file containing the following structure:
58
+
59
+ ```json
60
+ {
61
+ "minimumDependencyVersions": {
62
+ "commander": "^2.0.0",
63
+ "nodemon": "^3.9.0"
64
+ },
65
+ "standardStructure": {
66
+ "src": {
67
+ "core": null,
68
+ "utils": null,
69
+ "operators": null,
70
+ "rules": null,
71
+ "facts": null
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ TODO REMOTE CONF + LLM
78
+
79
+ ## License
80
+
81
+ This project is licensed under the MIT License.
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ extends: ['@commitlint/config-conventional']
3
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.options = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const commander_1 = require("commander");
6
+ const banner = (`
7
+ =====================================
8
+ __ __ ________ ______
9
+ | ## | ## | ######## \\######
10
+ \\##\\/ ## ______ | ##__ | ##
11
+ >## ## | \\| ## \\ | ##
12
+ / ####\\ \\######| ###### | ##
13
+ | ## \\##\\ | ## _| ##_
14
+ | ## | ## | ## | ## \\
15
+ \\## \\## \\## \\######
16
+
17
+ -------------------------------------
18
+ ${new Date().toString()}`);
19
+ console.log(banner);
20
+ logger_1.logger.info([banner]);
21
+ commander_1.program
22
+ .option("-d, --dir <directory>", "The repo checkout directory")
23
+ .option("-c, --configUrl <url>", "The URL used to fetch config");
24
+ commander_1.program.parse();
25
+ const options = commander_1.program.opts();
26
+ exports.options = options;
27
+ // print help if no arguments are passed
28
+ if (commander_1.program.options.length === 0)
29
+ commander_1.program.help();
30
+ if (!options.dir) {
31
+ console.error("Repo directory is required. Please specify the directory using the -d or --dir option.");
32
+ process.exit(1);
33
+ }
34
+ console.log(`Analysis of: ${process.env.PWD}/${options.dir}`);
35
+ logger_1.logger.info(`Analysis of: ${process.env.PWD}/${options.dir}`);
36
+ console.log('=====================================');
37
+ logger_1.logger.info('=====================================');
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.analyzeCodebase = void 0;
13
+ const logger_1 = require("../utils/logger");
14
+ const json_rules_engine_1 = require("json-rules-engine");
15
+ const repoFilesystemFacts_1 = require("../facts/repoFilesystemFacts");
16
+ const rules_1 = require("../rules");
17
+ const operators_1 = require("../operators");
18
+ const repoDependencyFacts_1 = require("../facts/repoDependencyFacts");
19
+ function analyzeCodebase(repoPath, configUrl) {
20
+ return __awaiter(this, void 0, void 0, function* () {
21
+ const installedDependencyVersions = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)();
22
+ const fileData = yield (0, repoFilesystemFacts_1.collectRepoFileData)(repoPath);
23
+ const minimumDependencyVersions = yield (0, repoDependencyFacts_1.collectMinimumDependencyVersions)(configUrl);
24
+ const standardStructure = yield (0, repoFilesystemFacts_1.collectStandardDirectoryStructure)(configUrl);
25
+ const engine = new json_rules_engine_1.Engine([], { replaceFactsInEventParams: true });
26
+ // Add operators to engine
27
+ operators_1.operators.map((operator) => engine.addOperator(operator.name, operator.fn));
28
+ // Add rules to engine
29
+ const rules = yield (0, rules_1.loadRules)();
30
+ rules.map((rule) => {
31
+ try {
32
+ engine.addRule(rule);
33
+ }
34
+ catch (e) {
35
+ console.error(`Error loading rule: ${rule === null || rule === void 0 ? void 0 : rule.name}`);
36
+ console.error(e.message);
37
+ }
38
+ });
39
+ engine.on('failure', function (event, almanac, ruleResult) {
40
+ //console.log(event)
41
+ });
42
+ // Run the engine for each file's data
43
+ let results = [];
44
+ for (const file of fileData) {
45
+ logger_1.logger.info(`running engine for ${file.filePath}`);
46
+ const facts = { fileData: file, dependencyData: { installedDependencyVersions, minimumDependencyVersions },
47
+ standardStructure };
48
+ let fileFailures = [];
49
+ yield engine.run(facts)
50
+ .then(({ failureResults }) => {
51
+ failureResults.map((result) => {
52
+ var _a;
53
+ fileFailures.push({
54
+ ruleFailure: result === null || result === void 0 ? void 0 : result.name,
55
+ details: (_a = result === null || result === void 0 ? void 0 : result.event) === null || _a === void 0 ? void 0 : _a.params
56
+ });
57
+ });
58
+ }).catch(e => logger_1.logger.error(e));
59
+ if (fileFailures.length > 0) {
60
+ results.push({ filePath: file.filePath, errors: fileFailures });
61
+ }
62
+ }
63
+ logger_1.logger.info(`${fileData.length} files analyzed. ${results.length} files with errors.`);
64
+ return results;
65
+ });
66
+ }
67
+ exports.analyzeCodebase = analyzeCodebase;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const engine_1 = require("./engine");
13
+ const json_rules_engine_1 = require("json-rules-engine");
14
+ const repoFilesystemFacts_1 = require("../facts/repoFilesystemFacts");
15
+ const repoDependencyFacts_1 = require("../facts/repoDependencyFacts");
16
+ const rules_1 = require("../rules");
17
+ jest.mock('json-rules-engine');
18
+ jest.mock('../facts/repoFilesystemFacts');
19
+ jest.mock('../facts/repoDependencyFacts');
20
+ jest.mock('../rules');
21
+ jest.mock('../operators');
22
+ jest.mock('../utils/logger');
23
+ describe('analyzeCodebase', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+ it('should analyze the codebase and return results', () => __awaiter(void 0, void 0, void 0, function* () {
28
+ const mockFileData = [{ filePath: 'src/index.ts', fileContent: 'console.log("Hello, world!");' }];
29
+ const mockDependencyData = [{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' }];
30
+ const mockStandardStructure = { src: { core: null, utils: null, operators: null, rules: null, facts: null } };
31
+ const mockRules = [{ name: 'mockRule', conditions: { all: [] }, event: { type: 'mockEvent' } }];
32
+ repoFilesystemFacts_1.collectRepoFileData.mockResolvedValue(mockFileData);
33
+ repoDependencyFacts_1.getDependencyVersionFacts.mockResolvedValue(mockDependencyData);
34
+ repoDependencyFacts_1.collectMinimumDependencyVersions.mockResolvedValue({});
35
+ repoFilesystemFacts_1.collectStandardDirectoryStructure.mockResolvedValue(mockStandardStructure);
36
+ rules_1.loadRules.mockResolvedValue(mockRules);
37
+ const engineRunMock = jest.fn().mockResolvedValue({ failureResults: [] });
38
+ json_rules_engine_1.Engine.mockImplementation(() => ({
39
+ addOperator: jest.fn(),
40
+ addRule: jest.fn(),
41
+ on: jest.fn(),
42
+ run: engineRunMock
43
+ }));
44
+ const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath');
45
+ expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath');
46
+ expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalled();
47
+ expect(repoDependencyFacts_1.collectMinimumDependencyVersions).toHaveBeenCalled();
48
+ expect(repoFilesystemFacts_1.collectStandardDirectoryStructure).toHaveBeenCalled();
49
+ expect(rules_1.loadRules).toHaveBeenCalled();
50
+ expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
51
+ expect(results).toEqual([]);
52
+ }));
53
+ it('should handle errors during analysis', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const mockFileData = [{ filePath: 'src/index.ts', fileContent: 'console.log("Hello, world!");' }];
55
+ const mockDependencyData = [{ dep: 'commander', ver: '2.0.0', min: '^2.0.0' }];
56
+ const mockStandardStructure = { src: { core: null, utils: null, operators: null, rules: null, facts: null } };
57
+ const mockRules = [{ name: 'mockRule', conditions: { all: [] }, event: { type: 'mockEvent' } }];
58
+ repoFilesystemFacts_1.collectRepoFileData.mockResolvedValue(mockFileData);
59
+ repoDependencyFacts_1.getDependencyVersionFacts.mockResolvedValue(mockDependencyData);
60
+ repoDependencyFacts_1.collectMinimumDependencyVersions.mockResolvedValue({});
61
+ repoFilesystemFacts_1.collectStandardDirectoryStructure.mockResolvedValue(mockStandardStructure);
62
+ rules_1.loadRules.mockResolvedValue(mockRules);
63
+ const engineRunMock = jest.fn().mockRejectedValue(new Error('mock error'));
64
+ json_rules_engine_1.Engine.mockImplementation(() => ({
65
+ addOperator: jest.fn(),
66
+ addRule: jest.fn(),
67
+ on: jest.fn(),
68
+ run: engineRunMock
69
+ }));
70
+ const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath');
71
+ expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath');
72
+ expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalled();
73
+ expect(repoDependencyFacts_1.collectMinimumDependencyVersions).toHaveBeenCalled();
74
+ expect(repoFilesystemFacts_1.collectStandardDirectoryStructure).toHaveBeenCalled();
75
+ expect(rules_1.loadRules).toHaveBeenCalled();
76
+ expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
77
+ expect(results).toEqual([]);
78
+ }));
79
+ });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.findPropertiesInTree = exports.getDependencyVersionFacts = exports.collectLocalDependencies = exports.collectMinimumDependencyVersions = void 0;
16
+ const logger_1 = require("../utils/logger");
17
+ const lodash_1 = __importDefault(require("lodash"));
18
+ const axios_1 = __importDefault(require("axios"));
19
+ const child_process_1 = require("child_process");
20
+ /**
21
+ * Collects the minimum dependency versions.
22
+ * @returns The minimum dependency versions.
23
+ */
24
+ function collectMinimumDependencyVersions(configUrl) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ if (configUrl) {
27
+ try {
28
+ const response = yield axios_1.default.get(configUrl);
29
+ return response.data.minimumDependencyVersions;
30
+ }
31
+ catch (error) {
32
+ logger_1.logger.error(`Error fetching minimum dependency versions from configUrl: ${error}`);
33
+ return {};
34
+ }
35
+ }
36
+ else {
37
+ return {
38
+ commander: '^2.0.0',
39
+ nodemon: '^3.9.0'
40
+ };
41
+ }
42
+ });
43
+ }
44
+ exports.collectMinimumDependencyVersions = collectMinimumDependencyVersions;
45
+ /**
46
+ * Collects the local dependencies.
47
+ * @returns The local dependencies.
48
+ */
49
+ function collectLocalDependencies() {
50
+ let result = {};
51
+ try {
52
+ let stdout = (0, child_process_1.execSync)('npm ls -a --json');
53
+ result = JSON.parse(stdout.toString());
54
+ }
55
+ catch (e) {
56
+ logger_1.logger.error(`exec error: ${e}`);
57
+ //console.error(`exec error: ${e}`);
58
+ }
59
+ return result;
60
+ }
61
+ exports.collectLocalDependencies = collectLocalDependencies;
62
+ /**
63
+ * Gets the installed dependency versions.
64
+ * @returns The installed dependency versions.
65
+ */
66
+ function getDependencyVersionFacts() {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ const localDependencies = yield collectLocalDependencies();
69
+ const minimumDependencyVersions = yield collectMinimumDependencyVersions();
70
+ //console.log(localDependencies);
71
+ const installedDependencyVersions = findPropertiesInTree(localDependencies, minimumDependencyVersions);
72
+ return installedDependencyVersions;
73
+ });
74
+ }
75
+ exports.getDependencyVersionFacts = getDependencyVersionFacts;
76
+ /**
77
+ * Recursively search for properties in a tree of objects.
78
+ * @param depGraph - The object to search.
79
+ * @param minVersions - The minimum dependency versions to search for.
80
+ * @returns An array of results.
81
+ */
82
+ function findPropertiesInTree(depGraph, minVersions) {
83
+ let results = [];
84
+ logger_1.logger.debug(`depGraph: ${depGraph}`);
85
+ function walk(depGraph) {
86
+ if (lodash_1.default.isObject(depGraph) && !lodash_1.default.isArray(depGraph)) {
87
+ for (let depName in depGraph) {
88
+ if (Object.keys(minVersions).includes(depName)) {
89
+ results.push({ dep: depName, ver: depGraph[depName].version, min: minVersions[depName] });
90
+ }
91
+ if (lodash_1.default.isObject(depGraph[depName])) {
92
+ walk(depGraph[depName]);
93
+ }
94
+ }
95
+ }
96
+ else if (lodash_1.default.isArray(depGraph)) {
97
+ for (let item of depGraph) {
98
+ walk(item);
99
+ }
100
+ }
101
+ }
102
+ walk(depGraph);
103
+ return results;
104
+ }
105
+ exports.findPropertiesInTree = findPropertiesInTree;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const child_process_1 = require("child_process");
13
+ const repoDependencyFacts_1 = require("./repoDependencyFacts");
14
+ const logger_1 = require("../utils/logger");
15
+ jest.mock('child_process', () => ({
16
+ execSync: jest.fn(),
17
+ }));
18
+ jest.mock('../utils/logger', () => ({
19
+ logger: {
20
+ error: jest.fn(),
21
+ debug: jest.fn(),
22
+ },
23
+ }));
24
+ describe('collectMinimumDependencyVersions', () => {
25
+ it('should return the correct minimum dependency versions', () => __awaiter(void 0, void 0, void 0, function* () {
26
+ const result = yield (0, repoDependencyFacts_1.collectMinimumDependencyVersions)();
27
+ expect(result).toEqual({
28
+ commander: '^2.0.0',
29
+ nodemon: '^3.9.0'
30
+ });
31
+ }));
32
+ });
33
+ describe('collectLocalDependencies', () => {
34
+ afterEach(() => {
35
+ jest.clearAllMocks();
36
+ });
37
+ it('should return parsed JSON result when execSync succeeds', () => {
38
+ const mockResult = JSON.stringify({ dependencies: {} });
39
+ child_process_1.execSync.mockReturnValue(Buffer.from(mockResult));
40
+ const result = (0, repoDependencyFacts_1.collectLocalDependencies)();
41
+ expect(result).toEqual({ dependencies: {} });
42
+ });
43
+ it('should log error and return empty object when execSync fails', () => {
44
+ child_process_1.execSync.mockImplementation(() => {
45
+ throw new Error('mock error');
46
+ });
47
+ const result = (0, repoDependencyFacts_1.collectLocalDependencies)();
48
+ expect(logger_1.logger.error).toHaveBeenCalledWith('exec error: Error: mock error');
49
+ //expect(console.error).toHaveBeenCalledWith('exec error: Error: mock error');
50
+ expect(result).toEqual({});
51
+ });
52
+ it('should handle non-JSON response gracefully', () => {
53
+ child_process_1.execSync.mockReturnValue(Buffer.from('invalid json'));
54
+ const result = (0, repoDependencyFacts_1.collectLocalDependencies)();
55
+ expect(result).toEqual({});
56
+ });
57
+ });
58
+ describe('getDependencyVersionFacts', () => {
59
+ afterEach(() => {
60
+ jest.clearAllMocks();
61
+ });
62
+ it('should return installed dependency versions correctly', () => __awaiter(void 0, void 0, void 0, function* () {
63
+ const mockLocalDependencies = { dependencies: { commander: { version: '2.0.0' }, nodemon: { version: '3.9.0' } } };
64
+ const mockMinimumVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
65
+ child_process_1.execSync.mockReturnValue(Buffer.from(JSON.stringify(mockLocalDependencies)));
66
+ const result = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)();
67
+ expect(result).toEqual([
68
+ { dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
69
+ { dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
70
+ ]);
71
+ }));
72
+ it('should return an empty array if no dependencies match minimum versions', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ const mockLocalDependencies = { dependencies: { someOtherDep: { version: '1.0.0' } } };
74
+ const mockMinimumVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
75
+ child_process_1.execSync.mockReturnValue(Buffer.from(JSON.stringify(mockLocalDependencies)));
76
+ const result = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)();
77
+ expect(result).toEqual([]);
78
+ }));
79
+ });
80
+ describe('findPropertiesInTree', () => {
81
+ it('should find properties in the tree correctly', () => {
82
+ const depGraph = {
83
+ commander: { version: '2.0.0' },
84
+ nodemon: { version: '3.9.0' }
85
+ };
86
+ const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
87
+ const result = (0, repoDependencyFacts_1.findPropertiesInTree)(depGraph, minVersions);
88
+ expect(result).toEqual([
89
+ { dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
90
+ { dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
91
+ ]);
92
+ });
93
+ it('should return an empty array if no properties match', () => {
94
+ const depGraph = {
95
+ someOtherDep: { version: '1.0.0' }
96
+ };
97
+ const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
98
+ const result = (0, repoDependencyFacts_1.findPropertiesInTree)(depGraph, minVersions);
99
+ expect(result).toEqual([]);
100
+ });
101
+ it('should handle nested dependencies correctly', () => {
102
+ const depGraph = {
103
+ commander: { version: '2.0.0',
104
+ dependencies: {
105
+ nodemon: { version: '3.9.0' }
106
+ }
107
+ }
108
+ };
109
+ const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
110
+ const result = (0, repoDependencyFacts_1.findPropertiesInTree)(depGraph, minVersions);
111
+ expect(result).toEqual([
112
+ { dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
113
+ { dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' }
114
+ ]);
115
+ });
116
+ it('should not add non-matching dependencies to results', () => {
117
+ const depGraph = {
118
+ someOtherDep: { version: '1.0.0' }
119
+ };
120
+ const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0' };
121
+ const result = (0, repoDependencyFacts_1.findPropertiesInTree)(depGraph, minVersions);
122
+ expect(result).toEqual([]);
123
+ });
124
+ it('should handle complex mixed structures', () => {
125
+ const depGraph = {
126
+ commander: {
127
+ version: '2.0.0',
128
+ dependencies: {
129
+ nodemon: {
130
+ version: '3.9.0',
131
+ dependencies: {
132
+ lodash: {
133
+ version: '4.17.21'
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ };
140
+ const minVersions = { commander: '^2.0.0', nodemon: '^3.9.0', lodash: '^4.17.20' };
141
+ const result = (0, repoDependencyFacts_1.findPropertiesInTree)(depGraph, minVersions);
142
+ //console.log(result);
143
+ expect(result).toEqual([
144
+ { dep: 'commander', ver: '2.0.0', min: '^2.0.0' },
145
+ { dep: 'nodemon', ver: '3.9.0', min: '^3.9.0' },
146
+ { dep: 'lodash', ver: '4.17.21', min: '^4.17.20' }
147
+ ]);
148
+ });
149
+ });