Skip to content

Commit 50f428a

Browse files
authored
feat(api): add a dev script to check API dependency violations
1 parent 259c223 commit 50f428a

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ high-level-tests/e2e/cypress/videos
7070
high-level-tests/load-testing/report/*.json
7171
high-level-tests/load-testing/report/*.html
7272

73+
# Dependency violations script
74+
api/dependencies-violations.json
75+
api/dependencies-violations.md
76+
7377
# Answers extracts
7478
api/scripts/extractions
7579

api/package-lock.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"chai-as-promised": "^8.0.0",
101101
"chai-sorted": "^0.2.0",
102102
"depcheck": "^1.4.3",
103+
"es-module-lexer": "^1.6.0",
103104
"eslint": "^9.0.0",
104105
"eslint-config-prettier": "^10.0.0",
105106
"eslint-plugin-chai-expect": "^3.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/* eslint-disable no-sync */
2+
import fs from 'node:fs';
3+
import { builtinModules } from 'node:module';
4+
import path from 'node:path';
5+
6+
import { init, parse } from 'es-module-lexer';
7+
import { glob } from 'glob';
8+
9+
import { Script } from '../../src/shared/application/scripts/script.js';
10+
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';
11+
12+
await init;
13+
14+
export class CheckApiDependencyViolations extends Script {
15+
constructor() {
16+
super({
17+
description: 'Check API dependency violations between contexts [dev only]',
18+
permanent: false,
19+
options: {
20+
report: {
21+
type: 'string',
22+
choices: ['json', 'md'],
23+
describe: 'Report output file type',
24+
requiresArg: true,
25+
default: 'json',
26+
},
27+
context: {
28+
type: 'string',
29+
describe: 'Filter with an API context name',
30+
requiresArg: true,
31+
},
32+
},
33+
});
34+
}
35+
36+
async handle({ options, logger }) {
37+
config.basePath = path.resolve(import.meta.dirname, '../..');
38+
if (!config.basePath.endsWith('/api')) {
39+
throw new Error('Pix API folder not found.');
40+
}
41+
42+
const excludedModules = [...getNodeModules(config), ...getBuiltinModules()];
43+
const excludedPath = [...getExcludedPath(config), ...getExcludedApisPath(config)];
44+
45+
const violationsByEntry = new Map();
46+
for (const entryPath of config.entries) {
47+
if (options.context && !entryPath.startsWith(`src/${options.context}`)) {
48+
continue;
49+
}
50+
51+
const violationsByFile = await checkDependencyViolations(
52+
config.basePath,
53+
entryPath,
54+
excludedModules,
55+
excludedPath,
56+
);
57+
violationsByEntry.set(entryPath, violationsByFile);
58+
}
59+
60+
const stats = computeGlobalStats(violationsByEntry);
61+
62+
let content = null;
63+
if (options.report === 'json') {
64+
content = JSON.stringify({ stats, violations: Object.fromEntries(violationsByEntry) });
65+
} else if (options.report === 'md') {
66+
content = reportToMarkdown(stats, violationsByEntry);
67+
}
68+
69+
if (content) {
70+
const reportFile = `${config.basePath}/dependencies-violations.${options.report}`;
71+
fs.writeFileSync(reportFile, content);
72+
logger.info(`Generating report file: ${reportFile}`);
73+
}
74+
}
75+
}
76+
77+
const config = {
78+
entries: [
79+
'src/authorization',
80+
'src/banner',
81+
'src/certification/complementary-certification',
82+
'src/certification/configuration',
83+
'src/certification/enrolment',
84+
'src/certification/evaluation',
85+
'src/certification/flash-certification',
86+
'src/certification/results',
87+
'src/certification/scoring',
88+
'src/certification/session-management',
89+
'src/certification/shared',
90+
'src/devcomp',
91+
'src/evaluation',
92+
'src/identity-access-management',
93+
'src/learning-content',
94+
'src/legal-documents',
95+
'src/maddo',
96+
'src/monitoring',
97+
'src/organizational-entities',
98+
'src/prescription/campaign',
99+
'src/prescription/campaign-participation',
100+
'src/prescription/learner-management',
101+
'src/prescription/organization-learner',
102+
'src/prescription/organization-learner-feature',
103+
'src/prescription/organization-place',
104+
'src/prescription/scripts',
105+
'src/prescription/shared',
106+
'src/prescription/target-profile',
107+
'src/privacy',
108+
'src/profile',
109+
'src/quest',
110+
'src/school',
111+
'src/shared',
112+
'src/team',
113+
],
114+
exclude: [
115+
'db',
116+
'config',
117+
'src/shared',
118+
'src/certification/shared',
119+
'src/prescription/shared',
120+
'translations',
121+
'package.json',
122+
],
123+
};
124+
125+
function getNodeModules(config) {
126+
const nodeModulesPath = path.join(config.basePath, 'node_modules');
127+
const folderContent = fs.readdirSync(nodeModulesPath);
128+
return folderContent.filter((file) => fs.statSync(path.join(nodeModulesPath, file)).isDirectory());
129+
}
130+
131+
function getBuiltinModules() {
132+
return [...builtinModules, ...builtinModules.map((module) => `node:${module}`)];
133+
}
134+
135+
function getExcludedPath(config) {
136+
return config.exclude.map((folderPath) => path.resolve(config.basePath, folderPath));
137+
}
138+
139+
function getExcludedApisPath(config) {
140+
return config.entries.map((entryPath) => path.resolve(config.basePath, entryPath, 'application/api'));
141+
}
142+
143+
async function checkDependencyViolations(basePath, entryPath, excludedModules, excludedPath) {
144+
const entryAbsolutePath = path.resolve(basePath, entryPath);
145+
const globPath = path.join(entryAbsolutePath, '**/*.js');
146+
const files = await glob(globPath);
147+
148+
const violationsByFile = {};
149+
150+
for (const file of files) {
151+
const source = fs.readFileSync(file, { encoding: 'utf-8' });
152+
const [dependencies] = parse(source);
153+
154+
for (const dependency of dependencies) {
155+
const name = dependency.n;
156+
157+
if (!name) continue;
158+
159+
if (excludedModules.includes(name)) continue;
160+
161+
const dependencyAbsolutePath = path.resolve(path.dirname(file), name);
162+
163+
if (excludedPath.some((p) => dependencyAbsolutePath.startsWith(p))) continue;
164+
165+
if (dependencyAbsolutePath.startsWith(entryAbsolutePath)) continue;
166+
167+
const line = source.slice(dependency.ss, dependency.se).replaceAll('\n', '');
168+
169+
const relativeFilePath = path.relative(basePath, file);
170+
if (violationsByFile[relativeFilePath]) {
171+
violationsByFile[relativeFilePath].push(line);
172+
} else {
173+
violationsByFile[relativeFilePath] = [line];
174+
}
175+
}
176+
}
177+
return violationsByFile;
178+
}
179+
180+
function computeGlobalStats(violationsByEntry) {
181+
let totalFiles = 0;
182+
183+
const entries = {};
184+
violationsByEntry.forEach((violationsByFile, entryPath) => {
185+
const violationsByFileEntries = Object.entries(violationsByFile);
186+
entries[entryPath] = violationsByFileEntries.length;
187+
totalFiles += violationsByFileEntries.length;
188+
});
189+
190+
return { totalFiles, entries };
191+
}
192+
193+
function reportToMarkdown(stats, violationsByEntry) {
194+
let content = '';
195+
content += '# Dependency violations report\n\n';
196+
197+
content += `| Entries | Files with violations |\n`;
198+
content += `| --- | --- |\n`;
199+
for (const [entryPath, totalFiles] of Object.entries(stats.entries)) {
200+
content += `| [${entryPath}](#${entryPath}) | ${totalFiles} |\n`;
201+
}
202+
content += `| **Total** | **${stats.totalFiles}** |\n`;
203+
204+
content += '# Entries\n\n';
205+
violationsByEntry.forEach((violationsByFile, entryPath) => {
206+
const violationsByFileEntries = Object.entries(violationsByFile);
207+
if (violationsByFileEntries.length > 0) {
208+
content += `## ${entryPath}\n\n`;
209+
for (const [file, violations] of violationsByFileEntries) {
210+
content += '```javascript\n';
211+
content += `// ${file}\n`;
212+
for (const violation of violations) {
213+
content += `${violation}\n`;
214+
}
215+
content += '```\n\n';
216+
}
217+
}
218+
});
219+
return content;
220+
}
221+
222+
await ScriptRunner.execute(import.meta.url, CheckApiDependencyViolations);
223+
/* eslint-enable no-sync */

0 commit comments

Comments
 (0)