@grafana/openapi-to-k6 0.1.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 (40) hide show
  1. package/.eslintrc.json +31 -0
  2. package/.github/workflows/publish.yaml +37 -0
  3. package/.github/workflows/tests.yaml +43 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierrc +10 -0
  7. package/LICENSE.md +660 -0
  8. package/README.md +80 -0
  9. package/dist/analytics.js +80 -0
  10. package/dist/cli.js +84 -0
  11. package/dist/constants.js +21 -0
  12. package/dist/generator.js +57 -0
  13. package/dist/helper.js +198 -0
  14. package/dist/k6SdkClient.js +307 -0
  15. package/dist/logger.js +50 -0
  16. package/jest.config.js +15 -0
  17. package/package.json +56 -0
  18. package/src/analytics.ts +71 -0
  19. package/src/cli.ts +96 -0
  20. package/src/constants.ts +19 -0
  21. package/src/generator.ts +71 -0
  22. package/src/helper.ts +210 -0
  23. package/src/k6SdkClient.ts +434 -0
  24. package/src/logger.ts +61 -0
  25. package/src/type.d.ts +34 -0
  26. package/tests/e2e/k6Script.ts +77 -0
  27. package/tests/e2e/schema.json +289 -0
  28. package/tests/functional-tests/fixtures/schemas/basic_schema.json +41 -0
  29. package/tests/functional-tests/fixtures/schemas/form_data_schema.json +94 -0
  30. package/tests/functional-tests/fixtures/schemas/form_url_encoded_data_schema.json +93 -0
  31. package/tests/functional-tests/fixtures/schemas/form_url_encoded_data_with_query_params_schema.json +113 -0
  32. package/tests/functional-tests/fixtures/schemas/get_request_with_path_parameters_schema.json +66 -0
  33. package/tests/functional-tests/fixtures/schemas/headers_schema.json +163 -0
  34. package/tests/functional-tests/fixtures/schemas/no_title_schema.json +40 -0
  35. package/tests/functional-tests/fixtures/schemas/post_request_with_query_params.json +95 -0
  36. package/tests/functional-tests/fixtures/schemas/query_params_schema.json +115 -0
  37. package/tests/functional-tests/fixtures/schemas/simple_post_request_schema.json +132 -0
  38. package/tests/functional-tests/generator.test.ts +73 -0
  39. package/tests/helper.test.ts +203 -0
  40. package/tsconfig.json +17 -0
