From 780b301ea96e72bc9436fc83cab0fc38ad963cb5 Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Mon, 13 Oct 2025 12:45:56 +0100 Subject: [PATCH 1/3] * added ndjson support * now ignores identical files --- bun.lock | 1 + package.json | 2 + src/api/builder.ts | 89 ++++++++++++++++++++++++------------- src/utils/codegen-logger.ts | 2 +- 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/bun.lock b/bun.lock index cf0936b0b..8b4b0506a 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@inquirer/prompts": "^7.8.1", "ajv": "^8.17.1", "handlebars": "^4.7.8", + "lodash": "^4.17.21", "ora": "^8.2.0", "picocolors": "^1.1.1", "yargs": "^18.0.0", diff --git a/package.json b/package.json index d06ac3ed7..7fdc7f4ee 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", "@types/handlebars": "^4.1.0", + "@types/lodash": "^4.17.20", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", "ts-prune": "^0.10.3", @@ -75,6 +76,7 @@ "@inquirer/prompts": "^7.8.1", "ajv": "^8.17.1", "handlebars": "^4.7.8", + "lodash": "^4.17.21", "ora": "^8.2.0", "picocolors": "^1.1.1", "yargs": "^18.0.0" diff --git a/src/api/builder.ts b/src/api/builder.ts index 1ad067303..fc0c471ae 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -8,6 +8,7 @@ import * as fs from "node:fs"; import * as afs from "node:fs/promises"; import * as Path from "node:path"; +import _ from 'lodash'; import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager"; import type { GeneratedFile } from "@root/api/generators/base/types"; import { camelCase } from "@root/api/writer-generator/utils.ts"; @@ -268,42 +269,68 @@ export class APIBuilder { return this; } + private static async doesIdenticalFileExist(path: string, ts: TypeSchema): Promise { + if(! await afs.exists(path)) return false; + const json = await afs.readFile(path); + const obj = JSON.parse(json.toString()) as TypeSchema; + return _.isEqual(obj, ts); + } + + private async writeTypeSchemasToSeparateFiles(typeSchemas: TypeSchema[], outputDir :string): Promise { + if (this.options.cleanOutput) fs.rmSync(outputDir, { recursive: true, force: true }); + await afs.mkdir(outputDir, { recursive: true }); + + const usedNames: Record = {}; + + this.logger.info(`Writing TypeSchema files to ${outputDir}...`); + + for (const ts of typeSchemas) { + const package_name = camelCase(ts.identifier.package.replaceAll("/", "-")); + const name = normalizeFileName(ts.identifier.name.toString()); + const json = JSON.stringify(ts, null, 2); + + const baseName = Path.join(outputDir, package_name, name); + let fullName: string; + if (usedNames[baseName] !== undefined) { + usedNames[baseName]++; + fullName = `${baseName}-${usedNames[baseName]}.typeschema.json`; + } else { + usedNames[baseName] = 0; + fullName = `${baseName}.typeschema.json`; + } + + if(await APIBuilder.doesIdenticalFileExist(fullName, ts)) continue; + + await afs.mkdir(Path.dirname(fullName), {recursive: true}); + await afs.writeFile(fullName, json); + } + } + + private async writeTypeSchemasToSingleFile(typeSchemas: TypeSchema[], outputFile: string): Promise { + if (this.options.cleanOutput && fs.existsSync(outputFile)) fs.rmSync(outputFile); + await afs.mkdir(Path.dirname(outputFile), { recursive: true }); + + this.logger.info(`Writing TypeSchemas to one file ${outputFile}...`); + + for (const ts of typeSchemas) { + const json = JSON.stringify(ts, null, 2); + await afs.appendFile(outputFile, json + '\n'); + } + } + private async tryWriteTypeSchema(typeSchemas: TypeSchema[]) { if (!this.options.typeSchemaOutputDir) return; try { - if (this.options.cleanOutput) fs.rmSync(this.options.typeSchemaOutputDir, { recursive: true, force: true }); - await afs.mkdir(this.options.typeSchemaOutputDir, { recursive: true }); - - let writtenCount = 0; - let overrideCount = 0; - const usedNames: Record = {}; - - this.logger.info(`Writing TypeSchema files to ${this.options.typeSchemaOutputDir}...`); - - for (const ts of typeSchemas) { - const package_name = camelCase(ts.identifier.package.replaceAll("/", "-")); - const name = normalizeFileName(ts.identifier.name.toString()); - - const baseName = Path.join(this.options.typeSchemaOutputDir, package_name, name); - let fullName: string; - if (usedNames[baseName] !== undefined) { - usedNames[baseName]++; - fullName = `${baseName}-${usedNames[baseName]}.typeschema.json`; - } else { - usedNames[baseName] = 0; - fullName = `${baseName}.typeschema.json`; - } - - await afs.mkdir(Path.dirname(fullName), { recursive: true }); - afs.writeFile(fullName, JSON.stringify(ts, null, 2)); - - if (await afs.exists(fullName)) overrideCount++; - else writtenCount++; - } - this.logger.info(`Created ${writtenCount} new TypeSchema files, overrode ${overrideCount} files`); + this.logger.info(`Starting writing TypeSchema files.`); + + if(Path.extname(this.options.typeSchemaOutputDir) === ".ndjson") + await this.writeTypeSchemasToSingleFile(typeSchemas, this.options.typeSchemaOutputDir); + else + await this.writeTypeSchemasToSeparateFiles(typeSchemas, this.options.typeSchemaOutputDir); + + this.logger.info(`Finished writing TypeSchema files.`); } catch (error) { if (this.options.throwException) throw error; - this.logger.error( "Failed to write TypeSchema output", error instanceof Error ? error : new Error(String(error)), diff --git a/src/utils/codegen-logger.ts b/src/utils/codegen-logger.ts index dea984a5b..e4757bcd5 100644 --- a/src/utils/codegen-logger.ts +++ b/src/utils/codegen-logger.ts @@ -74,7 +74,7 @@ export class CodegenLogger { */ error(message: string, error?: Error): void { if (this.isSuppressed(LogLevel.ERROR)) return; - console.error(this.formatMessage("", message, pc.red)); + console.error(this.formatMessage("X", message, pc.red)); if (error && this.options.verbose) { console.error(pc.red(` ${error.message}`)); if (error.stack) { From a2910ece59641d9e6286ade1ef43d98024a2257b Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Mon, 13 Oct 2025 13:22:20 +0100 Subject: [PATCH 2/3] added deep-equal removed lodash --- bun.lock | 4 +++- package.json | 1 - src/api/builder.ts | 14 +++++++------- src/api/writer-generator/utils.ts | 28 ++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 8b4b0506a..175f38e5a 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "@inquirer/prompts": "^7.8.1", "ajv": "^8.17.1", "handlebars": "^4.7.8", - "lodash": "^4.17.21", "ora": "^8.2.0", "picocolors": "^1.1.1", "yargs": "^18.0.0", @@ -18,6 +17,7 @@ "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", "@types/handlebars": "^4.1.0", + "@types/lodash": "^4.17.20", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", "ts-prune": "^0.10.3", @@ -205,6 +205,8 @@ "@types/handlebars": ["@types/handlebars@4.1.0", "", { "dependencies": { "handlebars": "*" } }, "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA=="], + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], + "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], diff --git a/package.json b/package.json index 7fdc7f4ee..6b343ffd9 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@inquirer/prompts": "^7.8.1", "ajv": "^8.17.1", "handlebars": "^4.7.8", - "lodash": "^4.17.21", "ora": "^8.2.0", "picocolors": "^1.1.1", "yargs": "^18.0.0" diff --git a/src/api/builder.ts b/src/api/builder.ts index fc0c471ae..f20a3f623 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -8,10 +8,9 @@ import * as fs from "node:fs"; import * as afs from "node:fs/promises"; import * as Path from "node:path"; -import _ from 'lodash'; import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager"; import type { GeneratedFile } from "@root/api/generators/base/types"; -import { camelCase } from "@root/api/writer-generator/utils.ts"; +import { camelCase, deepEqual } from "@root/api/writer-generator/utils.ts"; import { registerFromManager } from "@root/typeschema/register"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; import { generateTypeSchemas, TypeSchemaCache, TypeSchemaGenerator, TypeSchemaParser } from "@typeschema/index"; @@ -269,11 +268,12 @@ export class APIBuilder { return this; } - private static async doesIdenticalFileExist(path: string, ts: TypeSchema): Promise { - if(! await afs.exists(path)) return false; + private static async isIdenticalTo(path: string, tsJSON: string): Promise { + if(!await afs.exists(path)) return false; const json = await afs.readFile(path); - const obj = JSON.parse(json.toString()) as TypeSchema; - return _.isEqual(obj, ts); + const ts1 = JSON.parse(json.toString()) as TypeSchema; + const ts2 = JSON.parse(tsJSON) as TypeSchema; + return deepEqual(ts1, ts2); } private async writeTypeSchemasToSeparateFiles(typeSchemas: TypeSchema[], outputDir :string): Promise { @@ -299,7 +299,7 @@ export class APIBuilder { fullName = `${baseName}.typeschema.json`; } - if(await APIBuilder.doesIdenticalFileExist(fullName, ts)) continue; + if(await APIBuilder.isIdenticalTo(fullName, json)) continue; await afs.mkdir(Path.dirname(fullName), {recursive: true}); await afs.writeFile(fullName, json); diff --git a/src/api/writer-generator/utils.ts b/src/api/writer-generator/utils.ts index cdceeb69c..22c557e7d 100644 --- a/src/api/writer-generator/utils.ts +++ b/src/api/writer-generator/utils.ts @@ -37,3 +37,31 @@ export const uppercaseFirstLetter = (str: string): string => { export const uppercaseFirstLetterOfEach = (strings: string[]): string[] => { return strings.map((str) => uppercaseFirstLetter(str)); }; + + +export function deepEqual(obj1: T, obj2: T): boolean { + if (obj1 === obj2) return true; + + if (obj1 === null || obj2 === null || + typeof obj1 !== 'object' || typeof obj2 !== 'object') { + return false; + } + + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) return false; + return obj1.every((item, index) => deepEqual(item, obj2[index])); + } + + if (Array.isArray(obj1) || Array.isArray(obj2)) { + return false; + } + + const keys1 = Object.keys(obj1) as (keyof T)[]; + const keys2 = Object.keys(obj2) as (keyof T)[]; + + if (keys1.length !== keys2.length) return false; + + return keys1.every(key => + keys2.includes(key) && deepEqual(obj1[key], obj2[key]) + ); +} \ No newline at end of file From bde25c32f3b90fc3a2cd1bc9bba22f692d5c808d Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Tue, 14 Oct 2025 16:02:11 +0100 Subject: [PATCH 3/3] * formatted code * removed unused lodash usages --- bun.lock | 5 +-- package.json | 1 - src/api/builder.ts | 15 ++++---- src/api/writer-generator/utils.ts | 36 +++++++++---------- test/helpers/mock-generators.ts | 2 +- .../generators/base/error-handling.test.ts | 2 +- 6 files changed, 26 insertions(+), 35 deletions(-) diff --git a/bun.lock b/bun.lock index 175f38e5a..35ecb04e6 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,6 @@ "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", "@types/handlebars": "^4.1.0", - "@types/lodash": "^4.17.20", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", "ts-prune": "^0.10.3", @@ -205,8 +204,6 @@ "@types/handlebars": ["@types/handlebars@4.1.0", "", { "dependencies": { "handlebars": "*" } }, "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA=="], - "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], - "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], @@ -571,4 +568,4 @@ "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6b343ffd9..d06ac3ed7 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@biomejs/biome": "^2.1.4", "@types/bun": "^1.2.23", "@types/handlebars": "^4.1.0", - "@types/lodash": "^4.17.20", "@types/node": "^22.17.1", "@types/yargs": "^17.0.33", "ts-prune": "^0.10.3", diff --git a/src/api/builder.ts b/src/api/builder.ts index f20a3f623..6e18a595c 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -269,14 +269,14 @@ export class APIBuilder { } private static async isIdenticalTo(path: string, tsJSON: string): Promise { - if(!await afs.exists(path)) return false; + if (!(await afs.exists(path))) return false; const json = await afs.readFile(path); const ts1 = JSON.parse(json.toString()) as TypeSchema; const ts2 = JSON.parse(tsJSON) as TypeSchema; return deepEqual(ts1, ts2); } - private async writeTypeSchemasToSeparateFiles(typeSchemas: TypeSchema[], outputDir :string): Promise { + private async writeTypeSchemasToSeparateFiles(typeSchemas: TypeSchema[], outputDir: string): Promise { if (this.options.cleanOutput) fs.rmSync(outputDir, { recursive: true, force: true }); await afs.mkdir(outputDir, { recursive: true }); @@ -299,9 +299,9 @@ export class APIBuilder { fullName = `${baseName}.typeschema.json`; } - if(await APIBuilder.isIdenticalTo(fullName, json)) continue; + if (await APIBuilder.isIdenticalTo(fullName, json)) continue; - await afs.mkdir(Path.dirname(fullName), {recursive: true}); + await afs.mkdir(Path.dirname(fullName), { recursive: true }); await afs.writeFile(fullName, json); } } @@ -314,7 +314,7 @@ export class APIBuilder { for (const ts of typeSchemas) { const json = JSON.stringify(ts, null, 2); - await afs.appendFile(outputFile, json + '\n'); + await afs.appendFile(outputFile, json + "\n"); } } @@ -323,10 +323,9 @@ export class APIBuilder { try { this.logger.info(`Starting writing TypeSchema files.`); - if(Path.extname(this.options.typeSchemaOutputDir) === ".ndjson") + if (Path.extname(this.options.typeSchemaOutputDir) === ".ndjson") await this.writeTypeSchemasToSingleFile(typeSchemas, this.options.typeSchemaOutputDir); - else - await this.writeTypeSchemasToSeparateFiles(typeSchemas, this.options.typeSchemaOutputDir); + else await this.writeTypeSchemasToSeparateFiles(typeSchemas, this.options.typeSchemaOutputDir); this.logger.info(`Finished writing TypeSchema files.`); } catch (error) { diff --git a/src/api/writer-generator/utils.ts b/src/api/writer-generator/utils.ts index 22c557e7d..a2596da6d 100644 --- a/src/api/writer-generator/utils.ts +++ b/src/api/writer-generator/utils.ts @@ -38,30 +38,26 @@ export const uppercaseFirstLetterOfEach = (strings: string[]): string[] => { return strings.map((str) => uppercaseFirstLetter(str)); }; - export function deepEqual(obj1: T, obj2: T): boolean { - if (obj1 === obj2) return true; + if (obj1 === obj2) return true; - if (obj1 === null || obj2 === null || - typeof obj1 !== 'object' || typeof obj2 !== 'object') { - return false; - } + if (obj1 === null || obj2 === null || typeof obj1 !== "object" || typeof obj2 !== "object") { + return false; + } - if (Array.isArray(obj1) && Array.isArray(obj2)) { - if (obj1.length !== obj2.length) return false; - return obj1.every((item, index) => deepEqual(item, obj2[index])); - } + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) return false; + return obj1.every((item, index) => deepEqual(item, obj2[index])); + } - if (Array.isArray(obj1) || Array.isArray(obj2)) { - return false; - } + if (Array.isArray(obj1) || Array.isArray(obj2)) { + return false; + } - const keys1 = Object.keys(obj1) as (keyof T)[]; - const keys2 = Object.keys(obj2) as (keyof T)[]; + const keys1 = Object.keys(obj1) as (keyof T)[]; + const keys2 = Object.keys(obj2) as (keyof T)[]; - if (keys1.length !== keys2.length) return false; + if (keys1.length !== keys2.length) return false; - return keys1.every(key => - keys2.includes(key) && deepEqual(obj1[key], obj2[key]) - ); -} \ No newline at end of file + return keys1.every((key) => keys2.includes(key) && deepEqual(obj1[key], obj2[key])); +} diff --git a/test/helpers/mock-generators.ts b/test/helpers/mock-generators.ts index 519be60a7..a9e728a52 100644 --- a/test/helpers/mock-generators.ts +++ b/test/helpers/mock-generators.ts @@ -2,9 +2,9 @@ * Mock implementations for testing */ -import type { TypeSchema } from "@typeschema/types"; import { BaseGenerator } from "@root/api/generators/base/BaseGenerator"; import type { BaseGeneratorOptions, GeneratedFile, TemplateContext } from "@root/api/generators/base/types"; +import type { TypeSchema } from "@typeschema/types"; /** * Mock logger that captures all log messages diff --git a/test/unit/api/generators/base/error-handling.test.ts b/test/unit/api/generators/base/error-handling.test.ts index a9b1f48f1..23e7f9ff0 100644 --- a/test/unit/api/generators/base/error-handling.test.ts +++ b/test/unit/api/generators/base/error-handling.test.ts @@ -9,8 +9,8 @@ import { EnhancedTemplateError, } from "@root/api/generators/base/enhanced-errors"; import { ErrorHandler, GeneratorErrorBoundary } from "@root/api/generators/base/error-handler"; -import { createMockSchema } from "../../../../helpers/schema-helpers"; import { MockLogger } from "../../../../helpers/mock-generators"; +import { createMockSchema } from "../../../../helpers/schema-helpers"; describe("Enhanced Error Handling", () => { describe("EnhancedSchemaValidationError", () => {