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';
11 interfaceName: string;
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;
28 return ts.forEachChild(node, visit);
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);
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);
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 = {
96 async function generateCode(parsedFiles: ParsedFile[]) {
97 let includeCounterImport = false;
98 let includeHistogramImport = false;
101 import MetricsBase from './lib/MetricsBase';
102 import type IMetricsRequestService from './lib/types/IMetricsRequestService';
105 const addImport = (interfaceName: string, importName: string) => {
107 import type { ${interfaceName} } from '${typesDirectoryPath}/${importName}';
111 parsedFiles.forEach((file) => {
112 addImport(file.interfaceName, file.importName);
116 class Metrics extends MetricsBase {
119 const addProperty = ({ propName, type, interfaceName }: ParsedFile) => {
120 if (!includeCounterImport && type === 'Counter') {
121 includeCounterImport = true;
122 } else if (!includeHistogramImport && type === 'Histogram') {
123 includeHistogramImport = true;
127 public ${propName}: ${type}<${interfaceName}>;
131 parsedFiles.forEach((file) => {
136 constructor(requestService: IMetricsRequestService) {
137 super(requestService);
140 const initProperty = ({ propName, type, interfaceName, metricName, version }: ParsedFile) => {
142 this.${propName} = new ${type}<${interfaceName}>({ name: '${metricName}', version: ${version} }, this.requestService);
146 parsedFiles.forEach((file) => {
154 export default Metrics;
157 if (includeCounterImport) {
160 import Counter from './lib/Counter';
164 if (includeHistogramImport) {
167 import Histogram from './lib/Histogram';
174 * THIS CODE IS AUTOGENERATED
175 * using \`yarn workspace @proton/metrics generate-metrics\`
177 * For more information please consult the documentation
178 * https://confluence.protontech.ch/pages/viewpage.action?pageId=121927830
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);
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) {
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): `
222 if (answer.toLowerCase().includes('y')) {
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`
226 dedupedFiles[file.metricName] = file;
229 dedupedFiles[file.metricName] = file;
235 const code = await generateCode(Object.values(dedupedFiles));
237 await fs.writeFile('./Metrics.ts', code);