package/src/helper.ts ADDED
@@ -0,0 +1,210 @@
1
+ import { camel, getFileInfo } from '@orval/core'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { format, resolveConfig } from 'prettier'
5
+ import packageJson from '../package.json'
6
+ import { SAMPLE_K6_SCRIPT_FILE_NAME } from './constants'
7
+ import { logger } from './logger'
8
+ import { PackageDetails } from './type'
9
+
10
+ export const getPackageDetails = (): PackageDetails => {
11
+ const commandName = Object.keys(packageJson.bin)[0] || 'openapi-to-k6'
12
+ return {
13
+ name: packageJson.name,
14
+ commandName,
15
+ description: packageJson.description,
16
+ version: packageJson.version,
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Format the given file using Prettier.
22
+ *
23
+ * @param filePath - Path to the file to format.
24
+ */
25
+ export async function formatFileWithPrettier(filePath: string) {
26
+ // Read file contents
27
+ const content = fs.readFileSync(filePath, 'utf-8')
28
+ // Format using Prettier
29
+ const options = await resolveConfig(filePath)
30
+ const formatted = await format(content, {
31
+ ...options,
32
+ filepath: filePath,
33
+ })
34
+
35
+ // Write formatted content back to the file
36
+ fs.writeFileSync(filePath, formatted)
37
+ logger.debug(`Formatted: ${filePath}`)
38
+ }
39
+
40
+ /**
41
+ * Format the generated files using Prettier.
42
+ *
43
+ * @param outputTarget - Path to the generated files.
44
+ * @param schemaTitle - Title of the schema.
45
+ */
46
+ export async function formatGeneratedFiles(
47
+ outputTarget: string,
48
+ schemaTitle: string,
49
+ isSampleK6ScriptGenerated: boolean
50
+ ) {
51
+ // Here we call the original function from @orval/core used by the library to generate the
52
+ // file name with same defaults.
53
+ const { path: clientPath } = await getGeneratedClientPath(
54
+ outputTarget,
55
+ schemaTitle
56
+ )
57
+
58
+ logger.debug('Following are the details for formatting generated files:')
59
+ logger.debug(`Path: ${path}`)
60
+ logger.debug(`Schema Title: ${schemaTitle}`)
61
+ logger.debug(`Output Target: ${outputTarget}`)
62
+
63
+ await exports.formatFileWithPrettier(clientPath)
64
+
65
+ if (isSampleK6ScriptGenerated) {
66
+ const k6ScriptPath = path.join(
67
+ getDirectoryForPath(clientPath),
68
+ SAMPLE_K6_SCRIPT_FILE_NAME
69
+ )
70
+ logger.debug(`Generated sample K6 Script Path: ${k6ScriptPath}`)
71
+
72
+ if (fs.existsSync(k6ScriptPath)) {
73
+ logger.debug('Formatting sample k6 script file')
74
+ await exports.formatFileWithPrettier(k6ScriptPath)
75
+ } else {
76
+ logger.error(
77
+ 'Unable to format sample K6 script file as it does not exist!'
78
+ )
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get the path for the generated client file.
85
+ *
86
+ * @param outputTarget - Path to the generated files.
87
+ * @param schemaTitle - Title of the schema.
88
+ *
89
+ * @returns Path to the generated client file.
90
+ */
91
+
92
+ export async function getGeneratedClientPath(
93
+ outputTarget: string,
94
+ schemaTitle: string
95
+ ): Promise<{
96
+ path: string
97
+ filename: string
98
+ extension: string
99
+ }> {
100
+ const { path, filename, extension } = getFileInfo(outputTarget, {
101
+ backupFilename: camel(schemaTitle),
102
+ extension: '.ts',
103
+ })
104
+
105
+ return { path, filename, extension }
106
+ }
107
+
108
+ /**
109
+ * A singleton Class to allow redirecting stdout and stderr to a null stream.
110
+ * This is used to supress the output from third-party libraries.
111
+ *
112
+ * Note: Make sure to call restoreOutput() after the third-party library call to restore the output.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const outputOverrider = OutputOverrider.getInstance();
117
+ * outputOverrider.redirectOutputToNullStream();
118
+ * // Call the third-party library
119
+ * outputOverrider.restoreOutput();
120
+ * ```
121
+ *
122
+ * @export
123
+ * @class OutputOverrider
124
+ */
125
+ export class OutputOverrider {
126
+ private static instance: OutputOverrider | null = null
127
+ private originalStdoutWrite: any // eslint-disable-line @typescript-eslint/no-explicit-any
128
+ private originalStderrWrite: any // eslint-disable-line @typescript-eslint/no-explicit-any
129
+
130
+ // Making the constructor private to prevent direct instantiation
131
+ private constructor() {
132
+ this.originalStdoutWrite = process.stdout.write
133
+ this.originalStderrWrite = process.stderr.write
134
+ }
135
+ // Static method to get the single instance of the class
136
+ public static getInstance() {
137
+ if (OutputOverrider.instance === null) {
138
+ OutputOverrider.instance = new OutputOverrider()
139
+ }
140
+ return OutputOverrider.instance
141
+ }
142
+
143
+ public async redirectOutputToNullStream(callback?: () => Promise<void>) {
144
+ process.stdout.write = process.stderr.write = () => true
145
+
146
+ try {
147
+ if (callback) {
148
+ await callback()
149
+ }
150
+ } finally {
151
+ this._restoreOutput()
152
+ }
153
+ }
154
+
155
+ private _restoreOutput() {
156
+ process.stdout.write = this.originalStdoutWrite
157
+ process.stderr.write = this.originalStderrWrite
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Check if the current script is running with ts-node. i.e. directly from source.
163
+ *
164
+ * @export
165
+ * @returns {boolean}
166
+ */
167
+ export function isTsNode(): boolean {
168
+ const scriptPath = process.argv[1]
169
+ if (scriptPath) {
170
+ return scriptPath.endsWith('.ts') || scriptPath.includes('ts-node')
171
+ } else {
172
+ return false
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Create a hash from the given string using the djb2 algorithm.
178
+ *
179
+ * @param str
180
+ * @returns generated hash
181
+ */
182
+ export function djb2Hash(str: string): number {
183
+ let hash = 5381
184
+ for (let i = 0; i < str.length; i++) {
185
+ hash = (hash * 33) ^ str.charCodeAt(i)
186
+ }
187
+ return hash >>> 0 // Ensure the hash is a positive integer
188
+ }
189
+
190
+ /**
191
+ * Returns the directory for the given path.
192
+ *
193
+ * If the path is a file, the directory of the file is returned.
194
+ * If the path is a directory, the path itself is returned.
195
+ *
196
+ * @param path - The path to get the directory for.
197
+ *
198
+ * @returns The directory for the given path.
199
+ */
200
+ export function getDirectoryForPath(pathString: string): string {
201
+ const extensionName = path.extname(pathString)
202
+
203
+ if (!extensionName) {
204
+ // If the path does not have an extension, it is a directory
205
+ return pathString
206
+ }
207
+
208
+ // If the path has an extension, it is a file
209
+ return path.dirname(pathString)
210
+ }
@@ -0,0 +1,434 @@
1
+ import {
2
+ ClientDependenciesBuilder,
3
+ ClientExtraFilesBuilder,
4
+ ClientFooterBuilder,
5
+ ClientGeneratorsBuilder,
6
+ ClientHeaderBuilder,
7
+ ClientTitleBuilder,
8
+ ContextSpecs,
9
+ generateFormDataAndUrlEncodedFunction,
10
+ generateVerbImports,
11
+ GeneratorMutator,
12
+ GeneratorOptions,
13
+ GeneratorSchema,
14
+ GeneratorVerbOptions,
15
+ GetterBody,
16
+ GetterQueryParam,
17
+ GetterResponse,
18
+ ParamsSerializerOptions,
19
+ pascal,
20
+ sanitize,
21
+ toObjectString,
22
+ Verbs,
23
+ } from '@orval/core'
24
+ import Handlebars from 'handlebars'
25
+ import path from 'path'
26
+ import {
27
+ DEFAULT_SCHEMA_TITLE,
28
+ K6_SCRIPT_TEMPLATE,
29
+ SAMPLE_K6_SCRIPT_FILE_NAME,
30
+ } from './constants'
31
+ import { getDirectoryForPath, getGeneratedClientPath } from './helper'
32
+ import { AnalyticsData, SchemaDetails } from './type'
33
+
34
+ // A map to store the operationNames for which a return type is to be written at the end to export
35
+ // and the return type definition
36
+ const returnTypesToWrite: Map<string, string> = new Map()
37
+
38
+ export const getK6Dependencies: ClientDependenciesBuilder = () => [
39
+ {
40
+ exports: [
41
+ {
42
+ name: 'http',
43
+ default: true,
44
+ values: true,
45
+ syntheticDefaultImport: true,
46
+ },
47
+ { name: 'Response' },
48
+ { name: 'ResponseBody' },
49
+ { name: 'Params' },
50
+ ],
51
+ dependency: 'k6/http',
52
+ },
53
+ {
54
+ exports: [
55
+ {
56
+ name: 'URLSearchParams',
57
+ default: false,
58
+ values: true,
59
+ // syntheticDefaultImport: true,
60
+ },
61
+ {
62
+ name: 'URL',
63
+ default: false,
64
+ values: true,
65
+ // syntheticDefaultImport: true,
66
+ },
67
+ ],
68
+ dependency: 'https://jslib.k6.io/url/1.0.0/index.js',
69
+ },
70
+ {
71
+ exports: [
72
+ {
73
+ name: 'FormData',
74
+ default: false,
75
+ values: true,
76
+ // syntheticDefaultImport: true,
77
+ },
78
+ ],
79
+ dependency: 'https://jslib.k6.io/formdata/0.0.2/index.js',
80
+ },
81
+ ]
82
+
83
+ function getSchemaTitleFromContext(context: ContextSpecs) {
84
+ const specData = Object.values(context.specs)
85
+
86
+ if (specData[0]) {
87
+ return specData[0].info.title
88
+ }
89
+
90
+ return DEFAULT_SCHEMA_TITLE
91
+ }
92
+
93
+ function _generateResponseTypeName(operationName: string): string {
94
+ return `${pascal(operationName)}Response`
95
+ }
96
+
97
+ function _generateResponseTypeDefinition(
98
+ operationName: string,
99
+ response: GetterResponse
100
+ ): string {
101
+ const typeName = _generateResponseTypeName(operationName)
102
+ let responseDataType = ''
103
+
104
+ if (response.definition.success) {
105
+ responseDataType += response.definition.success + ' | '
106
+ }
107
+ responseDataType += 'ResponseBody'
108
+
109
+ return `export type ${typeName} = {
110
+ response: Response
111
+ data: ${responseDataType}
112
+ };`
113
+ }
114
+
115
+ const generateK6Implementation = (
116
+ {
117
+ headers,
118
+ queryParams,
119
+ operationName,
120
+ response,
121
+ body,
122
+ props,
123
+ verb,
124
+ override,
125
+ formData,
126
+ formUrlEncoded,
127
+ paramsSerializer,
128
+ }: GeneratorVerbOptions,
129
+ { route }: GeneratorOptions,
130
+ analyticsData?: AnalyticsData
131
+ ) => {
132
+ if (analyticsData) {
133
+ analyticsData.generatedRequestsCount[verb] += 1
134
+ }
135
+
136
+ const bodyForm = generateFormDataAndUrlEncodedFunction({
137
+ formData,
138
+ formUrlEncoded,
139
+ body,
140
+ isFormData: true,
141
+ isFormUrlEncoded: false,
142
+ })
143
+
144
+ // Generate response return types
145
+ returnTypesToWrite.set(
146
+ operationName,
147
+ _generateResponseTypeDefinition(operationName, response)
148
+ )
149
+
150
+ let url = `cleanBaseUrl + \`${route}\``
151
+
152
+ if (queryParams) {
153
+ url += '+`?${new URLSearchParams(params).toString()}`'
154
+ }
155
+ const urlGeneration = `const url = new URL(${url});`
156
+
157
+ const options = getK6RequestOptions({
158
+ route,
159
+ body,
160
+ headers,
161
+ queryParams,
162
+ response,
163
+ verb,
164
+ requestOptions: override?.requestOptions,
165
+ paramsSerializer,
166
+ paramsSerializerOptions: override?.paramsSerializerOptions,
167
+ })
168
+
169
+ return `const ${operationName} = (\n ${toObjectString(props, 'implementation')} requestParameters?: Params): ${_generateResponseTypeName(operationName)} => {${bodyForm}
170
+ ${urlGeneration}
171
+ const mergedRequestParameters = _mergeRequestParameters(requestParameters || {}, clientOptions.commonRequestParameters);
172
+ const response = http.request(${options});
173
+ let data;
174
+
175
+ try {
176
+ data = response.json();
177
+ } catch (error) {
178
+ data = response.body;
179
+ }
180
+ return {
181
+ response,
182
+ data
183
+ }
184
+ }
185
+ `
186
+ }
187
+
188
+ type OptionsInput = {
189
+ route: string
190
+ body: GetterBody
191
+ headers?: GetterQueryParam
192
+ queryParams?: GetterQueryParam
193
+ response: GetterResponse
194
+ verb: Verbs
195
+ requestOptions?: object | boolean
196
+ isVue?: boolean
197
+ paramsSerializer?: GeneratorMutator
198
+ paramsSerializerOptions?: ParamsSerializerOptions
199
+ }
200
+
201
+ const getParamsInputValue = ({
202
+ response,
203
+ queryParams,
204
+ headers,
205
+ body,
206
+ }: {
207
+ response: GetterResponse
208
+ body: GetterBody
209
+ queryParams?: GeneratorSchema
210
+ headers?: GeneratorSchema
211
+ }) => {
212
+ if (!queryParams && !headers && !response.isBlob && !body.contentType) {
213
+ // No parameters to merge, return the request parameters directly
214
+ return 'mergedRequestParameters'
215
+ }
216
+
217
+ let value = '\n ...mergedRequestParameters,'
218
+
219
+ if (response.isBlob) {
220
+ value += `\n responseType: 'binary',`
221
+ }
222
+ // Expand the headers
223
+ if (body.contentType || headers) {
224
+ let headersValue = `\n headers: {`
225
+ if (body.contentType) {
226
+ if (body.formData) {
227
+ headersValue += `\n'Content-Type': '${body.contentType}; boundary=' + formData.boundary,`
228
+ } else {
229
+ headersValue += `\n'Content-Type': '${body.contentType}',`
230
+ }
231
+ }
232
+
233
+ if (headers) {
234
+ headersValue += `\n...headers,`
235
+ }
236
+
237
+ headersValue += `\n...mergedRequestParameters?.headers},`
238
+ value += headersValue
239
+ }
240
+
241
+ return `{${value}}`
242
+ }
243
+
244
+ const getK6RequestOptions = (options: OptionsInput) => {
245
+ const { body, headers, queryParams, response, verb } = options
246
+
247
+ let fetchBodyOption = 'undefined'
248
+
249
+ if (body.formData) {
250
+ // Use the FormData.body() method to get the body of the request
251
+ fetchBodyOption = 'formData.body()'
252
+ } else if (body.formUrlEncoded || body.implementation) {
253
+ fetchBodyOption = `JSON.stringify(${body.implementation})`
254
+ }
255
+
256
+ // Generate the params input for the call
257
+
258
+ const paramsValue = getParamsInputValue({
259
+ response,
260
+ body,
261
+ headers: headers?.schema,
262
+ queryParams: queryParams?.schema,
263
+ })
264
+
265
+ // Sample output
266
+ // 'GET', 'http://test.com/route', <body>, <options>
267
+
268
+ return `"${verb.toUpperCase()}",
269
+ url.toString(),
270
+ ${fetchBodyOption},
271
+ ${paramsValue}`
272
+ }
273
+
274
+ function _getRequestParametersMergerFunctionImplementation() {
275
+ return `/**
276
+ * Merges the provided request parameters with default parameters for the client.
277
+ *
278
+ * @param {Params} requestParameters - The parameters provided specifically for the request
279
+ * @param {Params} commonRequestParameters - Common parameters for all requests
280
+ * @returns {Params} - The merged parameters
281
+ */
282
+ const _mergeRequestParameters = (requestParameters?: Params, commonRequestParameters?: Params): Params => {
283
+ return {
284
+ ...commonRequestParameters, // Default to common parameters
285
+ ...requestParameters, // Override with request-specific parameters
286
+ headers: {
287
+ ...commonRequestParameters?.headers || {}, // Ensure headers are defined
288
+ ...requestParameters?.headers || {},
289
+ },
290
+ cookies: {
291
+ ...commonRequestParameters?.cookies || {}, // Ensure cookies are defined
292
+ ...requestParameters?.cookies || {},
293
+ },
294
+ tags: {
295
+ ...commonRequestParameters?.tags || {}, // Ensure tags are defined
296
+ ...requestParameters?.tags || {},
297
+ },
298
+ };
299
+ };`
300
+ }
301
+
302
+ export const generateTitle: ClientTitleBuilder = (title) => {
303
+ const sanTitle = sanitize(title || DEFAULT_SCHEMA_TITLE)
304
+ return `create${pascal(sanTitle)}`
305
+ }
306
+
307
+ export const generateK6Header: ClientHeaderBuilder = ({ title }) => {
308
+ const clientOptionsTypeName = `${pascal(title)}Options`
309
+ return `
310
+ export type ${clientOptionsTypeName} = {
311
+ baseUrl: string,
312
+ commonRequestParameters?: Params
313
+ }
314
+
315
+ /**
316
+ * This is the base client to use for interacting with the API.
317
+ */
318
+ export const ${title} = (clientOptions: ${clientOptionsTypeName}) => {\n
319
+ const cleanBaseUrl = clientOptions.baseUrl.replace(/\\/+$/, '');\n`
320
+ }
321
+
322
+ export const generateFooter: ClientFooterBuilder = ({ operationNames }) => {
323
+ let footer = ''
324
+
325
+ footer += `return {${operationNames.join(',')}}};\n\n`
326
+
327
+ operationNames.forEach((operationName) => {
328
+ if (returnTypesToWrite.has(operationName)) {
329
+ footer += returnTypesToWrite.get(operationName) + '\n'
330
+ }
331
+ })
332
+
333
+ // Add function definition for merging request parameters
334
+ footer += `\n\n${_getRequestParametersMergerFunctionImplementation()}\n`
335
+
336
+ return footer
337
+ }
338
+
339
+ const k6ScriptBuilder: ClientExtraFilesBuilder = async (
340
+ verbOptions,
341
+ output,
342
+ context
343
+ ) => {
344
+ console.log(
345
+ JSON.stringify(
346
+ {
347
+ verbOptions,
348
+ output,
349
+ context,
350
+ },
351
+ null,
352
+ 2
353
+ )
354
+ )
355
+
356
+ const schemaTitle = getSchemaTitleFromContext(context)
357
+ const {
358
+ path: pathOfGeneratedClient,
359
+ filename,
360
+ extension,
361
+ } = await getGeneratedClientPath(output.target!, schemaTitle)
362
+ const directoryPath = getDirectoryForPath(pathOfGeneratedClient)
363
+ const generateScriptPath = path.join(
364
+ directoryPath,
365
+ SAMPLE_K6_SCRIPT_FILE_NAME
366
+ )
367
+
368
+ const clientFunctionsList = []
369
+
370
+ for (const verbOption of Object.values(verbOptions)) {
371
+ const { operationName, summary, props } = verbOption
372
+ const requiredProps = props.filter((prop) => prop.required)
373
+ clientFunctionsList.push({
374
+ operationName,
375
+ summary,
376
+ requiredParametersString: toObjectString(requiredProps, 'name'),
377
+ })
378
+ }
379
+
380
+ const scriptContentData = {
381
+ clientFunctionName: generateTitle(schemaTitle),
382
+ clientPath: `./${filename}${extension}`,
383
+ clientFunctionsList,
384
+ }
385
+ const template = Handlebars.compile(K6_SCRIPT_TEMPLATE)
386
+
387
+ return [
388
+ {
389
+ path: generateScriptPath,
390
+ content: template(scriptContentData),
391
+ },
392
+ ]
393
+ }
394
+
395
+ function getK6Client(
396
+ schemaDetails: SchemaDetails,
397
+ analyticsData?: AnalyticsData
398
+ ) {
399
+ return function (
400
+ verbOptions: GeneratorVerbOptions,
401
+ options: GeneratorOptions
402
+ ) {
403
+ const imports = generateVerbImports(verbOptions)
404
+ const implementation = generateK6Implementation(
405
+ verbOptions,
406
+ options,
407
+ analyticsData
408
+ )
409
+ schemaDetails.title = getSchemaTitleFromContext(options.context)
410
+ const specData = Object.values(options.context.specs)
411
+ if (specData[0]) {
412
+ if (analyticsData) {
413
+ analyticsData.openApiSpecVersion = specData[0].openapi
414
+ }
415
+ }
416
+
417
+ return { implementation, imports }
418
+ }
419
+ }
420
+
421
+ export function getK6ClientBuilder(
422
+ schemaDetails: SchemaDetails,
423
+ shouldGenerateSampleK6Script?: boolean,
424
+ analyticsData?: AnalyticsData
425
+ ): ClientGeneratorsBuilder {
426
+ return {
427
+ client: getK6Client(schemaDetails, analyticsData),
428
+ header: generateK6Header,
429
+ dependencies: getK6Dependencies,
430
+ footer: generateFooter,
431
+ title: generateTitle,
432
+ extraFiles: shouldGenerateSampleK6Script ? k6ScriptBuilder : undefined,
433
+ }
434
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,61 @@
1
+ import chalk from 'chalk'
2
+
3
+ enum LogLevel {
4
+ INFO = 'INFO',
5
+ WARNING = 'WARNING',
6
+ ERROR = 'ERROR',
7
+ DEBUG = 'DEBUG',
8
+ }
9
+
10
+ class Logger {
11
+ private static instance: Logger
12
+ private isVerbose: boolean = false
13
+
14
+ private constructor() {}
15
+
16
+ public static getInstance(): Logger {
17
+ if (!Logger.instance) {
18
+ Logger.instance = new Logger()
19
+ }
20
+ return Logger.instance
21
+ }
22
+
23
+ public setVerbose(verbose: boolean): void {
24
+ this.isVerbose = verbose
25
+ }
26
+
27
+ private logWithColor(
28
+ message: string,
29
+ level: LogLevel,
30
+ color: chalk.Chalk
31
+ ): void {
32
+ const timestamp = new Date().toISOString()
33
+ console.log(
34
+ `${color(`[${level}]`)} ${chalk.gray(`[${timestamp}]`)} ${message}`
35
+ )
36
+ }
37
+
38
+ public info(message: string): void {
39
+ this.logWithColor(message, LogLevel.INFO, chalk.blue)
40
+ }
41
+
42
+ public logMessage(message: string): void {
43
+ console.log(message)
44
+ }
45
+
46
+ public warning(message: string): void {
47
+ this.logWithColor(message, LogLevel.WARNING, chalk.yellow)
48
+ }
49
+
50
+ public error(message: string): void {
51
+ this.logWithColor(message, LogLevel.ERROR, chalk.red)
52
+ }
53
+
54
+ public debug(message: string): void {
55
+ if (this.isVerbose) {
56
+ this.logWithColor(message, LogLevel.DEBUG, chalk.green)
57
+ }
58
+ }
59
+ }
60
+
61
+ export const logger = Logger.getInstance()