From 4b227c6aa22619074abe8c2a3893d227c8903e83 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Wed, 20 Aug 2025 18:03:44 +0300 Subject: [PATCH 01/12] feat: unified CLI generating TypeScript types and error classes from OpenAPI specifications --- package-lock.json | 12 ++- package.json | 7 +- src/cli/entrypoint.mts | 78 ++++++++++++++++ src/generator/generateErrors.mts | 150 ++++++++++++++----------------- src/generator/generateTypes.mts | 58 +++++------- src/generator/index.mts | 2 + tests/errors.ts | 64 +++++++++++++ tests/test-with-errors.yaml | 59 ++++++++++++ 8 files changed, 307 insertions(+), 123 deletions(-) create mode 100644 src/cli/entrypoint.mts create mode 100644 src/generator/index.mts create mode 100644 tests/errors.ts create mode 100644 tests/test-with-errors.yaml diff --git a/package-lock.json b/package-lock.json index 0c97499..602bf25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,13 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "change-case": "^5.4.4", + "commander": "^14.0.0", "oas-normalize": "^14.1.2", "ts-essentials": "^10.1.1", "yaml": "^2.8.0" }, "bin": { - "openapi-helpers": "dist/generator/generateTypes.mjs" + "openapi-helpers": "dist/cli/entrypoint.mjs" }, "devDependencies": { "@commitlint/cli": "^19.8.1", @@ -4089,6 +4090,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/commitlint": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/commitlint/-/commitlint-19.8.1.tgz", diff --git a/package.json b/package.json index 2538a83..d97076c 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,13 @@ "./typedRequestHandler": { "default": "./dist/typedRequestHandler/typedRequestHandler.js", "types": "./dist/typedRequestHandler/typedRequestHandler.d.ts" + }, + "./generators": { + "import": "./dist/generator/index.mjs", + "types": "./dist/generator/index.d.mts" } }, - "bin": "./dist/generator/generateTypes.mjs", + "bin": "./dist/cli/entrypoint.mjs", "scripts": { "format": "prettier --check .", "format:fix": "prettier --write .", @@ -51,6 +55,7 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "change-case": "^5.4.4", + "commander": "^14.0.0", "oas-normalize": "^14.1.2", "ts-essentials": "^10.1.1", "yaml": "^2.8.0" diff --git a/src/cli/entrypoint.mts b/src/cli/entrypoint.mts new file mode 100644 index 0000000..f927426 --- /dev/null +++ b/src/cli/entrypoint.mts @@ -0,0 +1,78 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import { generateTypes } from '../generator/generateTypes.mjs'; +import { generateErrors } from '../generator/generateErrors.mjs'; +import { SchemaObject, TransformNodeOptions, TransformObject } from 'openapi-typescript'; +import { TypeNode } from 'typescript'; + +const program = new Command(); + +program.name('openapi-helpers').description('Generate TypeScript types and error classes from OpenAPI specifications').version('3.1.0'); + +program + .command('types') + .description('Generate TypeScript types from OpenAPI spec') + .argument('', 'Path to the OpenAPI specification file') + .argument('', 'Path where the generated types will be saved') + .option('-f, --format', 'Format the generated code using Prettier') + .option('-t, --add-typed-request-handler', 'Add typed request handler types to the generated output') + .option('-i, --inject', 'Inject additional code into the generated output') + .option('-tm, --transform', 'Add transformation to the generated output') + .action( + async ( + openapiPath: string, + destinationPath: string, + options: { + format?: boolean; + addTypedRequestHandler?: boolean; + inject?: string; + transform?: (schemaObject: SchemaObject, metadata: TransformNodeOptions) => TypeNode | TransformObject | undefined; + } + ) => { + try { + await generateTypes( + openapiPath, + destinationPath, + options.format === true, + options.addTypedRequestHandler === true, + options.inject, + options.transform + ); + console.log('Types generated successfully'); + } catch (error) { + console.error('Error generating types:', error); + process.exit(1); + } + } + ); + +program + .command('errors') + .description('Generate error classes from OpenAPI spec') + .argument('', 'Path to the OpenAPI specification file') + .argument('', 'Path where the generated error classes will be saved') + .option('-f, --format', 'Format the generated code using Prettier') + .action(async (openapiPath: string, destinationPath: string, options: { format?: boolean }) => { + try { + await generateErrors(openapiPath, destinationPath, options.format === true); + console.log('Errors generated successfully'); + } catch (error) { + console.error('Error generating errors:', error); + process.exit(1); + } + }); + +// Add examples to the help +program.addHelpText( + 'after', + ` +Examples: + $ openapi-helpers types api.yaml types.ts + $ openapi-helpers errors api.yaml errors.ts --format + $ openapi-helpers types api.yaml types.ts --add-typed-request-handler --format + $ openapi-helpers --help + $ openapi-helpers types --help +` +); + +program.parse(); diff --git a/src/generator/generateErrors.mts b/src/generator/generateErrors.mts index 34a9b71..432004d 100644 --- a/src/generator/generateErrors.mts +++ b/src/generator/generateErrors.mts @@ -1,83 +1,63 @@ import fs from 'node:fs/promises'; -import { parseArgs } from 'node:util'; import path from 'node:path'; import { dereference } from '@apidevtools/json-schema-ref-parser'; import { format, resolveConfig } from 'prettier'; import * as changeCase from 'change-case'; import type { OpenAPI3, OperationObject, ResponseObject, SchemaObject } from 'openapi-typescript'; -const ARGS_SLICE = 2; +export async function generateErrors(openapiPath: string, destinationPath: string, shouldFormat: boolean): Promise { + const openapi = await dereference(openapiPath); -const { - values: { format: shouldFormat }, - positionals, -} = parseArgs({ - args: process.argv.slice(ARGS_SLICE), - options: { - format: { type: 'boolean', alias: 'f' }, - }, - allowPositionals: true, -}); - -const [openapiPath, destinationPath] = positionals; - -if (openapiPath === undefined || destinationPath === undefined) { - console.error('Usage: generateErrors '); - process.exit(1); -} - -const openapi = await dereference(openapiPath); - -if (openapi.paths === undefined) { - console.error('No paths found in the OpenAPI document.'); - process.exit(1); -} + if (openapi.paths === undefined) { + console.error('No paths found in the OpenAPI document.'); + process.exit(1); + } -const errorCodes = new Set(); + const errorCodes = new Set(); -function extractCodeFromSchema(schema: SchemaObject): void { - // Handle direct code property - if (schema.type === 'object' && schema.properties?.code) { - const codeProperty = schema.properties.code as SchemaObject; + function extractCodeFromSchema(schema: SchemaObject): void { + // Handle direct code property + if (schema.type === 'object' && schema.properties?.code) { + const codeProperty = schema.properties.code as SchemaObject; - // Handle enum values - if (codeProperty.enum) { - codeProperty.enum.map(String).forEach((code) => { - errorCodes.add(code); - }); + // Handle enum values + if (codeProperty.enum) { + codeProperty.enum.map(String).forEach((code) => { + errorCodes.add(code); + }); + } } - } - // Handle allOf combinations - if (schema.allOf) { - for (const subSchema of schema.allOf) { - extractCodeFromSchema(subSchema as SchemaObject); + // Handle allOf combinations + if (schema.allOf) { + for (const subSchema of schema.allOf) { + extractCodeFromSchema(subSchema as SchemaObject); + } } - } - // Handle oneOf combinations - if (schema.oneOf) { - for (const subSchema of schema.oneOf) { - extractCodeFromSchema(subSchema as SchemaObject); + // Handle oneOf combinations + if (schema.oneOf) { + for (const subSchema of schema.oneOf) { + extractCodeFromSchema(subSchema as SchemaObject); + } } - } - // Handle anyOf combinations - if (schema.anyOf) { - for (const subSchema of schema.anyOf) { - extractCodeFromSchema(subSchema as SchemaObject); + // Handle anyOf combinations + if (schema.anyOf) { + for (const subSchema of schema.anyOf) { + extractCodeFromSchema(subSchema as SchemaObject); + } } } -} -function createError(code: string): string { - let className = changeCase.pascalCase(code); + function createError(code: string): string { + let className = changeCase.pascalCase(code); - if (!className.endsWith('Error')) { - className += 'Error'; - } + if (!className.endsWith('Error')) { + className += 'Error'; + } - return `export class ${className} extends Error { + return `export class ${className} extends Error { public readonly code = '${code}'; /** * Creates an instance of ${className}. @@ -89,41 +69,41 @@ function createError(code: string): string { Object.setPrototypeOf(this, new.target.prototype); } };\n`; -} - -for (const [, methods] of Object.entries(openapi.paths)) { - for (const [key, operation] of Object.entries(methods) as [string, OperationObject][]) { - if (['servers', 'parameters'].includes(key)) { - continue; - } + } - for (const [statusCode, response] of Object.entries(operation.responses ?? {}) as [string, ResponseObject][]) { - if (statusCode.startsWith('2') || statusCode.startsWith('3')) { - continue; // Skip successful and redirection responses + for (const [, methods] of Object.entries(openapi.paths)) { + for (const [key, operation] of Object.entries(methods) as [string, OperationObject][]) { + if (['servers', 'parameters'].includes(key)) { + continue; } - const schema = response.content?.['application/json']?.schema as SchemaObject | undefined; - if (schema) { - extractCodeFromSchema(schema); + for (const [statusCode, response] of Object.entries(operation.responses ?? {}) as [string, ResponseObject][]) { + if (statusCode.startsWith('2') || statusCode.startsWith('3')) { + continue; // Skip successful and redirection responses + } + + const schema = response.content?.['application/json']?.schema as SchemaObject | undefined; + if (schema) { + extractCodeFromSchema(schema); + } } } } -} - -if (errorCodes.size === 0) { - console.warn('No error codes found in the OpenAPI document.'); - process.exit(0); -} -let errorFile = errorCodes.values().map(createError).toArray().join('\n'); + if (errorCodes.size === 0) { + console.warn('No error codes found in the OpenAPI document.'); + process.exit(0); + } -if (shouldFormat === true) { - const prettierOptions = await resolveConfig('./src/index.ts'); + let errorFile = errorCodes.values().map(createError).toArray().join('\n'); - errorFile = await format(errorFile, { ...prettierOptions, parser: 'typescript' }); -} + if (shouldFormat) { + const prettierOptions = await resolveConfig('./src/index.ts'); + errorFile = await format(errorFile, { ...prettierOptions, parser: 'typescript' }); + } -const directory = path.dirname(destinationPath); -await fs.mkdir(directory, { recursive: true }); + const directory = path.dirname(destinationPath); + await fs.mkdir(directory, { recursive: true }); -await fs.writeFile(destinationPath, errorFile); + await fs.writeFile(destinationPath, errorFile); +} diff --git a/src/generator/generateTypes.mts b/src/generator/generateTypes.mts index 3fc1579..05e738e 100644 --- a/src/generator/generateTypes.mts +++ b/src/generator/generateTypes.mts @@ -1,29 +1,8 @@ #!/usr/bin/env node import fs from 'node:fs/promises'; -import { parseArgs } from 'node:util'; import { format, resolveConfig } from 'prettier'; -import openapiTS, { astToString } from 'openapi-typescript'; - -const ARGS_SLICE = 2; - -const { - values: { format: shouldFormat, 'add-typed-request-handler': addTypedRequestHandler }, - positionals, -} = parseArgs({ - args: process.argv.slice(ARGS_SLICE), - options: { - format: { type: 'boolean', alias: 'f' }, - 'add-typed-request-handler': { type: 'boolean', alias: 't' }, - }, - allowPositionals: true, -}); - -const [openapiPath, destinationPath] = positionals; - -if (openapiPath === undefined || destinationPath === undefined) { - console.error('Usage: generateTypes '); - process.exit(1); -} +import openapiTS, { astToString, SchemaObject, TransformNodeOptions, TransformObject } from 'openapi-typescript'; +import { TypeNode } from 'typescript'; const ESLINT_DISABLE = '/* eslint-disable */\n'; @@ -31,22 +10,29 @@ const typedRequestHandlerImport = "import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-helpers/typedRequestHandler';\n"; const exportTypedRequestHandlers = 'export type TypedRequestHandlers = ImportedTypedRequestHandlers;\n'; -const ast = await openapiTS(await fs.readFile(openapiPath, 'utf-8'), { exportType: true }); +export async function generateTypes( + openapiPath: string, + destinationPath: string, + shouldFormat: boolean, + addTypedRequestHandler: boolean, + inject?: string, + transform?: (schemaObject: SchemaObject, metadata: TransformNodeOptions) => TypeNode | TransformObject | undefined +): Promise { + const ast = await openapiTS(await fs.readFile(openapiPath, 'utf-8'), { exportType: true, inject, transform }); -let content = astToString(ast); + let content = astToString(ast); -if (addTypedRequestHandler === true) { - content = typedRequestHandlerImport + content + exportTypedRequestHandlers; -} + if (addTypedRequestHandler) { + content = typedRequestHandlerImport + content + exportTypedRequestHandlers; + } -content = ESLINT_DISABLE + content; + content = ESLINT_DISABLE + content; -if (shouldFormat === true) { - const prettierOptions = await resolveConfig('./src/index.ts'); + if (shouldFormat) { + const prettierOptions = await resolveConfig('./src/index.ts'); - content = await format(content, { ...prettierOptions, parser: 'typescript' }); -} + content = await format(content, { ...prettierOptions, parser: 'typescript' }); + } -await fs.writeFile(destinationPath, content); - -console.log('Types generated successfully'); + await fs.writeFile(destinationPath, content); +} diff --git a/src/generator/index.mts b/src/generator/index.mts new file mode 100644 index 0000000..071382e --- /dev/null +++ b/src/generator/index.mts @@ -0,0 +1,2 @@ +export * from './generateTypes.mjs'; +export * from './generateErrors.mjs'; diff --git a/tests/errors.ts b/tests/errors.ts new file mode 100644 index 0000000..deacb14 --- /dev/null +++ b/tests/errors.ts @@ -0,0 +1,64 @@ +export class InvalidInputError extends Error { + public readonly code = 'INVALID_INPUT'; + /** + * Creates an instance of InvalidInputError. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class MissingParameterError extends Error { + public readonly code = 'MISSING_PARAMETER'; + /** + * Creates an instance of MissingParameterError. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class ResourceNotFoundError extends Error { + public readonly code = 'RESOURCE_NOT_FOUND'; + /** + * Creates an instance of ResourceNotFoundError. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InternalError extends Error { + public readonly code = 'INTERNAL_ERROR'; + /** + * Creates an instance of InternalError. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class DatabaseError extends Error { + public readonly code = 'DATABASE_ERROR'; + /** + * Creates an instance of DatabaseError. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/tests/test-with-errors.yaml b/tests/test-with-errors.yaml new file mode 100644 index 0000000..bab5c66 --- /dev/null +++ b/tests/test-with-errors.yaml @@ -0,0 +1,59 @@ +openapi: 3.0.0 +info: + title: Test API with Errors + version: 1.0.0 +paths: + /test: + get: + operationId: testOperation + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + message: + type: string + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + code: + type: string + enum: + - INVALID_INPUT + - MISSING_PARAMETER + message: + type: string + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + code: + type: string + enum: + - RESOURCE_NOT_FOUND + message: + type: string + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + code: + type: string + enum: + - INTERNAL_ERROR + - DATABASE_ERROR + message: + type: string From 0c914988ee578220b41272c89a1de171dfac15c2 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Thu, 21 Aug 2025 11:35:20 +0300 Subject: [PATCH 02/12] feat: enhance CLI for generating TypeScript types and error classes from OpenAPI specifications --- README.md | 92 ++++++++++- package-lock.json | 266 +++++++++++++++++++++++++++++++ package.json | 2 + src/cli/entrypoint.mts | 56 ++++--- src/generator/generateErrors.mts | 75 ++++++--- src/generator/generateTypes.mts | 4 +- tests/errors.ts | 154 ++++++++++-------- 7 files changed, 529 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 0153e6f..684adc8 100644 --- a/README.md +++ b/README.md @@ -9,22 +9,98 @@ Run the following commands: npm install --save-dev @map-colonies/openapi-helpers supertest prettier openapi-typescript @types/express ``` -## types-generator -The package contains a script that wraps the `openapi-typescript` package and generates types for the OpenAPI schema. The script also formats the generated types using `prettier`. -The command structure is as follows: +## CLI Usage + +The package provides a unified CLI for generating TypeScript types and error classes from OpenAPI specifications. All code generation is now performed using the `generate` command, which supports subcommands for types and errors. + +### Generate Types + +Generate TypeScript types from an OpenAPI schema: +```bash +npx @map-colonies/openapi-helpers generate types [options] +``` + +For example: +```bash +npx @map-colonies/openapi-helpers generate types ./openapi3.yaml ./src/openapi.d.ts --format --add-typed-request-handler +``` + +Options: +- `--format` - Format the generated types using `prettier`. +- `--add-typed-request-handler` - Add the `TypedRequestHandler` type to the generated types. + +### Generate Errors + +Generate error classes and error code mappings from an OpenAPI schema: ```bash -npx @map-colonies/openapi-helpers --format --add-typed-request-handler +npx @map-colonies/openapi-helpers generate errors [options] ``` For example: ```bash -npx @map-colonies/openapi-helpers ./openapi3.yaml ./src/openapi.d.ts --format --add-typed-request-handler +npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.ts --format ``` -### options -- `--format` - format the generated types using `prettier`. -- `--add-typed-request-handler` - add the `TypedRequestHandler` type to the generated types. +Options: +- `--format` - Format the generated code using `prettier`. +- `--no-mapping` - Disable the generation of error code mappings. +- `--no-error-classes` - Disable the generation of error classes. + +### Help and Examples + +To see all available commands and options: +```bash +npx @map-colonies/openapi-helpers --help +npx @map-colonies/openapi-helpers generate --help +npx @map-colonies/openapi-helpers generate types --help +npx @map-colonies/openapi-helpers generate errors --help +``` + +#### Example: Run all generations + +You can run both types and errors generation in sequence: +```bash +npx @map-colonies/openapi-helpers generate types ./openapi3.yaml ./src/openapi.d.ts --format --add-typed-request-handler +npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.ts --format +``` + + + +## Functional Programming Support + +The code generators (`generateTypes.mts` and `generateErrors.mts`) now support functional programming patterns. You can inject custom transformation logic or AST manipulation by providing functional arguments, making the generators more flexible and composable for advanced use cases. + +### Programmatic Usage + +You can import and use the generators directly in your own scripts for full functional programming flexibility: + +```typescript +import { generateTypes, generateErrors } from '@map-colonies/openapi-helpers/generators'; + +// Generate types +await generateTypes( + 'openapi3.yaml', + 'src/openapi.d.ts', + true, // shouldFormat + true, // addTypedRequestHandler + /* inject? */ undefined, + /* transform? */ undefined // or provide a custom transform function +); + +// Generate errors +await generateErrors( + 'openapi3.yaml', + 'src/errors.ts', + true, // shouldFormat + true, // includeMapping + true // includeErrorClasses +); +``` + +You can pass custom `inject` or `transform` functions to `generateTypes` for advanced AST/code manipulation, enabling highly composable and functional workflows. + +---- ## TypedRequestHandler The package contains a wrapper for the `express` types package that provides autocomplete for all the request handlers to the API based on the OpenAPI schema. The TypedRequestHandler is initialized with the types generated by `openapi-typescript`, and is configured based on operation name or method and path. diff --git a/package-lock.json b/package-lock.json index 602bf25..04d5a25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", + "@commander-js/extra-typings": "^14.0.0", "change-case": "^5.4.4", "commander": "^14.0.0", "oas-normalize": "^14.1.2", + "ora": "^8.2.0", "ts-essentials": "^10.1.1", "yaml": "^2.8.0" }, @@ -625,6 +627,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@commander-js/extra-typings": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", + "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==", + "license": "MIT", + "peerDependencies": { + "commander": "~14.0.0" + } + }, "node_modules/@commitlint/cli": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", @@ -4024,6 +4035,33 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5315,6 +5353,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5798,6 +5848,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5850,6 +5912,18 @@ "node": ">=8" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6983,6 +7057,46 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", @@ -7187,6 +7301,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7576,6 +7702,91 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8149,6 +8360,49 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -8612,6 +8866,18 @@ "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index d97076c..a0dcb92 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,11 @@ "homepage": "https://github.com/MapColonies/openapi-helpers#readme", "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", + "@commander-js/extra-typings": "^14.0.0", "change-case": "^5.4.4", "commander": "^14.0.0", "oas-normalize": "^14.1.2", + "ora": "^8.2.0", "ts-essentials": "^10.1.1", "yaml": "^2.8.0" }, diff --git a/src/cli/entrypoint.mts b/src/cli/entrypoint.mts index f927426..b15a5f7 100644 --- a/src/cli/entrypoint.mts +++ b/src/cli/entrypoint.mts @@ -1,23 +1,22 @@ #!/usr/bin/env node -import { Command } from 'commander'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { program } from '@commander-js/extra-typings'; import { generateTypes } from '../generator/generateTypes.mjs'; import { generateErrors } from '../generator/generateErrors.mjs'; -import { SchemaObject, TransformNodeOptions, TransformObject } from 'openapi-typescript'; -import { TypeNode } from 'typescript'; - -const program = new Command(); +import ora from 'ora'; +const SECOND = 1000; program.name('openapi-helpers').description('Generate TypeScript types and error classes from OpenAPI specifications').version('3.1.0'); -program +const command = program.command('generate').description('Generate code artifacts (types, error classes) from OpenAPI specifications'); + +command .command('types') .description('Generate TypeScript types from OpenAPI spec') .argument('', 'Path to the OpenAPI specification file') .argument('', 'Path where the generated types will be saved') .option('-f, --format', 'Format the generated code using Prettier') .option('-t, --add-typed-request-handler', 'Add typed request handler types to the generated output') - .option('-i, --inject', 'Inject additional code into the generated output') - .option('-tm, --transform', 'Add transformation to the generated output') .action( async ( openapiPath: string, @@ -25,19 +24,13 @@ program options: { format?: boolean; addTypedRequestHandler?: boolean; - inject?: string; - transform?: (schemaObject: SchemaObject, metadata: TransformNodeOptions) => TypeNode | TransformObject | undefined; } ) => { try { - await generateTypes( - openapiPath, - destinationPath, - options.format === true, - options.addTypedRequestHandler === true, - options.inject, - options.transform - ); + const spinner = ora('Generating types').start(); + await generateTypes(openapiPath, destinationPath, options.format === true, options.addTypedRequestHandler === true); + await sleep(SECOND); + spinner.stop(); console.log('Types generated successfully'); } catch (error) { console.error('Error generating types:', error); @@ -46,15 +39,20 @@ program } ); -program +command .command('errors') .description('Generate error classes from OpenAPI spec') .argument('', 'Path to the OpenAPI specification file') .argument('', 'Path where the generated error classes will be saved') .option('-f, --format', 'Format the generated code using Prettier') - .action(async (openapiPath: string, destinationPath: string, options: { format?: boolean }) => { + .option('-m, --no-mapping', 'Disable the generation of error code mappings') + .option('-e, --no-error-classes', 'Disable the generation of error classes') + .action(async (openapiPath: string, destinationPath: string, options) => { try { - await generateErrors(openapiPath, destinationPath, options.format === true); + const spinner = ora('Generating errors').start(); + await generateErrors(openapiPath, destinationPath, options.format === true, options.mapping, options.errorClasses); + await sleep(SECOND); + spinner.stop(); console.log('Errors generated successfully'); } catch (error) { console.error('Error generating errors:', error); @@ -67,11 +65,19 @@ program.addHelpText( 'after', ` Examples: - $ openapi-helpers types api.yaml types.ts - $ openapi-helpers errors api.yaml errors.ts --format - $ openapi-helpers types api.yaml types.ts --add-typed-request-handler --format + $ openapi-helpers generate types api.yaml types.ts + $ openapi-helpers generate types api.yaml types.ts --format + $ openapi-helpers generate types api.yaml types.ts --add-typed-request-handler + $ openapi-helpers generate types api.yaml types.ts --add-typed-request-handler --format + $ openapi-helpers generate errors api.yaml errors.ts + $ openapi-helpers generate errors api.yaml errors.ts --format + $ openapi-helpers generate errors api.yaml errors.ts --no-mapping + $ openapi-helpers generate errors api.yaml errors.ts --no-error-classes + $ openapi-helpers generate errors api.yaml errors.ts --no-mapping --no-error-classes $ openapi-helpers --help - $ openapi-helpers types --help + $ openapi-helpers generate --help + $ openapi-helpers generate types --help + $ openapi-helpers generate errors --help ` ); diff --git a/src/generator/generateErrors.mts b/src/generator/generateErrors.mts index 432004d..d24c670 100644 --- a/src/generator/generateErrors.mts +++ b/src/generator/generateErrors.mts @@ -5,7 +5,50 @@ import { format, resolveConfig } from 'prettier'; import * as changeCase from 'change-case'; import type { OpenAPI3, OperationObject, ResponseObject, SchemaObject } from 'openapi-typescript'; -export async function generateErrors(openapiPath: string, destinationPath: string, shouldFormat: boolean): Promise { +const ESLINT_DISABLE = '/* eslint-disable */\n'; +const FILE_HEADER = `${ESLINT_DISABLE}// This file was auto-generated. Do not edit manually. +// To update, run the error generation script again.\n\n`; + +function createError(code: string): string { + let className = changeCase.pascalCase(code); + + if (!className.endsWith('Error')) { + className += 'Error'; + } + + return `export class ${className} extends Error { + public readonly code = '${code}'; + /** + * Creates an instance of ${className}. + * @param message - The error message. + * @param cause - Optional original error or server response data. + */ + public constructor(message: string, cause?: unknown) { + super(message, { cause }); + Object.setPrototypeOf(this, new.target.prototype); + } +};\n`; +} + +function buildErrorMapping(errorCodes: Set): string { + return errorCodes + .values() + .map((code) => `'${code}': '${code}'`) + .reduce((acc, curr) => `${acc}, ${curr}`); +} + +export async function generateErrors( + openapiPath: string, + destinationPath: string, + shouldFormat: boolean, + includeMapping?: boolean, + includeErrorClasses?: boolean +): Promise { + if (includeMapping !== true && includeErrorClasses !== true) { + console.error('No mapping and no error classes generation is enabled. Exiting...'); + process.exit(1); + } + const openapi = await dereference(openapiPath); if (openapi.paths === undefined) { @@ -50,27 +93,6 @@ export async function generateErrors(openapiPath: string, destinationPath: strin } } - function createError(code: string): string { - let className = changeCase.pascalCase(code); - - if (!className.endsWith('Error')) { - className += 'Error'; - } - - return `export class ${className} extends Error { - public readonly code = '${code}'; - /** - * Creates an instance of ${className}. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } -};\n`; - } - for (const [, methods] of Object.entries(openapi.paths)) { for (const [key, operation] of Object.entries(methods) as [string, OperationObject][]) { if (['servers', 'parameters'].includes(key)) { @@ -94,8 +116,15 @@ export async function generateErrors(openapiPath: string, destinationPath: strin console.warn('No error codes found in the OpenAPI document.'); process.exit(0); } + let errorFile = FILE_HEADER; - let errorFile = errorCodes.values().map(createError).toArray().join('\n'); + if (includeErrorClasses === true) { + errorFile += errorCodes.values().map(createError).toArray().join('\n'); + } + + if (includeMapping === true) { + errorFile += ` export const API_ERRORS_MAP = { ${buildErrorMapping(errorCodes)} } as const;\n`; + } if (shouldFormat) { const prettierOptions = await resolveConfig('./src/index.ts'); diff --git a/src/generator/generateTypes.mts b/src/generator/generateTypes.mts index 05e738e..bb8a938 100644 --- a/src/generator/generateTypes.mts +++ b/src/generator/generateTypes.mts @@ -5,6 +5,8 @@ import openapiTS, { astToString, SchemaObject, TransformNodeOptions, TransformOb import { TypeNode } from 'typescript'; const ESLINT_DISABLE = '/* eslint-disable */\n'; +const FILE_HEADER = `${ESLINT_DISABLE}// This file was auto-generated. Do not edit manually. +// To update, run the error generation script again.\n\n`; const typedRequestHandlerImport = "import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-helpers/typedRequestHandler';\n"; @@ -26,7 +28,7 @@ export async function generateTypes( content = typedRequestHandlerImport + content + exportTypedRequestHandlers; } - content = ESLINT_DISABLE + content; + content = FILE_HEADER + content; if (shouldFormat) { const prettierOptions = await resolveConfig('./src/index.ts'); diff --git a/tests/errors.ts b/tests/errors.ts index deacb14..fee527f 100644 --- a/tests/errors.ts +++ b/tests/errors.ts @@ -1,64 +1,92 @@ -export class InvalidInputError extends Error { - public readonly code = 'INVALID_INPUT'; - /** - * Creates an instance of InvalidInputError. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class MissingParameterError extends Error { - public readonly code = 'MISSING_PARAMETER'; - /** - * Creates an instance of MissingParameterError. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class ResourceNotFoundError extends Error { - public readonly code = 'RESOURCE_NOT_FOUND'; - /** - * Creates an instance of ResourceNotFoundError. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class InternalError extends Error { - public readonly code = 'INTERNAL_ERROR'; - /** - * Creates an instance of InternalError. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class DatabaseError extends Error { - public readonly code = 'DATABASE_ERROR'; - /** - * Creates an instance of DatabaseError. - * @param message - The error message. - * @param cause - Optional original error or server response data. - */ - public constructor(message: string, cause?: unknown) { - super(message, { cause }); - Object.setPrototypeOf(this, new.target.prototype); - } +/* eslint-disable */ +export type paths = { + '/test': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['testOperation']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +}; +export type webhooks = Record; +export type components = { + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +}; +export type $defs = Record; +export interface operations { + testOperation: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + message?: string; + }; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {string} */ + code?: 'INVALID_INPUT' | 'MISSING_PARAMETER'; + message?: string; + }; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {string} */ + code?: 'RESOURCE_NOT_FOUND'; + message?: string; + }; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {string} */ + code?: 'INTERNAL_ERROR' | 'DATABASE_ERROR'; + message?: string; + }; + }; + }; + }; + }; } From fe34f001716177e9e108b2a3d008734e40e6ace3 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Thu, 21 Aug 2025 11:38:49 +0300 Subject: [PATCH 03/12] chore: add prepack script to build before packaging --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a0dcb92..619a7c8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "generate:test:types": "npm run build && node dist/generator/generateTypes.mjs tests/openapi3.yaml tests/types.d.ts", "clean": "rimraf dist", "prepublish": "npm run build", + "prepack": "npm run build", "prepare": "husky", "docs": "typedoc" }, From d45d125d3b529c7d12cfbcec949c4c05c87f29be Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Thu, 21 Aug 2025 13:28:32 +0300 Subject: [PATCH 04/12] feat: add error output options for generate command in CLI --- src/cli/entrypoint.mts | 19 +++++++-- tests/errors.ts | 92 ------------------------------------------ 2 files changed, 16 insertions(+), 95 deletions(-) delete mode 100644 tests/errors.ts diff --git a/src/cli/entrypoint.mts b/src/cli/entrypoint.mts index b15a5f7..cedcb79 100644 --- a/src/cli/entrypoint.mts +++ b/src/cli/entrypoint.mts @@ -5,6 +5,13 @@ import { generateTypes } from '../generator/generateTypes.mjs'; import { generateErrors } from '../generator/generateErrors.mjs'; import ora from 'ora'; +const errorOutput = ['all', 'map', 'classes'] as const; +type ErrorsOutput = (typeof errorOutput)[number]; + +function isErrorsOutput(value: string): value is ErrorsOutput { + return errorOutput.includes(value as ErrorsOutput); +} + const SECOND = 1000; program.name('openapi-helpers').description('Generate TypeScript types and error classes from OpenAPI specifications').version('3.1.0'); @@ -45,12 +52,18 @@ command .argument('', 'Path to the OpenAPI specification file') .argument('', 'Path where the generated error classes will be saved') .option('-f, --format', 'Format the generated code using Prettier') - .option('-m, --no-mapping', 'Disable the generation of error code mappings') - .option('-e, --no-error-classes', 'Disable the generation of error classes') + .option('-e, --errors-output ', 'Specify the errors output type', 'all') .action(async (openapiPath: string, destinationPath: string, options) => { try { + if (!isErrorsOutput(options.errorsOutput)) { + console.error(`Invalid errors output type: ${options.errorsOutput}`); + process.exit(1); + } + const includeMapping = options.errorsOutput === 'map' || options.errorsOutput === 'all'; + const includeErrorClasses = options.errorsOutput === 'classes' || options.errorsOutput === 'all'; + const spinner = ora('Generating errors').start(); - await generateErrors(openapiPath, destinationPath, options.format === true, options.mapping, options.errorClasses); + await generateErrors(openapiPath, destinationPath, options.format === true, includeMapping, includeErrorClasses); await sleep(SECOND); spinner.stop(); console.log('Errors generated successfully'); diff --git a/tests/errors.ts b/tests/errors.ts deleted file mode 100644 index fee527f..0000000 --- a/tests/errors.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable */ -export type paths = { - '/test': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['testOperation']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -}; -export type webhooks = Record; -export type components = { - schemas: never; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -}; -export type $defs = Record; -export interface operations { - testOperation: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - message?: string; - }; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {string} */ - code?: 'INVALID_INPUT' | 'MISSING_PARAMETER'; - message?: string; - }; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {string} */ - code?: 'RESOURCE_NOT_FOUND'; - message?: string; - }; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {string} */ - code?: 'INTERNAL_ERROR' | 'DATABASE_ERROR'; - message?: string; - }; - }; - }; - }; - }; -} From d0a40d72a56d567f57e1f6bf4b17de8b3f069a95 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Thu, 21 Aug 2025 14:01:07 +0300 Subject: [PATCH 05/12] feat: update CLI options for error generation and refactor generator functions --- README.md | 29 +++++++++++++++++++---------- src/cli/entrypoint.mts | 11 +++++++++-- src/generator/generateErrors.mts | 19 ++++++++----------- src/generator/generateTypes.mts | 16 +++++++++------- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 684adc8..2b1f02c 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,13 @@ For example: npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.ts --format ``` + Options: - `--format` - Format the generated code using `prettier`. -- `--no-mapping` - Disable the generation of error code mappings. -- `--no-error-classes` - Disable the generation of error classes. +- `--errors-output ` - Specify what to generate: + - `all` (default): generate both error classes and error code mapping + - `map`: generate only the error code mapping + - `classes`: generate only the error classes ### Help and Examples @@ -57,12 +60,13 @@ npx @map-colonies/openapi-helpers generate types --help npx @map-colonies/openapi-helpers generate errors --help ``` + #### Example: Run all generations You can run both types and errors generation in sequence: ```bash npx @map-colonies/openapi-helpers generate types ./openapi3.yaml ./src/openapi.d.ts --format --add-typed-request-handler -npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.ts --format +npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.ts --format --errors-output all ``` @@ -73,6 +77,7 @@ The code generators (`generateTypes.mts` and `generateErrors.mts`) now support f ### Programmatic Usage + You can import and use the generators directly in your own scripts for full functional programming flexibility: ```typescript @@ -82,19 +87,23 @@ import { generateTypes, generateErrors } from '@map-colonies/openapi-helpers/gen await generateTypes( 'openapi3.yaml', 'src/openapi.d.ts', - true, // shouldFormat - true, // addTypedRequestHandler - /* inject? */ undefined, - /* transform? */ undefined // or provide a custom transform function + { + shouldFormat: true, + addTypedRequestHandler: true, + // inject?: string, + // transform?: (schemaObject, metadata) => ... + } ); // Generate errors await generateErrors( 'openapi3.yaml', 'src/errors.ts', - true, // shouldFormat - true, // includeMapping - true // includeErrorClasses + { + shouldFormat: true, + includeMapping: true, + includeErrorClasses: true + } ); ``` diff --git a/src/cli/entrypoint.mts b/src/cli/entrypoint.mts index cedcb79..653d78c 100644 --- a/src/cli/entrypoint.mts +++ b/src/cli/entrypoint.mts @@ -35,7 +35,7 @@ command ) => { try { const spinner = ora('Generating types').start(); - await generateTypes(openapiPath, destinationPath, options.format === true, options.addTypedRequestHandler === true); + await generateTypes(openapiPath, destinationPath, { shouldFormat: options.format, addTypedRequestHandler: options.addTypedRequestHandler }); await sleep(SECOND); spinner.stop(); console.log('Types generated successfully'); @@ -63,9 +63,16 @@ command const includeErrorClasses = options.errorsOutput === 'classes' || options.errorsOutput === 'all'; const spinner = ora('Generating errors').start(); - await generateErrors(openapiPath, destinationPath, options.format === true, includeMapping, includeErrorClasses); + + await generateErrors(openapiPath, destinationPath, { + shouldFormat: options.format, + includeMapping, + includeErrorClasses, + }); + await sleep(SECOND); spinner.stop(); + console.log('Errors generated successfully'); } catch (error) { console.error('Error generating errors:', error); diff --git a/src/generator/generateErrors.mts b/src/generator/generateErrors.mts index d24c670..2c08c5d 100644 --- a/src/generator/generateErrors.mts +++ b/src/generator/generateErrors.mts @@ -40,15 +40,12 @@ function buildErrorMapping(errorCodes: Set): string { export async function generateErrors( openapiPath: string, destinationPath: string, - shouldFormat: boolean, - includeMapping?: boolean, - includeErrorClasses?: boolean -): Promise { - if (includeMapping !== true && includeErrorClasses !== true) { - console.error('No mapping and no error classes generation is enabled. Exiting...'); - process.exit(1); + options: { + shouldFormat?: boolean; + includeMapping?: boolean; + includeErrorClasses?: boolean; } - +): Promise { const openapi = await dereference(openapiPath); if (openapi.paths === undefined) { @@ -118,15 +115,15 @@ export async function generateErrors( } let errorFile = FILE_HEADER; - if (includeErrorClasses === true) { + if (options.includeErrorClasses === true) { errorFile += errorCodes.values().map(createError).toArray().join('\n'); } - if (includeMapping === true) { + if (options.includeMapping === true) { errorFile += ` export const API_ERRORS_MAP = { ${buildErrorMapping(errorCodes)} } as const;\n`; } - if (shouldFormat) { + if (options.shouldFormat === true) { const prettierOptions = await resolveConfig('./src/index.ts'); errorFile = await format(errorFile, { ...prettierOptions, parser: 'typescript' }); } diff --git a/src/generator/generateTypes.mts b/src/generator/generateTypes.mts index bb8a938..367abd4 100644 --- a/src/generator/generateTypes.mts +++ b/src/generator/generateTypes.mts @@ -15,22 +15,24 @@ const exportTypedRequestHandlers = 'export type TypedRequestHandlers = ImportedT export async function generateTypes( openapiPath: string, destinationPath: string, - shouldFormat: boolean, - addTypedRequestHandler: boolean, - inject?: string, - transform?: (schemaObject: SchemaObject, metadata: TransformNodeOptions) => TypeNode | TransformObject | undefined + options: { + shouldFormat?: boolean; + addTypedRequestHandler?: boolean; + inject?: string; + transform?: (schemaObject: SchemaObject, metadata: TransformNodeOptions) => TypeNode | TransformObject | undefined; + } ): Promise { - const ast = await openapiTS(await fs.readFile(openapiPath, 'utf-8'), { exportType: true, inject, transform }); + const ast = await openapiTS(await fs.readFile(openapiPath, 'utf-8'), { exportType: true, inject: options.inject, transform: options.transform }); let content = astToString(ast); - if (addTypedRequestHandler) { + if (options.addTypedRequestHandler === true) { content = typedRequestHandlerImport + content + exportTypedRequestHandlers; } content = FILE_HEADER + content; - if (shouldFormat) { + if (options.shouldFormat === true) { const prettierOptions = await resolveConfig('./src/index.ts'); content = await format(content, { ...prettierOptions, parser: 'typescript' }); From 1938d8a9ca1932cbae3f28a17674303539f7a9b6 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 09:58:18 +0300 Subject: [PATCH 06/12] docs: update section titles and enhance programmatic support description in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b1f02c..5a47404 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,11 @@ npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.t -## Functional Programming Support +## Programmatic Support The code generators (`generateTypes.mts` and `generateErrors.mts`) now support functional programming patterns. You can inject custom transformation logic or AST manipulation by providing functional arguments, making the generators more flexible and composable for advanced use cases. -### Programmatic Usage +### API Usage You can import and use the generators directly in your own scripts for full functional programming flexibility: From 34f2acbdc2ae88e319b920b789a2fb3e697fe911 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 10:01:33 +0300 Subject: [PATCH 07/12] docs: enhance programmatic support for CLI generators with functional programming patterns --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a47404..b01d496 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,13 @@ npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.t ## Programmatic Support +> **Note** +> Programmatic usage of the CLI (importing and using the generators directly) is only supported in ECMAScript modules (ESM). CommonJS is not supported for direct imports. + The code generators (`generateTypes.mts` and `generateErrors.mts`) now support functional programming patterns. You can inject custom transformation logic or AST manipulation by providing functional arguments, making the generators more flexible and composable for advanced use cases. -### API Usage +### API Usage You can import and use the generators directly in your own scripts for full functional programming flexibility: @@ -109,8 +112,6 @@ await generateErrors( You can pass custom `inject` or `transform` functions to `generateTypes` for advanced AST/code manipulation, enabling highly composable and functional workflows. ----- - ## TypedRequestHandler The package contains a wrapper for the `express` types package that provides autocomplete for all the request handlers to the API based on the OpenAPI schema. The TypedRequestHandler is initialized with the types generated by `openapi-typescript`, and is configured based on operation name or method and path. From 32ecf66a8dfe830c3deb4e77becc04fe7fbd3b91 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 10:04:19 +0300 Subject: [PATCH 08/12] docs: clarify programmatic support for CLI usage and ESM requirements --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b01d496..ea2ebeb 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,9 @@ npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.t ## Programmatic Support -> **Note** -> Programmatic usage of the CLI (importing and using the generators directly) is only supported in ECMAScript modules (ESM). CommonJS is not supported for direct imports. + +> [!NOTE] +> **Programmatic usage of the CLI (importing and using the generators directly) is only supported in ECMAScript modules (ESM).** CommonJS is not supported for direct imports. The code generators (`generateTypes.mts` and `generateErrors.mts`) now support functional programming patterns. You can inject custom transformation logic or AST manipulation by providing functional arguments, making the generators more flexible and composable for advanced use cases. From b8c02094c3a2ed0e8151be2f8e936c921fd44d3f Mon Sep 17 00:00:00 2001 From: ronenkapelian <72082238+ronenkapelian@users.noreply.github.com> Date: Sun, 24 Aug 2025 10:26:04 +0300 Subject: [PATCH 09/12] fix: Update src/generator/generateErrors.mts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/generator/generateErrors.mts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/generator/generateErrors.mts b/src/generator/generateErrors.mts index 2c08c5d..5849b09 100644 --- a/src/generator/generateErrors.mts +++ b/src/generator/generateErrors.mts @@ -36,7 +36,10 @@ function buildErrorMapping(errorCodes: Set): string { .map((code) => `'${code}': '${code}'`) .reduce((acc, curr) => `${acc}, ${curr}`); } - + return Array.from(errorCodes) + .map((code) => `'${code}': '${code}'`) + .join(', '); +} export async function generateErrors( openapiPath: string, destinationPath: string, From ce5c56a53cbde8bd1840609722bfd237a32f6046 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 10:26:41 +0300 Subject: [PATCH 10/12] fix: simplify buildErrorMapping function in generateErrors.mts --- src/generator/generateErrors.mts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/generator/generateErrors.mts b/src/generator/generateErrors.mts index 5849b09..0ee0efe 100644 --- a/src/generator/generateErrors.mts +++ b/src/generator/generateErrors.mts @@ -31,15 +31,11 @@ function createError(code: string): string { } function buildErrorMapping(errorCodes: Set): string { - return errorCodes - .values() - .map((code) => `'${code}': '${code}'`) - .reduce((acc, curr) => `${acc}, ${curr}`); -} return Array.from(errorCodes) .map((code) => `'${code}': '${code}'`) .join(', '); } + export async function generateErrors( openapiPath: string, destinationPath: string, From 169aef669ecc3116071ffda24b7a886b7fe35974 Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 10:30:47 +0300 Subject: [PATCH 11/12] docs: enhance CLI arguments reference in README for better clarity --- README.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ea2ebeb..bae4c52 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,29 @@ npm install --save-dev @map-colonies/openapi-helpers supertest prettier openapi- The package provides a unified CLI for generating TypeScript types and error classes from OpenAPI specifications. All code generation is now performed using the `generate` command, which supports subcommands for types and errors. +#### CLI Arguments Reference + +**Positional Arguments:** +For both `generate types` and `generate errors` commands, the positional arguments are: + +- ``: Path to the OpenAPI YAML or JSON file to use as the source schema. +- ``: Path to the file where the generated code will be written. + +These arguments are required and must be provided in the order shown. + +**Optional Arguments:** + +For `generate types`: +- `-f, --format`: Format the generated types using Prettier +- `-t, --add-typed-request-handler`: Add the TypedRequestHandler type to the generated types + +For `generate errors`: +- `-f, --format`: Format the generated code using Prettier +- `-e, --errors-output `: Specify what to generate (default: all) + - `all`: generate both error classes and error code mapping + - `map`: generate only the error code mapping + - `classes`: generate only the error classes + ### Generate Types Generate TypeScript types from an OpenAPI schema: @@ -27,8 +50,8 @@ npx @map-colonies/openapi-helpers generate types ./openapi3.yaml ./src/openapi.d ``` Options: -- `--format` - Format the generated types using `prettier`. -- `--add-typed-request-handler` - Add the `TypedRequestHandler` type to the generated types. +- `-f, --format` - Format the generated types using `prettier`. +- `-t, --add-typed-request-handler` - Add the `TypedRequestHandler` type to the generated types. ### Generate Errors @@ -44,8 +67,8 @@ npx @map-colonies/openapi-helpers generate errors ./openapi3.yaml ./src/errors.t Options: -- `--format` - Format the generated code using `prettier`. -- `--errors-output ` - Specify what to generate: +- `-f, --format` - Format the generated code using `prettier`. +- `-e, --errors-output ` - Specify what to generate: - `all` (default): generate both error classes and error code mapping - `map`: generate only the error code mapping - `classes`: generate only the error classes From 301dc9c82dc6ae512b9ff693a6cd25ebb85a749c Mon Sep 17 00:00:00 2001 From: ronenk1 Date: Sun, 24 Aug 2025 10:37:36 +0300 Subject: [PATCH 12/12] feat: use dynamic package version in CLI entrypoint --- package-lock.json | 37 +++++++++++++++++++++++++------------ package.json | 1 + src/cli/entrypoint.mts | 3 ++- src/common/constants.ts | 5 +++++ 4 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/common/constants.ts diff --git a/package-lock.json b/package-lock.json index 04d5a25..e2b5ce0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "@commander-js/extra-typings": "^14.0.0", + "@map-colonies/read-pkg": "^1.0.0", "change-case": "^5.4.4", "commander": "^14.0.0", "oas-normalize": "^14.1.2", @@ -2122,6 +2123,18 @@ "node": ">=20" } }, + "node_modules/@map-colonies/read-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/read-pkg/-/read-pkg-1.0.0.tgz", + "integrity": "sha512-powpYwGoy1x+uYa9wvBIwTQBufv2Rzm35IiYlsyZbh8yIq3e2PIxwonAmZ6hleaDAuu35BS5STAnWCUTcMKBiQ==", + "license": "ISC", + "dependencies": { + "type-fest": "^4.33.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@map-colonies/tsconfig": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@map-colonies/tsconfig/-/tsconfig-1.0.1.tgz", @@ -7673,18 +7686,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/openapi-typescript/node_modules/type-fest": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.24.0.tgz", - "integrity": "sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==", - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9282,6 +9283,18 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 619a7c8..4915801 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "@commander-js/extra-typings": "^14.0.0", + "@map-colonies/read-pkg": "^1.0.0", "change-case": "^5.4.4", "commander": "^14.0.0", "oas-normalize": "^14.1.2", diff --git a/src/cli/entrypoint.mts b/src/cli/entrypoint.mts index 653d78c..2e04378 100644 --- a/src/cli/entrypoint.mts +++ b/src/cli/entrypoint.mts @@ -4,6 +4,7 @@ import { program } from '@commander-js/extra-typings'; import { generateTypes } from '../generator/generateTypes.mjs'; import { generateErrors } from '../generator/generateErrors.mjs'; import ora from 'ora'; +import { PACKAGE_VERSION } from '../common/constants.js'; const errorOutput = ['all', 'map', 'classes'] as const; type ErrorsOutput = (typeof errorOutput)[number]; @@ -13,7 +14,7 @@ function isErrorsOutput(value: string): value is ErrorsOutput { } const SECOND = 1000; -program.name('openapi-helpers').description('Generate TypeScript types and error classes from OpenAPI specifications').version('3.1.0'); +program.name('openapi-helpers').description('Generate TypeScript types and error classes from OpenAPI specifications').version(PACKAGE_VERSION); const command = program.command('generate').description('Generate code artifacts (types, error classes) from OpenAPI specifications'); diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 0000000..36d6135 --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,5 @@ +import { readPackageJsonSync } from '@map-colonies/read-pkg'; + +const packageJson = readPackageJsonSync(); +const PACKAGE_VERSION = packageJson.version ?? 'unknown'; +export { PACKAGE_VERSION };