Merge branch 'fix-typo-drive' into 'main'
[ProtonMail-WebClient.git] / packages / metrics / scripts / generate-metrics.ts
blobf78265485b26d2986b2856f15c40750f29e63b40
1 import fs from 'fs/promises';
2 import path from 'path';
3 import * as prettier from 'prettier';
4 import readline from 'readline';
5 import ts from 'typescript';
7 type MetricType = 'Counter' | 'Histogram';
9 interface ParsedFile {
10     importName: string;
11     interfaceName: string;
12     type: MetricType;
13     propName: string;
14     metricName: string;
15     version: number;
18 async function findFirstInterfaceName(fileName: string) {
19     const sourceCode = await fs.readFile(fileName, 'utf-8');
20     const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.ES2021, true);
22     function visit(node: ts.Node): any {
23         if (ts.isInterfaceDeclaration(node)) {
24             const foundInterfaceName = node.name && node.name.text;
25             return foundInterfaceName;
26         }
28         return ts.forEachChild(node, visit);
29     }
31     return visit(sourceFile);
34 const typesDirectoryPath = './types';
36 function getImportName(fileName: string) {
37     return fileName.replace(/\.d\.ts$/, '');
40 function getType(fileName: string): MetricType {
41     const isCounterRegex = /.*total_v\d.schema.d.ts$/;
42     const isCounter = isCounterRegex.test(fileName);
44     if (isCounter) {
45         return 'Counter';
46     }
48     return 'Histogram';
51 function getPropName(fileName: string) {
52     return fileName.replace(/^web_/, '').replace(/_v\d.schema.d.ts/, '');
55 function getMetricName(fileName: string) {
56     return fileName.replace(/_v\d.schema.d.ts/, '');
59 function getVersion(fileName: string): number {
60     const regex = /_v(\d+)\.schema\.d\.ts/;
61     const match = fileName.match(regex);
63     if (!match) {
64         return NaN;
65     }
67     return parseInt(match[1]);
70 function getVersionInMetricsClassByName(metricsClass: string, metricName: string) {
71     const existingVersionMatch = metricsClass.match(new RegExp(`${metricName}.*version: (\\d+)`));
72     const existingVersion = existingVersionMatch ? parseInt(existingVersionMatch[1], 10) : 0;
73     return existingVersion;
76 async function handleFile(fileName: string) {
77     const importName = getImportName(fileName);
78     const interfaceName = await findFirstInterfaceName(path.join(typesDirectoryPath, fileName));
79     const type = getType(fileName);
80     const propName = getPropName(fileName);
81     const metricName = getMetricName(fileName);
82     const version = getVersion(fileName);
84     const parsedFile: ParsedFile = {
85         importName,
86         interfaceName,
87         type,
88         propName,
89         metricName,
90         version,
91     };
93     return parsedFile;
96 async function generateCode(parsedFiles: ParsedFile[]) {
97     let includeCounterImport = false;
98     let includeHistogramImport = false;
100     let code = `
101 import MetricsBase from './lib/MetricsBase';
102 import type IMetricsRequestService from './lib/types/IMetricsRequestService';
103     `;
105     const addImport = (interfaceName: string, importName: string) => {
106         code += `
107 import type { ${interfaceName} } from '${typesDirectoryPath}/${importName}'; 
108         `;
109     };
111     parsedFiles.forEach((file) => {
112         addImport(file.interfaceName, file.importName);
113     });
115     code += `
116 class Metrics extends MetricsBase {
117     `;
119     const addProperty = ({ propName, type, interfaceName }: ParsedFile) => {
120         if (!includeCounterImport && type === 'Counter') {
121             includeCounterImport = true;
122         } else if (!includeHistogramImport && type === 'Histogram') {
123             includeHistogramImport = true;
124         }
126         code += `
127     public ${propName}: ${type}<${interfaceName}>;
128     `;
129     };
131     parsedFiles.forEach((file) => {
132         addProperty(file);
133     });
135     code += `
136     constructor(requestService: IMetricsRequestService) {
137         super(requestService);
138     `;
140     const initProperty = ({ propName, type, interfaceName, metricName, version }: ParsedFile) => {
141         code += `
142     this.${propName} = new ${type}<${interfaceName}>({ name: '${metricName}', version: ${version} }, this.requestService);
143     `;
144     };
146     parsedFiles.forEach((file) => {
147         initProperty(file);
148     });
150     code += `
151     }
154 export default Metrics;
157     if (includeCounterImport) {
158         code =
159             `
160 import Counter from './lib/Counter';
161     ` + code;
162     }
164     if (includeHistogramImport) {
165         code =
166             `
167 import Histogram from './lib/Histogram';
168     ` + code;
169     }
171     code =
172         `
174  * THIS CODE IS AUTOGENERATED
175  * using \`yarn workspace @proton/metrics generate-metrics\`
176  * 
177  * For more information please consult the documentation
178  * https://confluence.protontech.ch/pages/viewpage.action?pageId=121927830
179  */
181 ` + code;
183     const options = await prettier.resolveConfig(path.join('..', '..', 'prettier.config.mjs'));
184     return prettier.format(code, { ...options, parser: 'typescript' });
187 const rl = readline.createInterface({
188     input: process.stdin,
189     output: process.stdout,
192 const promptUser = (question: string): Promise<string> => {
193     return new Promise((resolve) => {
194         rl.question(question, resolve);
195     });
198 async function main() {
199     const existingMetricsContent = await fs.readFile('./Metrics.ts', 'utf-8');
201     const files = await fs.readdir(typesDirectoryPath);
203     const tsFiles = files.filter((fileName) => fileName.endsWith('.ts'));
205     const parsedFiles = await Promise.all(tsFiles.map((file) => handleFile(file)));
207     const dedupedFiles: { [metricName: string]: ParsedFile } = {};
209     for (const file of parsedFiles) {
210         const existingVersion = getVersionInMetricsClassByName(existingMetricsContent, file.metricName);
212         const futureVersion = dedupedFiles[file.metricName];
213         if (futureVersion && futureVersion.version >= file.version) {
214             continue;
215         }
217         if (existingVersion !== 0 && file.version > existingVersion) {
218             const answer = await promptUser(
219                 `Metric "${file.metricName}" already exists with version ${existingVersion}. ` +
220                     `Do you want to replace it with version ${file.version}? (y/n): `
221             );
222             if (answer.toLowerCase().includes('y')) {
223                 console.log(
224                     `\x1b[1m\x1b[31mConsider deleting the version ${existingVersion} of ${file.metricName} when possible. See documentation here: https://confluence.protontech.ch/pages/viewpage.action?pageId=121927830#id-@proton/metrics-Deletinganexistingmetric\x1b[0m`
225                 );
226                 dedupedFiles[file.metricName] = file;
227             }
228         } else {
229             dedupedFiles[file.metricName] = file;
230         }
231     }
233     rl.close();
235     const code = await generateCode(Object.values(dedupedFiles));
237     await fs.writeFile('./Metrics.ts', code);
240 void main();