From 1bbb06fcd8486f2cfff7e1c54d5156c9034786f6 Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Wed, 18 Feb 2026 22:37:34 +0000 Subject: [PATCH 01/32] new logger with suppression functionality replaced the old logger with the new one --- examples/mustache/mustache-java-r4-gen.ts | 2 +- examples/python/generate.ts | 8 +- src/api/builder.ts | 24 +- src/api/index.ts | 2 +- src/api/writer-generator/mustache.ts | 4 +- src/api/writer-generator/writer.ts | 6 +- src/cli/commands/typeschema/generate.ts | 13 +- src/typeschema/core/binding.ts | 19 +- src/typeschema/core/field-builder.ts | 12 +- src/typeschema/core/nested-types.ts | 6 +- src/typeschema/core/transformer.ts | 15 +- src/typeschema/index.ts | 16 +- src/typeschema/ir/tree-shake.ts | 4 +- src/typeschema/register.ts | 19 +- src/typeschema/types.ts | 2 +- src/typeschema/utils.ts | 5 +- src/utils/logger.ts | 160 +++++++ test/api/mustache.test.ts | 2 +- test/api/write-generator/csharp.test.ts | 2 +- .../api/write-generator/introspection.test.ts | 8 +- test/api/write-generator/python.test.ts | 2 +- test/api/write-generator/typescript.test.ts | 29 +- test/unit/typeschema/utils.ts | 6 +- test/unit/utils/logger.test.ts | 433 ++++++++++++++++++ 24 files changed, 686 insertions(+), 113 deletions(-) create mode 100644 src/utils/logger.ts create mode 100644 test/unit/utils/logger.test.ts diff --git a/examples/mustache/mustache-java-r4-gen.ts b/examples/mustache/mustache-java-r4-gen.ts index 942bcac3f..d96a4c6ff 100644 --- a/examples/mustache/mustache-java-r4-gen.ts +++ b/examples/mustache/mustache-java-r4-gen.ts @@ -4,7 +4,7 @@ if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .setLogLevel("DEBUG") + .setLogLevel("debug") .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .outputTo("./examples/mustache/mustache-java-r4-output") diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 3fb292500..babd98050 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,8 +1,14 @@ import { APIBuilder, prettyReport } from "../../src"; +import { type Logger, makeLogger } from "../../src/utils/logger"; console.log("📦 Generating FHIR R4 Core Types..."); -const builder = new APIBuilder() +const logger: Logger = makeLogger({ + prefix: "API", + suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], +}); + +const builder = new APIBuilder({ logger }) .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .python({ diff --git a/src/api/builder.ts b/src/api/builder.ts index 146333d84..3b7e5d344 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,13 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { - type CodegenLogger, - createLogger, - type LogLevel, - type LogLevelString, - parseLogLevel, -} from "@root/utils/codegen-logger"; +import { type Logger, type LogLevel, makeLogger } from "@root/utils/logger"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; @@ -100,7 +94,7 @@ export interface LocalStructureDefinitionConfig { dependencies?: PackageMeta[]; } -const cleanup = async (opts: APIBuilderOptions, logger: CodegenLogger): Promise => { +const cleanup = async (opts: APIBuilderOptions, logger: Logger): Promise => { logger.info(`Cleaning outputs...`); try { logger.info(`Clean ${opts.outputDir}`); @@ -125,7 +119,7 @@ export class APIBuilder { localSDs: LocalPackageConfig[]; localTgzPackages: TgzPackageConfig[]; }; - private logger: CodegenLogger; + private logger: Logger; private generators: { name: string; writer: FileSystemWriter }[] = []; constructor( @@ -133,7 +127,7 @@ export class APIBuilder { manager?: ReturnType; register?: Register; preprocessPackage?: (context: PreprocessContext) => PreprocessContext; - logger?: CodegenLogger; + logger?: Logger; } = {}, ) { const defaultOpts: APIBuilderOptions = { @@ -143,7 +137,7 @@ export class APIBuilder { treeShake: undefined, promoteLogical: undefined, registry: undefined, - logLevel: parseLogLevel("INFO"), + logLevel: "info", dropCanonicalManagerCache: false, }; const opts: APIBuilderOptions = { @@ -179,7 +173,7 @@ export class APIBuilder { dropCache: userOpts.dropCanonicalManagerCache, preprocessPackage: userOpts.preprocessPackage, }); - this.logger = userOpts.logger ?? createLogger({ prefix: "API", level: opts.logLevel }); + this.logger = userOpts.logger ?? makeLogger({ prefix: "API", level: opts.logLevel }); this.options = opts; } @@ -340,8 +334,8 @@ export class APIBuilder { return this; } - setLogLevel(level: LogLevel | LogLevelString): APIBuilder { - this.logger?.setLevel(typeof level === "string" ? parseLogLevel(level) : level); + setLogLevel(level: LogLevel): APIBuilder { + this.logger?.setLevel(level); return this; } @@ -449,7 +443,7 @@ export class APIBuilder { this.logger.debug(`Generation completed: ${result.filesGenerated.length} files`); } catch (error) { - this.logger.error("Code generation failed", error instanceof Error ? error : new Error(String(error))); + this.logger.error(`Code generation failed: ${error instanceof Error ? error.message : String(error)}`); result.errors.push(error instanceof Error ? error.message : String(error)); if (this.options.throwException) throw error; } diff --git a/src/api/index.ts b/src/api/index.ts index 2dbd32b35..1e8d9afa4 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,7 +8,7 @@ */ export type { IrConf, LogicalPromotionConf, TreeShakeConf } from "../typeschema/ir/types"; -export { LogLevel } from "../utils/codegen-logger"; +export type { LogLevel } from "../utils/logger"; export type { APIBuilderOptions, LocalStructureDefinitionConfig } from "./builder"; export { APIBuilder, prettyReport } from "./builder"; export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts index cf270e680..754db89ab 100644 --- a/src/api/writer-generator/mustache.ts +++ b/src/api/writer-generator/mustache.ts @@ -22,7 +22,7 @@ import type { ViewModel, } from "@mustache/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import { default as Mustache } from "mustache"; import { FileSystemWriter, type FileSystemWriterOptions } from "./writer"; @@ -57,7 +57,7 @@ export type MustacheGeneratorOptions = FileSystemWriterOptions & export function loadMustacheGeneratorConfig( templatePath: string, - logger?: CodegenLogger, + logger?: Logger, ): Partial { const filePath = Path.resolve(templatePath, "config.json"); try { diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index 70938815b..0a7a1029b 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -2,12 +2,12 @@ import * as fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import * as Path from "node:path"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; export type FileSystemWriterOptions = { outputDir: string; inMemoryOnly?: boolean; - logger?: CodegenLogger; + logger?: Logger; resolveAssets?: (fn: string) => string; }; @@ -36,7 +36,7 @@ export abstract class FileSystemWriter, G }, }, handler: async (argv) => { - const logger = createLogger({ + const logger = makeLogger({ prefix: "TypeSchema", }); try { - logger.step("Generating TypeSchema from FHIR packages"); + logger.info("Generating TypeSchema from FHIR packages"); logger.info(`Packages: ${argv.packages.join(", ")}`); logger.info(`Output: ${argv.output}`); @@ -113,7 +114,7 @@ export const generateTypeschemaCommand: CommandModule, G return { name: packageSpec, version: "latest" }; }); - logger.progress(`Processing packages: ${packageMetas.map((p) => `${p.name}@${p.version}`).join(", ")}`); + logger.info(`Processing packages: ${packageMetas.map((p) => `${p.name}@${p.version}`).join(", ")}`); // Create register from packages const register = await registerFromPackageMetas(packageMetas, { @@ -149,7 +150,7 @@ export const generateTypeschemaCommand: CommandModule, G const duration = Date.now() - startTime; complete(`Generated ${allSchemas.length} TypeSchema definitions`, duration, { schemas: allSchemas.length }); - logger.dim(`Output: ${outputPath}`); + logger.info(`Output: ${outputPath}`); if (argv.verbose) { logger.debug("Generated schemas:"); @@ -160,7 +161,7 @@ export const generateTypeschemaCommand: CommandModule, G list(schemaNames); } } catch (error) { - logger.error("Failed to generate TypeSchema", error instanceof Error ? error : new Error(String(error))); + logger.error(`Failed to generate TypeSchema: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }, diff --git a/src/typeschema/core/binding.ts b/src/typeschema/core/binding.ts index e8bbb1396..daa7de197 100644 --- a/src/typeschema/core/binding.ts +++ b/src/typeschema/core/binding.ts @@ -7,7 +7,7 @@ import assert from "node:assert"; import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { CodeSystem, CodeSystemConcept } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import type { Register } from "@typeschema/register"; import type { BindingTypeSchema, @@ -24,7 +24,7 @@ export function extractValueSetConceptsByUrl( register: Register, pkg: PackageMeta, valueSetUrl: CanonicalUrl, - logger?: CodegenLogger, + logger?: Logger, ): Concept[] | undefined { const cleanUrl = dropVersionFromUrl(valueSetUrl) || valueSetUrl; const valueSet = register.resolveVs(pkg, cleanUrl as CanonicalUrl); @@ -32,11 +32,7 @@ export function extractValueSetConceptsByUrl( return extractValueSetConcepts(register, valueSet, logger); } -function extractValueSetConcepts( - register: Register, - valueSet: RichValueSet, - _logger?: CodegenLogger, -): Concept[] | undefined { +function extractValueSetConcepts(register: Register, valueSet: RichValueSet, _logger?: Logger): Concept[] | undefined { if (valueSet.expansion?.contains) { return valueSet.expansion.contains .filter((item) => item.code !== undefined) @@ -106,7 +102,7 @@ export function buildEnum( register: Register, fhirSchema: RichFHIRSchema, element: FHIRSchemaElement, - logger?: CodegenLogger, + logger?: Logger, ): EnumDefinition | undefined { if (!element.binding) return undefined; @@ -115,7 +111,7 @@ export function buildEnum( if (!valueSetUrl) return undefined; if (!BINDABLE_TYPES.has(element.type ?? "")) { - logger?.dryWarn(`eld-11: Binding on non-bindable type '${element.type}' (valueSet: ${valueSetUrl})`); + logger?.dryWarn("BINDING", `eld-11: Binding on non-bindable type '${element.type}' (valueSet: ${valueSetUrl})`); return undefined; } @@ -132,6 +128,7 @@ export function buildEnum( if (codes.length > MAX_ENUM_LENGTH) { logger?.dryWarn( + "LARGE_VALUESET", `Value set ${valueSetUrl} has ${codes.length} which is more than ${MAX_ENUM_LENGTH} codes, which may cause issues with code generation.`, ); return undefined; @@ -146,7 +143,7 @@ function generateBindingSchema( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: CodegenLogger, + logger?: Logger, ): BindingTypeSchema | undefined { if (!element.binding?.valueSet) return undefined; @@ -171,7 +168,7 @@ function generateBindingSchema( export function collectBindingSchemas( register: Register, fhirSchema: RichFHIRSchema, - logger?: CodegenLogger, + logger?: Logger, ): BindingTypeSchema[] { const processedPaths = new Set(); if (!fhirSchema.elements) return []; diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 3676adce0..2f15edc8b 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import { packageMetaToFhir } from "@typeschema/types"; import type { BindingIdentifier, @@ -224,7 +224,7 @@ export function buildFieldType( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: CodegenLogger, + logger?: Logger, ): Identifier | undefined { if (element.elementReference) { const refPath = element.elementReference @@ -247,6 +247,7 @@ export function buildFieldType( // Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions // Log a warning but continue processing instead of throwing logger?.dryWarn( + "FIELD_TYPE_NOT_FOUND", `Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`, ); return undefined; @@ -258,7 +259,7 @@ export const mkField = ( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: CodegenLogger, + logger?: Logger, rawElement?: FHIRSchemaElement, ): Field => { let binding: BindingIdentifier | undefined; @@ -274,7 +275,10 @@ export const mkField = ( const fieldType = buildFieldType(register, fhirSchema, path, element, logger); // TODO: should be an exception if (!fieldType) - logger?.dryWarn(`Field type not found for '${fhirSchema.url}#${path.join(".")}' (${fhirSchema.derivation})`); + logger?.dryWarn( + "FIELD_TYPE_NOT_FOUND", + `Field type not found for '${fhirSchema.url}#${path.join(".")}' (${fhirSchema.derivation})`, + ); let valueConstraint: ValueConstraint | undefined; if (element.pattern) { diff --git a/src/typeschema/core/nested-types.ts b/src/typeschema/core/nested-types.ts index b2da3ad54..fa3254932 100644 --- a/src/typeschema/core/nested-types.ts +++ b/src/typeschema/core/nested-types.ts @@ -6,7 +6,7 @@ import type { FHIRSchema, FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { mergeFsElementProps, type Register, resolveFsElementGenealogy } from "@root/typeschema/register"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import type { CanonicalUrl, Field, Identifier, Name, NestedIdentifier, NestedType, RichFHIRSchema } from "../types"; import { mkField, mkNestedField } from "./field-builder"; @@ -114,7 +114,7 @@ function transformNestedElements( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record, - logger?: CodegenLogger, + logger?: Logger, ): Record { const fields: Record = {}; @@ -148,7 +148,7 @@ function transformNestedElements( export function mkNestedTypes( register: Register, fhirSchema: RichFHIRSchema, - logger?: CodegenLogger, + logger?: Logger, ): NestedType[] | undefined { if (!fhirSchema.elements) return undefined; diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 316618b31..289f4cc96 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { shouldSkipCanonical } from "@root/typeschema/skip-hack"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import type { Register } from "@typeschema/register"; import { concatIdentifiers, @@ -33,7 +33,7 @@ export function mkFields( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record | undefined, - logger?: CodegenLogger, + logger?: Logger, ): Record | undefined { if (!elements) return undefined; @@ -44,6 +44,7 @@ export function mkFields( const fcurl = elemSnapshot.type ? register.ensureSpecializationCanonicalUrl(elemSnapshot.type) : undefined; if (fcurl && shouldSkipCanonical(fhirSchema.package_meta, fcurl).shouldSkip) { logger?.warn( + "SKIP_CANONICAL", `Skipping field ${path} for ${fcurl} due to skip hack ${shouldSkipCanonical(fhirSchema.package_meta, fcurl).reason}`, ); continue; @@ -76,7 +77,7 @@ function extractFieldDependencies(fields: Record): Identifier[] { export async function transformValueSet( register: Register, valueSet: RichValueSet, - logger?: CodegenLogger, + logger?: Logger, ): Promise { if (!valueSet.url) throw new Error("ValueSet URL is required"); @@ -113,11 +114,7 @@ export function extractDependencies( return concatIdentifiers(filtered); } -function transformFhirSchemaResource( - register: Register, - fhirSchema: RichFHIRSchema, - logger?: CodegenLogger, -): TypeSchema[] { +function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSchema, logger?: Logger): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); let base: Identifier | undefined; @@ -158,7 +155,7 @@ function transformFhirSchemaResource( export async function transformFhirSchema( register: Register, fhirSchema: RichFHIRSchema, - logger?: CodegenLogger, + logger?: Logger, ): Promise { return transformFhirSchemaResource(register, fhirSchema, logger); } diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 9aa625a60..46a1d32b4 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -10,7 +10,7 @@ * - Validating TypeSchema documents */ -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import { transformFhirSchema, transformValueSet } from "./core/transformer"; import type { TypeSchemaCollisions } from "./ir/types"; import type { Register } from "./register"; @@ -33,10 +33,7 @@ type SchemaWithSource = { sourceCanonical: CanonicalUrl; }; -const deduplicateSchemas = ( - schemasWithSources: SchemaWithSource[], - logger?: CodegenLogger, -): GenerateTypeSchemasResult => { +const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Logger): GenerateTypeSchemasResult => { // key -> hash const groups: Record> = {}; @@ -62,7 +59,7 @@ const deduplicateSchemas = ( if (sorted.length > 1) { const pkg = best.typeSchema.identifier.package; const url = best.typeSchema.identifier.url; - logger?.dryWarn(`'${url}' from '${pkg}'' has ${sorted.length} versions`); + logger?.dryWarn("DUPLICATE_SCHEMA", `'${url}' from '${pkg}'' has ${sorted.length} versions`); collisions[pkg] ??= {}; collisions[pkg][url] = sorted.flatMap((v) => v.sources.map((s) => ({ @@ -77,10 +74,7 @@ const deduplicateSchemas = ( return { schemas, collisions }; }; -export const generateTypeSchemas = async ( - register: Register, - logger?: CodegenLogger, -): Promise => { +export const generateTypeSchemas = async (register: Register, logger?: Logger): Promise => { const schemasWithSources: { schema: TypeSchema; sourcePackage: PkgName; sourceCanonical: CanonicalUrl }[] = []; for (const fhirSchema of register.allFs()) { @@ -88,7 +82,7 @@ export const generateTypeSchemas = async ( const skipCheck = shouldSkipCanonical(fhirSchema.package_meta, fhirSchema.url); if (skipCheck.shouldSkip) { - logger?.dryWarn(`Skip ${fhirSchema.url} from ${pkgId}. Reason: ${skipCheck.reason}`); + logger?.dryWarn("SKIP_CANONICAL", `Skip ${fhirSchema.url} from ${pkgId}. Reason: ${skipCheck.reason}`); continue; } diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index fad2f2d51..b13229af4 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import { extractDependencies } from "../core/transformer"; import { type CanonicalUrl, @@ -174,7 +174,7 @@ const mutableFillReport = (report: TreeShakeReport, tsIndex: TypeSchemaIndex, sh } }; -export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: CodegenLogger): TypeSchema => { +export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: Logger): TypeSchema => { schema = JSON.parse(JSON.stringify(schema)); if (isPrimitiveTypeSchema(schema) || isValueSetTypeSchema(schema) || isBindingSchema(schema)) return schema; diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index 59abcde91..cfaed0916 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -7,7 +7,7 @@ import { type StructureDefinition, } from "@atomic-ehr/fhirschema"; import { type CodeSystem, isCodeSystem, isValueSet, type ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import type { CanonicalUrl, Name, @@ -86,7 +86,7 @@ const mkPackageAwareResolver = async ( pkg: PackageMeta, deep: number, acc: PackageAwareResolver, - logger?: CodegenLogger, + logger?: Logger, ): Promise => { const pkgId = packageMetaToFhir(pkg); logger?.info(`${" ".repeat(deep * 2)}+ ${pkgId}`); @@ -98,7 +98,8 @@ const mkPackageAwareResolver = async ( if (!rawUrl) continue; if (!(isStructureDefinition(resource) || isValueSet(resource) || isCodeSystem(resource))) continue; const url = rawUrl as CanonicalUrl; - if (index.canonicalResolution[url]) logger?.dryWarn(`Duplicate canonical URL: ${url} at ${pkgId}.`); + if (index.canonicalResolution[url]) + logger?.dryWarn("DUPLICATE_CANONICAL", `Duplicate canonical URL: ${url} at ${pkgId}.`); index.canonicalResolution[url] = [{ deep, pkg: pkg, pkgId, resource: resource as FocusedResource }]; } @@ -118,7 +119,7 @@ const mkPackageAwareResolver = async ( return index; }; -const enrichResolver = (resolver: PackageAwareResolver, logger?: CodegenLogger) => { +const enrichResolver = (resolver: PackageAwareResolver, logger?: Logger) => { for (const { pkg, canonicalResolution } of Object.values(resolver)) { const pkgId = packageMetaToFhir(pkg); if (!resolver[pkgId]) throw new Error(`Package ${pkgId} not found`); @@ -144,11 +145,7 @@ const enrichResolver = (resolver: PackageAwareResolver, logger?: CodegenLogger) } }; -const packageAgnosticResolveCanonical = ( - resolver: PackageAwareResolver, - url: CanonicalUrl, - _logger?: CodegenLogger, -) => { +const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: CanonicalUrl, _logger?: Logger) => { const options = Object.values(resolver).flatMap((pkg) => pkg.canonicalResolution[url]); if (!options) throw new Error(`No canonical resolution found for ${url} in any package`); // if (options.length > 1) @@ -163,7 +160,7 @@ const packageAgnosticResolveCanonical = ( }; export type RegisterConfig = { - logger?: CodegenLogger; + logger?: Logger; focusedPackages?: PackageMeta[]; /** Custom FHIR package registry URL */ registry?: string; @@ -343,7 +340,7 @@ export const registerFromPackageMetas = async ( conf: RegisterConfig, ): Promise => { const packageNames = packageMetas.map(packageMetaToNpm); - conf?.logger?.step(`Loading FHIR packages: ${packageNames.join(", ")}`); + conf?.logger?.info(`Loading FHIR packages: ${packageNames.join(", ")}`); const manager = CanonicalManager({ packages: packageNames, workingDir: ".codegen-cache/canonical-manager-cache", diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 35b214c07..f4aa8c3cb 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -362,7 +362,7 @@ export const enrichValueSet = (vs: ValueSet, packageMeta: PackageMeta): RichValu /////////////////////////////////////////////////////////// export interface TypeschemaGeneratorOptions { - logger?: import("../utils/codegen-logger").CodegenLogger; + logger?: import("../utils/logger").Logger; treeshake?: string[]; manager: ReturnType; /** Custom FHIR package registry URL */ diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 340ec39ed..bb3921309 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -1,6 +1,6 @@ import * as afs from "node:fs/promises"; import * as Path from "node:path"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import * as YAML from "yaml"; import type { IrReport } from "./ir/types"; import type { Register } from "./register"; @@ -199,7 +199,7 @@ export const mkTypeSchemaIndex = ( irReport = {}, }: { register?: Register; - logger?: CodegenLogger; + logger?: Logger; irReport?: IrReport; }, ): TypeSchemaIndex => { @@ -274,6 +274,7 @@ export const mkTypeSchemaIndex = ( const resolved = resolve(base); if (!resolved) { logger?.warn( + "RESOLVE_BASE", `Failed to resolve base type: ${res.map((e) => `${e.identifier.url} (${e.identifier.kind})`).join(", ")}`, ); return undefined; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 000000000..75e247441 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,160 @@ +type TagsOf = L extends Logger ? T : never; + +export type ExtendLogger> = Logger | Extra>; + +export type LogLevel = "info" | "warn" | "error" | "debug"; + +export type LogEntry = { + level: LogLevel; + tag?: T; + message: string; + suppressed: boolean; + prefix: string; + timestamp: number; +}; + +export type LoggerOptions = { + prefix?: string; + suppressTags?: T[]; + level?: LogLevel; +}; + +export type TaggedLogFn = { + (msg: string): void; + (tag: T, msg: string): void; +}; + +export type Logger = { + warn: TaggedLogFn; + dryWarn: TaggedLogFn; + info: TaggedLogFn; + error: TaggedLogFn; + debug: TaggedLogFn; + + fork(prefix: string, opts?: Partial>): Logger; + as(): Logger; + + suppress(...tags: T[]): void; + setLevel(level: LogLevel): void; + tagCounts(): ReadonlyMap; + printSuppressedSummary(): void; + + buffer(): readonly LogEntry[]; + bufferClear(): void; +}; + +const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3 }; + +export function makeLogger(opts: LoggerOptions = {}): Logger { + const prefix = opts.prefix ?? ""; + const suppressedSet = new Set(opts.suppressTags ?? []); + const tagCountsMap = new Map(); + const entries: LogEntry[] = []; + const drySet = new Set(); + let currentLevel: LogLevel = opts.level ?? "info"; + + const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; + + const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`; + const red = (s: string) => `\x1b[31m${s}\x1b[0m`; + const colorize: Record string> = { + debug: (s) => s, + info: (s) => s, + warn: yellow, + error: red, + }; + + const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { + const pfx = prefix ? `[${prefix}] ` : ""; + const tagStr = tag ? `[${tag}] ` : ""; + return colorize[level](`${icon} ${pfx}${tagStr}${msg}`); + }; + + const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { + entries.push({ level, tag, message: msg, suppressed, prefix, timestamp: Date.now() }); + }; + + const parseArgs = (a: string, b?: string): { tag?: T; msg: string } => { + if (b !== undefined) return { tag: a as T, msg: b }; + return { msg: a }; + }; + + const mkLogFn = (level: LogLevel, icon: string, consoleFn: (...args: any[]) => void): TaggedLogFn => { + return ((a: string, b?: string) => { + const { tag, msg } = parseArgs(a, b); + if (tag) tagCountsMap.set(tag, (tagCountsMap.get(tag) ?? 0) + 1); + const isSuppressed = tag !== undefined && suppressedSet.has(tag); + pushEntry(level, msg, tag, isSuppressed); + if (isSuppressed) return; + if (!shouldLog(level)) return; + consoleFn(fmt(level, icon, msg, tag)); + }) as TaggedLogFn; + }; + + const mkDryLogFn = (level: LogLevel, icon: string, consoleFn: (...args: any[]) => void): TaggedLogFn => { + return ((a: string, b?: string) => { + const { tag, msg } = parseArgs(a, b); + if (tag) tagCountsMap.set(tag, (tagCountsMap.get(tag) ?? 0) + 1); + const isSuppressed = tag !== undefined && suppressedSet.has(tag); + pushEntry(level, msg, tag, isSuppressed); + if (isSuppressed) return; + if (!shouldLog(level)) return; + const dedupeKey = `${level}::${tag ?? ""}::${msg}`; + if (drySet.has(dedupeKey)) return; + drySet.add(dedupeKey); + consoleFn(fmt(level, icon, msg, tag)); + }) as TaggedLogFn; + }; + + const logger: Logger = { + warn: mkLogFn("warn", "!", console.warn), + dryWarn: mkDryLogFn("warn", "!", console.warn), + info: mkLogFn("info", "i", console.log), + error: mkLogFn("error", "X", console.error), + debug: mkLogFn("debug", "D", console.log), + + fork(childPrefix: string, childOpts?: Partial>): Logger { + const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; + return makeLogger({ + prefix: fullPrefix, + suppressTags: [...((opts.suppressTags ?? []) as unknown as C[]), ...(childOpts?.suppressTags ?? [])], + level: childOpts?.level ?? currentLevel, + }); + }, + + as(): Logger { + return logger as unknown as Logger; + }, + + suppress(...tags: T[]) { + for (const tag of tags) suppressedSet.add(tag); + }, + + setLevel(level: LogLevel) { + currentLevel = level; + }, + + tagCounts(): ReadonlyMap { + return tagCountsMap; + }, + + printSuppressedSummary() { + const suppressed = [...tagCountsMap.entries()] + .filter(([tag]) => suppressedSet.has(tag)) + .map(([tag, count]) => `${tag}: ${count}`); + if (suppressed.length > 0) { + logger.info(`Suppressed: ${suppressed.join(", ")}`); + } + }, + + buffer(): readonly LogEntry[] { + return entries; + }, + + bufferClear() { + entries.length = 0; + }, + }; + + return logger; +} diff --git a/test/api/mustache.test.ts b/test/api/mustache.test.ts index 02a42c5cb..b1548ed29 100644 --- a/test/api/mustache.test.ts +++ b/test/api/mustache.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("Mustache Template Based Generation", async () => { const report = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .mustache("./examples/mustache/java", { debug: "COMPACT", inMemoryOnly: true, diff --git a/test/api/write-generator/csharp.test.ts b/test/api/write-generator/csharp.test.ts index c4031e3a6..9964d1978 100644 --- a/test/api/write-generator/csharp.test.ts +++ b/test/api/write-generator/csharp.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("C# Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .csharp({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/introspection.test.ts b/test/api/write-generator/introspection.test.ts index 70779a7e0..f68cae92f 100644 --- a/test/api/write-generator/introspection.test.ts +++ b/test/api/write-generator/introspection.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("IntrospectionWriter - Fhir Schema Output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .introspection({ fhirSchemas: "introspection" }) .introspection({ fhirSchemas: "introspection.ndjson" }) .generate(); @@ -28,7 +28,7 @@ describe("IntrospectionWriter - Fhir Schema Output", async () => { describe("IntrospectionWriter - TypeSchema output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -68,7 +68,7 @@ describe("IntrospectionWriter - TypeSchema output", async () => { describe("IntrospectionWriter - typeTree", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -94,7 +94,7 @@ describe("IntrospectionWriter - typeTree", async () => { describe("IntrospectionWriter - StructureDefinition output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { diff --git a/test/api/write-generator/python.test.ts b/test/api/write-generator/python.test.ts index 04e99814d..a288417e1 100644 --- a/test/api/write-generator/python.test.ts +++ b/test/api/write-generator/python.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("Python Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .python({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index f270955aa..367ee50b4 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -1,24 +1,12 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { CodegenLogger, LogLevel } from "@root/utils/codegen-logger"; +import { makeLogger } from "@root/utils/logger"; import { ccdaManager, r4Manager } from "@typeschema-test/utils"; -/** Creates a logger that captures all warnings for testing */ -const createCapturingLogger = () => { - const warnings: string[] = []; - const logger = new CodegenLogger({ level: LogLevel.WARN }); - const originalWarn = logger.warn.bind(logger); - logger.warn = (message: string) => { - warnings.push(message); - originalWarn(message); - }; - return { logger, warnings }; -}; - describe("TypeScript Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("SILENT") + .setLogLevel("error") .typescript({ inMemoryOnly: true, }) @@ -42,7 +30,7 @@ describe("TypeScript Writer Generator", async () => { describe("TypeScript CDA with Logical Model Promotion to Resource", async () => { const result = await new APIBuilder({ register: ccdaManager }) - .setLogLevel("SILENT") + .setLogLevel("error") .typeSchema({ promoteLogical: { "hl7.cda.uv.core": ["http://hl7.org/cda/stds/core/StructureDefinition/Material" as CanonicalUrl], @@ -64,10 +52,9 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () => }); describe("TypeScript R4 Example (with generateProfile)", async () => { - const { logger, warnings } = createCapturingLogger(); + const logger = makeLogger({ level: "error" }); const result = await new APIBuilder({ register: r4Manager, logger }) - .setLogLevel("SILENT") .typescript({ inMemoryOnly: true, withDebugComment: false, @@ -80,9 +67,11 @@ describe("TypeScript R4 Example (with generateProfile)", async () => { expect(result.success).toBeTrue(); }); - it("file rewrite warnings match expected collisions", () => { - const rewriteWarnings = warnings.filter((w) => w.includes("File will be rewritten")); - expect(rewriteWarnings).toMatchSnapshot(); + it("has no file rewrite warnings", () => { + const rewriteWarnings = logger + .buffer() + .filter((e) => e.level === "warn" && e.message.includes("File will be rewritten")); + expect(rewriteWarnings).toEqual([]); }); it("generates bodyweight profile with validate()", () => { diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index ba4bcc086..69f3de1b9 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,7 +2,7 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; -import { type CodegenLogger, createLogger } from "@root/utils/codegen-logger"; +import { type Logger, makeLogger } from "@root/utils/logger"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; @@ -10,9 +10,9 @@ import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } export type PFS = Partial; export type PVS = Partial; -const logger = createLogger({ prefix: "TEST" }); +const logger = makeLogger({ prefix: "TEST" }); -export const mkIndex = async (register: Register, logger?: CodegenLogger) => { +export const mkIndex = async (register: Register, logger?: Logger) => { const { schemas } = await generateTypeSchemas(register, logger); return mkTypeSchemaIndex(schemas, { register, logger }); }; diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts new file mode 100644 index 000000000..8b0e1f9ba --- /dev/null +++ b/test/unit/utils/logger.test.ts @@ -0,0 +1,433 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { type ExtendLogger, type LogEntry, type Logger, makeLogger } from "@root/utils/logger"; + +type BufferFilter = { level?: string; tag?: T; suppressed?: boolean }; + +const bufferFilter = (logger: Logger, filter: BufferFilter): LogEntry[] => + logger.buffer().filter((e) => { + if (filter.level !== undefined && e.level !== filter.level) return false; + if (filter.tag !== undefined && e.tag !== filter.tag) return false; + if (filter.suppressed !== undefined && e.suppressed !== filter.suppressed) return false; + return true; + }); + +type TestTags = "TAG_A" | "TAG_B" | "TAG_C"; + +describe("makeLogger", () => { + let logger: Logger; + + beforeEach(() => { + logger = makeLogger({ prefix: "test" }); + mock.module("console", () => ({})); // silence console in tests + }); + + describe("untagged logging", () => { + it("buffers info messages", () => { + logger.info("hello"); + const entry = logger.buffer()[0]; + expect(entry).toBeDefined(); + expect(entry?.level).toBe("info"); + expect(entry?.message).toBe("hello"); + expect(entry?.tag).toBeUndefined(); + expect(entry?.suppressed).toBe(false); + expect(entry?.prefix).toBe("test"); + }); + + it("untagged messages are never suppressed", () => { + const l = makeLogger({ suppressTags: ["TAG_A", "TAG_B", "TAG_C"] }); + l.info("still visible"); + l.warn("still visible"); + l.error("still visible"); + l.debug("still visible"); + expect(bufferFilter(l, { suppressed: true })).toHaveLength(0); + expect(l.buffer()).toHaveLength(4); + }); + }); + + describe("tagged logging", () => { + it("buffers tagged messages with tag field set", () => { + logger.info("TAG_A", "tagged info"); + const entry = logger.buffer()[0]; + expect(entry).toBeDefined(); + expect(entry?.tag).toBe("TAG_A"); + expect(entry?.message).toBe("tagged info"); + expect(entry?.level).toBe("info"); + }); + + it("works for all log levels", () => { + logger.info("TAG_A", "i"); + logger.warn("TAG_B", "w"); + logger.error("TAG_C", "e"); + logger.debug("TAG_A", "d"); + expect(logger.buffer().map((e) => e.level)).toEqual(["info", "warn", "error", "debug"]); + expect(logger.buffer().every((e) => e.tag !== undefined)).toBe(true); + }); + + it("increments tag counts", () => { + logger.warn("TAG_A", "one"); + logger.warn("TAG_A", "two"); + logger.info("TAG_B", "three"); + expect(logger.tagCounts().get("TAG_A")).toBe(2); + expect(logger.tagCounts().get("TAG_B")).toBe(1); + expect(logger.tagCounts().has("TAG_C")).toBe(false); + }); + + it("does not increment tag counts for untagged messages", () => { + logger.info("no tag"); + expect(logger.tagCounts().size).toBe(0); + }); + }); + + describe("suppression", () => { + it("suppresses tagged messages matching suppressTags", () => { + const l = makeLogger({ suppressTags: ["TAG_A"] }); + l.warn("TAG_A", "suppressed"); + l.warn("TAG_B", "visible"); + + expect(l.buffer()).toHaveLength(2); + expect(l.buffer()[0]?.suppressed).toBe(true); + expect(l.buffer()[1]?.suppressed).toBe(false); + }); + + it("still counts suppressed tags", () => { + const l = makeLogger({ suppressTags: ["TAG_A"] }); + l.warn("TAG_A", "one"); + l.warn("TAG_A", "two"); + expect(l.tagCounts().get("TAG_A")).toBe(2); + }); + + it("suppress() adds tags at runtime", () => { + logger.warn("TAG_B", "before"); + expect(logger.buffer()[0]?.suppressed).toBe(false); + + logger.suppress("TAG_B"); + logger.warn("TAG_B", "after"); + expect(logger.buffer()[1]?.suppressed).toBe(true); + }); + }); + + describe("dryWarn deduplication", () => { + it("deduplicates identical tag+message pairs", () => { + logger.dryWarn("TAG_A", "same"); + logger.dryWarn("TAG_A", "same"); + logger.dryWarn("TAG_A", "same"); + // all 3 buffered + expect(logger.buffer()).toHaveLength(3); + // but only 1 was not suppressed (the first), the rest are deduped at console level + // all are marked suppressed=false since TAG_A is not in suppressTags + expect(bufferFilter(logger, { suppressed: false })).toHaveLength(3); + expect(logger.tagCounts().get("TAG_A")).toBe(3); + }); + + it("different messages are not deduped", () => { + logger.dryWarn("TAG_A", "msg1"); + logger.dryWarn("TAG_A", "msg2"); + expect(logger.buffer()).toHaveLength(2); + }); + + it("same message with different tags are not deduped", () => { + logger.dryWarn("TAG_A", "same"); + logger.dryWarn("TAG_B", "same"); + expect(logger.buffer()).toHaveLength(2); + }); + + it("untagged dryWarn deduplicates by message", () => { + logger.dryWarn("same msg"); + logger.dryWarn("same msg"); + logger.dryWarn("different msg"); + expect(logger.buffer()).toHaveLength(3); + }); + }); + + describe("fork", () => { + it("creates child with combined prefix", () => { + const child = logger.fork("child"); + child.info("hello"); + expect(child.buffer()[0]?.prefix).toBe("test:child"); + }); + + it("creates child from root without parent prefix", () => { + const root = makeLogger({}); + const child = root.fork("child"); + child.info("hello"); + expect(child.buffer()[0]?.prefix).toBe("child"); + }); + + it("inherits parent suppressTags", () => { + const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const child = parent.fork("child"); + child.warn("TAG_A", "inherited suppression"); + expect(child.buffer()[0]?.suppressed).toBe(true); + }); + + it("adds child-specific suppressTags", () => { + const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const child = parent.fork("child", { suppressTags: ["TAG_B"] }); + child.warn("TAG_A", "from parent"); + child.warn("TAG_B", "from child"); + child.warn("TAG_C", "not suppressed"); + expect(bufferFilter(child, { suppressed: true })).toHaveLength(2); + expect(child.buffer()[2]?.suppressed).toBe(false); + }); + + it("child has independent buffer", () => { + const child = logger.fork("child"); + logger.info("parent"); + child.info("child"); + expect(logger.buffer()).toHaveLength(1); + expect(child.buffer()).toHaveLength(1); + expect(logger.buffer()[0]?.message).toBe("parent"); + expect(child.buffer()[0]?.message).toBe("child"); + }); + + it("child has independent tag counts", () => { + const child = logger.fork("child"); + logger.warn("TAG_A", "parent"); + child.warn("TAG_A", "child"); + child.warn("TAG_A", "child2"); + expect(logger.tagCounts().get("TAG_A")).toBe(1); + expect(child.tagCounts().get("TAG_A")).toBe(2); + }); + + it("narrows tag set on fork", () => { + type Narrow = "TAG_A"; + const child = logger.fork("narrow"); + child.warn("TAG_A", "valid"); + expect(child.buffer()[0]?.tag).toBe("TAG_A"); + }); + }); + + describe("as (narrowing)", () => { + it("returns the same logger instance with narrowed type", () => { + type Narrow = "TAG_A" | "TAG_B"; + const narrow = logger.as(); + narrow.warn("TAG_A", "works"); + expect(logger.buffer()).toHaveLength(1); + expect(narrow.buffer()).toHaveLength(1); + }); + + it("narrowed logger inherits suppression from original", () => { + const parent = makeLogger({ suppressTags: ["TAG_A"] }); + type Narrow = "TAG_A"; + const narrow = parent.as(); + narrow.warn("TAG_A", "suppressed via parent"); + expect(narrow.buffer()[0]?.suppressed).toBe(true); + }); + + it("suppress on narrowed logger affects original", () => { + type Narrow = "TAG_A" | "TAG_B"; + const narrow = logger.as(); + narrow.suppress("TAG_A"); + logger.warn("TAG_A", "should be suppressed"); + expect(logger.buffer()[0]?.suppressed).toBe(true); + }); + }); + + describe("ExtendLogger (extending)", () => { + type BaseTags = "BASE_A" | "BASE_B"; + type ExtraTags = "EXTRA_X" | "EXTRA_Y"; + type Combined = ExtendLogger>; + + it("extended logger accepts both base and extra tags", () => { + const l: Combined = makeLogger({}); + l.warn("BASE_A", "base tag"); + l.warn("EXTRA_X", "extra tag"); + expect(l.buffer()).toHaveLength(2); + expect(l.buffer()[0]?.tag).toBe("BASE_A"); + expect(l.buffer()[1]?.tag).toBe("EXTRA_X"); + }); + + it("extended logger suppresses both base and extra tags", () => { + const l: Combined = makeLogger({ + suppressTags: ["BASE_A", "EXTRA_X"], + }); + l.warn("BASE_A", "suppressed base"); + l.warn("BASE_B", "visible base"); + l.warn("EXTRA_X", "suppressed extra"); + l.warn("EXTRA_Y", "visible extra"); + expect(bufferFilter(l, { suppressed: true })).toHaveLength(2); + expect(bufferFilter(l, { suppressed: false })).toHaveLength(2); + }); + + it("base logger can be passed where extended is expected via as()", () => { + const base = makeLogger({}); + const extended = base.as(); + extended.warn("EXTRA_X", "works at runtime"); + expect(base.buffer()).toHaveLength(1); + expect(extended.buffer()[0]?.tag).toBe("EXTRA_X"); + }); + + it("fork from extended logger can narrow to base tags", () => { + const extended: Combined = makeLogger({ + prefix: "root", + suppressTags: ["BASE_A"], + }); + const child = extended.fork("child"); + child.warn("BASE_A", "suppressed from parent"); + child.warn("BASE_B", "visible"); + expect(bufferFilter(child, { suppressed: true })).toHaveLength(1); + expect(child.buffer()[0]?.tag).toBe("BASE_A"); + expect(child.buffer()[1]?.prefix).toBe("root:child"); + }); + }); + + describe("buffer", () => { + it("returns entries in insertion order", () => { + logger.info("first"); + logger.warn("second"); + logger.error("third"); + expect(logger.buffer().map((e) => e.message)).toEqual(["first", "second", "third"]); + }); + + it("includes timestamp", () => { + const before = Date.now(); + logger.info("timed"); + const after = Date.now(); + const ts = logger.buffer()[0]?.timestamp; + expect(ts).toBeGreaterThanOrEqual(before); + expect(ts).toBeLessThanOrEqual(after); + }); + }); + + describe("bufferFilter", () => { + beforeEach(() => { + const l = makeLogger({ prefix: "f", suppressTags: ["TAG_C"] }); + l.info("untagged info"); + l.warn("TAG_A", "tagged warn"); + l.error("TAG_B", "tagged error"); + l.debug("untagged debug"); + l.info("TAG_C", "suppressed info"); + logger = l; + }); + + it("filters by level", () => { + expect(bufferFilter(logger, { level: "info" })).toHaveLength(2); + expect(bufferFilter(logger, { level: "warn" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "error" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "debug" })).toHaveLength(1); + }); + + it("filters by tag", () => { + expect(bufferFilter(logger, { tag: "TAG_A" })).toHaveLength(1); + expect(bufferFilter(logger, { tag: "TAG_B" })).toHaveLength(1); + expect(bufferFilter(logger, { tag: "TAG_C" })).toHaveLength(1); + }); + + it("filters by suppressed", () => { + expect(bufferFilter(logger, { suppressed: true })).toHaveLength(1); + expect(bufferFilter(logger, { suppressed: false })).toHaveLength(4); + }); + + it("combines filters", () => { + expect(bufferFilter(logger, { level: "info", suppressed: true })).toHaveLength(1); + expect(bufferFilter(logger, { level: "info", suppressed: false })).toHaveLength(1); + expect(bufferFilter(logger, { level: "warn", tag: "TAG_A" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "warn", tag: "TAG_B" })).toHaveLength(0); + }); + }); + + describe("bufferClear", () => { + it("empties the buffer", () => { + logger.info("a"); + logger.warn("b"); + expect(logger.buffer()).toHaveLength(2); + logger.bufferClear(); + expect(logger.buffer()).toHaveLength(0); + }); + + it("does not reset tag counts", () => { + logger.warn("TAG_A", "msg"); + logger.bufferClear(); + expect(logger.tagCounts().get("TAG_A")).toBe(1); + }); + }); + + describe("printSuppressedSummary", () => { + it("emits an info entry with suppressed counts", () => { + const l = makeLogger({ suppressTags: ["TAG_A", "TAG_B"] }); + l.warn("TAG_A", "a1"); + l.warn("TAG_A", "a2"); + l.warn("TAG_B", "b1"); + l.printSuppressedSummary(); + + const summaryEntries = bufferFilter(l, { level: "info" }); + expect(summaryEntries).toHaveLength(1); + expect(summaryEntries[0]?.message).toContain("TAG_A: 2"); + expect(summaryEntries[0]?.message).toContain("TAG_B: 1"); + }); + + it("does nothing when no tags are suppressed", () => { + logger.warn("TAG_A", "visible"); + const countBefore = logger.buffer().length; + logger.printSuppressedSummary(); + expect(logger.buffer()).toHaveLength(countBefore); + }); + }); + + describe("prefix", () => { + it("uses empty prefix by default", () => { + const l = makeLogger({}); + l.info("msg"); + expect(l.buffer()[0]?.prefix).toBe(""); + }); + + it("nests prefixes through multiple forks", () => { + const child = logger.fork("a").fork("b"); + child.info("deep"); + expect(child.buffer()[0]?.prefix).toBe("test:a:b"); + }); + }); + + describe("log level filtering", () => { + it("defaults to info level (debug messages not printed but buffered)", () => { + const l = makeLogger({}); + l.debug("hidden"); + l.info("visible"); + expect(l.buffer()).toHaveLength(2); + }); + + it("filters messages below configured level", () => { + const l = makeLogger({ level: "warn" }); + l.debug("d"); + l.info("i"); + l.warn("w"); + l.error("e"); + // all 4 buffered + expect(l.buffer()).toHaveLength(4); + }); + + it("setLevel changes level at runtime", () => { + const l = makeLogger({ level: "info" }); + l.debug("before"); + l.setLevel("debug"); + l.debug("after"); + // both buffered regardless + expect(l.buffer()).toHaveLength(2); + }); + + it("fork inherits parent level", () => { + const parent = makeLogger({ level: "warn" }); + const child = parent.fork("child"); + child.debug("d"); + child.info("i"); + child.warn("w"); + expect(child.buffer()).toHaveLength(3); + }); + + it("fork can override parent level", () => { + const parent = makeLogger({ level: "warn" }); + const child = parent.fork("child", { level: "debug" }); + child.debug("d"); + expect(child.buffer()).toHaveLength(1); + }); + + it("level filtering works alongside tag suppression", () => { + const l = makeLogger({ level: "warn", suppressTags: ["TAG_A"] }); + l.info("TAG_A", "suppressed + below level"); + l.warn("TAG_A", "suppressed at level"); + l.warn("TAG_B", "visible"); + expect(l.buffer()).toHaveLength(3); + expect(bufferFilter(l, { suppressed: true })).toHaveLength(2); + }); + }); +}); From eff5908eb6cb5a9ccb01adc406391725fb9d979b Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 5 Mar 2026 14:31:56 +0100 Subject: [PATCH 02/32] fix: Use picocolors instead of hardcoded ANSI escapes in logger Respects NO_COLOR, terminal detection, and piped output automatically. --- src/utils/logger.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 75e247441..130df67a2 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,3 +1,5 @@ +import pc from "picocolors"; + type TagsOf = L extends Logger ? T : never; export type ExtendLogger> = Logger | Extra>; @@ -55,13 +57,11 @@ export function makeLogger(opts: LoggerOptions = {}): Logge const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; - const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`; - const red = (s: string) => `\x1b[31m${s}\x1b[0m`; const colorize: Record string> = { debug: (s) => s, info: (s) => s, - warn: yellow, - error: red, + warn: pc.yellow, + error: pc.red, }; const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { From 8630d21566544334cb8d617ba80391db7b0ff5b9 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 5 Mar 2026 14:54:41 +0100 Subject: [PATCH 03/32] Consolidate mkLogFn and mkDryLogFn into single factory with dedupe flag --- src/utils/logger.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 130df67a2..4d07d3a8e 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -79,7 +79,12 @@ export function makeLogger(opts: LoggerOptions = {}): Logge return { msg: a }; }; - const mkLogFn = (level: LogLevel, icon: string, consoleFn: (...args: any[]) => void): TaggedLogFn => { + const mkLogFn = ( + level: LogLevel, + icon: string, + consoleFn: (...args: any[]) => void, + dedupe = false, + ): TaggedLogFn => { return ((a: string, b?: string) => { const { tag, msg } = parseArgs(a, b); if (tag) tagCountsMap.set(tag, (tagCountsMap.get(tag) ?? 0) + 1); @@ -87,28 +92,18 @@ export function makeLogger(opts: LoggerOptions = {}): Logge pushEntry(level, msg, tag, isSuppressed); if (isSuppressed) return; if (!shouldLog(level)) return; - consoleFn(fmt(level, icon, msg, tag)); - }) as TaggedLogFn; - }; - - const mkDryLogFn = (level: LogLevel, icon: string, consoleFn: (...args: any[]) => void): TaggedLogFn => { - return ((a: string, b?: string) => { - const { tag, msg } = parseArgs(a, b); - if (tag) tagCountsMap.set(tag, (tagCountsMap.get(tag) ?? 0) + 1); - const isSuppressed = tag !== undefined && suppressedSet.has(tag); - pushEntry(level, msg, tag, isSuppressed); - if (isSuppressed) return; - if (!shouldLog(level)) return; - const dedupeKey = `${level}::${tag ?? ""}::${msg}`; - if (drySet.has(dedupeKey)) return; - drySet.add(dedupeKey); + if (dedupe) { + const key = `${level}::${tag ?? ""}::${msg}`; + if (drySet.has(key)) return; + drySet.add(key); + } consoleFn(fmt(level, icon, msg, tag)); }) as TaggedLogFn; }; const logger: Logger = { warn: mkLogFn("warn", "!", console.warn), - dryWarn: mkDryLogFn("warn", "!", console.warn), + dryWarn: mkLogFn("warn", "!", console.warn, true), info: mkLogFn("info", "i", console.log), error: mkLogFn("error", "X", console.error), debug: mkLogFn("debug", "D", console.log), From ae32557ce50304c74991f6b6638cfd77fcb9c263 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 5 Mar 2026 16:28:03 +0100 Subject: [PATCH 04/32] fix: fork() inherits runtime suppress() calls and simplifies type cast Read from live suppressedSet instead of initial opts.suppressTags, so tags added via .suppress() after construction are inherited by children. --- src/utils/logger.ts | 3 ++- test/unit/utils/logger.test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 4d07d3a8e..7106cf17c 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -110,9 +110,10 @@ export function makeLogger(opts: LoggerOptions = {}): Logge fork(childPrefix: string, childOpts?: Partial>): Logger { const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; + const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as C[]; return makeLogger({ prefix: fullPrefix, - suppressTags: [...((opts.suppressTags ?? []) as unknown as C[]), ...(childOpts?.suppressTags ?? [])], + suppressTags: merged, level: childOpts?.level ?? currentLevel, }); }, diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts index 8b0e1f9ba..c9f6d1e44 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/logger.test.ts @@ -195,6 +195,17 @@ describe("makeLogger", () => { child.warn("TAG_A", "valid"); expect(child.buffer()[0]?.tag).toBe("TAG_A"); }); + + it("inherits runtime suppress() calls", () => { + const parent = makeLogger({ suppressTags: ["TAG_A"] }); + parent.suppress("TAG_B"); + const child = parent.fork("child"); + child.warn("TAG_A", "from init"); + child.warn("TAG_B", "from runtime suppress"); + child.warn("TAG_C", "not suppressed"); + expect(bufferFilter(child, { suppressed: true })).toHaveLength(2); + expect(child.buffer()[2]?.suppressed).toBe(false); + }); }); describe("as (narrowing)", () => { From 842d0675466abf1edc9bd0f96cde881e072446d5 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 5 Mar 2026 16:38:24 +0100 Subject: [PATCH 05/32] Add silent log level to suppress all console output Adds "silent" to LogLevel union. Existing tests using "SILENT" updated to match the lowercase convention. --- src/utils/logger.ts | 5 +++-- test/api/write-generator/multi-package/cda.test.ts | 6 +++--- .../write-generator/multi-package/local-package.test.ts | 6 +++--- .../write-generator/multi-package/sql-on-fhir.test.ts | 6 +++--- test/unit/utils/logger.test.ts | 9 +++++++++ 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 7106cf17c..e14930239 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,7 +4,7 @@ type TagsOf = L extends Logger ? T : never; export type ExtendLogger> = Logger | Extra>; -export type LogLevel = "info" | "warn" | "error" | "debug"; +export type LogLevel = "info" | "warn" | "error" | "debug" | "silent"; export type LogEntry = { level: LogLevel; @@ -45,7 +45,7 @@ export type Logger = { bufferClear(): void; }; -const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3 }; +const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; export function makeLogger(opts: LoggerOptions = {}): Logger { const prefix = opts.prefix ?? ""; @@ -62,6 +62,7 @@ export function makeLogger(opts: LoggerOptions = {}): Logge info: (s) => s, warn: pc.yellow, error: pc.red, + silent: (s) => s, }; const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { diff --git a/test/api/write-generator/multi-package/cda.test.ts b/test/api/write-generator/multi-package/cda.test.ts index f1daf1a27..f9cff5e13 100644 --- a/test/api/write-generator/multi-package/cda.test.ts +++ b/test/api/write-generator/multi-package/cda.test.ts @@ -19,7 +19,7 @@ describe("CDA", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -46,7 +46,7 @@ describe("CDA", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -70,7 +70,7 @@ describe("CDA", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/local-package.test.ts b/test/api/write-generator/multi-package/local-package.test.ts index e3a8e9594..9628e056e 100644 --- a/test/api/write-generator/multi-package/local-package.test.ts +++ b/test/api/write-generator/multi-package/local-package.test.ts @@ -30,7 +30,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -62,7 +62,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -99,7 +99,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/sql-on-fhir.test.ts b/test/api/write-generator/multi-package/sql-on-fhir.test.ts index 29228e42d..4fd1a3f93 100644 --- a/test/api/write-generator/multi-package/sql-on-fhir.test.ts +++ b/test/api/write-generator/multi-package/sql-on-fhir.test.ts @@ -21,7 +21,7 @@ describe("SQL-on-FHIR", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -55,7 +55,7 @@ describe("SQL-on-FHIR", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -86,7 +86,7 @@ describe("SQL-on-FHIR", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("SILENT") + .setLogLevel("silent") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts index c9f6d1e44..d5a0b6741 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/logger.test.ts @@ -440,5 +440,14 @@ describe("makeLogger", () => { expect(l.buffer()).toHaveLength(3); expect(bufferFilter(l, { suppressed: true })).toHaveLength(2); }); + + it("silent level suppresses all console output but still buffers", () => { + const l = makeLogger({ level: "silent" }); + l.debug("d"); + l.info("i"); + l.warn("w"); + l.error("e"); + expect(l.buffer()).toHaveLength(4); + }); }); }); From 7f2d4071199e2dc048a8a5657f47e523f123a9bf Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 14:20:30 +0100 Subject: [PATCH 06/32] Fix Logger type mismatch in profile-extensions.ts --- src/typeschema/core/profile-extensions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index c72105850..5f7db6987 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -7,7 +7,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { CodegenLogger } from "@root/utils/codegen-logger"; +import type { Logger } from "@root/utils/logger"; import { type CanonicalUrl, concatIdentifiers, @@ -24,7 +24,7 @@ const extractExtensionValueTypes = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: CodegenLogger, + logger?: Logger, ): Identifier[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -42,7 +42,7 @@ const extractExtensionValueTypes = ( const extractLegacySubExtensions = ( register: Register, extensionSchema: RichFHIRSchema, - logger?: CodegenLogger, + logger?: Logger, ): ExtensionSubField[] => { const subExtensions: ExtensionSubField[] = []; if (!extensionSchema.elements) return subExtensions; @@ -114,7 +114,7 @@ const extractSubExtensions = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: CodegenLogger, + logger?: Logger, ): ExtensionSubField[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -129,7 +129,7 @@ const extractSubExtensions = ( export const extractProfileExtensions = ( register: Register, fhirSchema: RichFHIRSchema, - logger?: CodegenLogger, + logger?: Logger, ): ProfileExtension[] | undefined => { const extensions: ProfileExtension[] = []; From e1acdddebae24456613d32d790c67e88696f7020 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 14:27:16 +0100 Subject: [PATCH 07/32] Fix file rewrite warnings test to use snapshot --- .../write-generator/__snapshots__/typescript.test.ts.snap | 2 +- test/api/write-generator/typescript.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index 4def252bc..15cb76e8c 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -276,7 +276,7 @@ export const isMaterial = (resource: unknown): resource is Material => { " `; -exports[`TypeScript R4 Example (with generateProfile) file rewrite warnings match expected collisions 1`] = ` +exports[`TypeScript R4 Example (with generateProfile) file rewrite warnings 1`] = ` [ "File will be rewritten 'generated/types/hl7-fhir-r4-core/profiles/Extension_assertedDate.ts'", "File will be rewritten 'generated/types/hl7-fhir-r4-core/profiles/Extension_author.ts'", diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 367ee50b4..15cf5a532 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -67,11 +67,12 @@ describe("TypeScript R4 Example (with generateProfile)", async () => { expect(result.success).toBeTrue(); }); - it("has no file rewrite warnings", () => { + it("file rewrite warnings", () => { const rewriteWarnings = logger .buffer() - .filter((e) => e.level === "warn" && e.message.includes("File will be rewritten")); - expect(rewriteWarnings).toEqual([]); + .filter((e) => e.level === "warn" && e.message.includes("File will be rewritten")) + .map((e) => e.message); + expect(rewriteWarnings).toMatchSnapshot(); }); it("generates bodyweight profile with validate()", () => { From 4d54ae43c85072533283fa95c2026892f56a1bcc Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 14:39:43 +0100 Subject: [PATCH 08/32] Remove codegen-logger.ts, migrate CLI to Logger + cli-fmt.ts --- src/cli/commands/index.ts | 54 +--- src/cli/commands/typeschema.ts | 13 +- src/cli/commands/typeschema/generate.ts | 2 +- src/utils/cli-fmt.ts | 23 ++ src/utils/codegen-logger.ts | 316 ------------------------ 5 files changed, 41 insertions(+), 367 deletions(-) create mode 100644 src/utils/cli-fmt.ts delete mode 100644 src/utils/codegen-logger.ts diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 91e444b7f..7a4254f67 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -6,7 +6,9 @@ * Modern CLI with subcommands for typeschema and code generation */ -import { configure, error, header, LogLevel } from "@root/utils/codegen-logger"; +import { header } from "@root/utils/cli-fmt"; +import type { LogLevel } from "@root/utils/logger"; +import { makeLogger } from "@root/utils/logger"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { typeschemaCommand } from "./typeschema"; @@ -20,42 +22,11 @@ export interface CLIArgv { logLevel?: "debug" | "info" | "warn" | "error" | "silent"; } -/** - * Map string log level to LogLevel enum - */ -function parseLogLevel(level: string | undefined): LogLevel | undefined { - if (!level) return undefined; - const levelMap: Record = { - debug: LogLevel.DEBUG, - info: LogLevel.INFO, - warn: LogLevel.WARN, - error: LogLevel.ERROR, - silent: LogLevel.SILENT, - }; - return levelMap[level.toLowerCase()]; -} +const cliLogger = makeLogger({ prefix: "cli" }); -/** - * Middleware to setup logging - */ async function setupLoggingMiddleware(argv: any) { - // Determine log level: explicit --log-level takes precedence over --verbose/--debug - let level = parseLogLevel(argv.logLevel); - - // If no explicit log level, use --verbose or --debug as shortcuts - if (level === undefined) { - if (argv.debug || argv.verbose) { - level = LogLevel.DEBUG; - } else { - level = LogLevel.INFO; - } - } - - // Configure the CliLogger with user preferences - configure({ - timestamp: argv.debug, - level, - }); + const level: LogLevel = argv.logLevel ?? (argv.debug || argv.verbose ? "debug" : "info"); + cliLogger.setLevel(level); } /** @@ -110,13 +81,8 @@ export function createCLI() { "Generate TypeSchemas from FHIR package", ) .fail((msg, err, _yargs) => { - if (err) { - error(err.message, err); - } else { - error(msg); - } - - error("\nUse --help for usage information"); + cliLogger.error(err ? err.message : msg); + cliLogger.error("Use --help for usage information"); process.exit(1); }) .wrap(Math.min(120, process.stdout.columns || 80)); @@ -132,8 +98,8 @@ export async function runCLI() { // Run CLI if this file is executed directly if (import.meta.main) { - runCLI().catch((error) => { - error("Unexpected error:", error); + runCLI().catch((err) => { + cliLogger.error(String(err)); process.exit(1); }); } diff --git a/src/cli/commands/typeschema.ts b/src/cli/commands/typeschema.ts index 37863c16e..6dd22816f 100644 --- a/src/cli/commands/typeschema.ts +++ b/src/cli/commands/typeschema.ts @@ -4,13 +4,16 @@ * Commands for validating and managing TypeSchema files */ -import { error, info, list } from "@root/utils/codegen-logger"; +import { list } from "@root/utils/cli-fmt"; +import { makeLogger } from "@root/utils/logger"; import type { CommandModule } from "yargs"; import { generateTypeschemaCommand } from "./typeschema/generate"; /** * TypeSchema command group */ +const logger = makeLogger({ prefix: "typeschema" }); + export const typeschemaCommand: CommandModule = { command: "typeschema [subcommand]", describe: "TypeSchema operations - generate, validate and merge schemas", @@ -21,9 +24,8 @@ export const typeschemaCommand: CommandModule = { .example("$0 typeschema generate hl7.fhir.r4.core@4.0.1", "Generate TypeSchema from FHIR R4 core package"); }, handler: (argv: any) => { - // If no subcommand provided, show available subcommands if (!argv.subcommand && argv._.length === 1) { - info("Available typeschema subcommands:"); + logger.info("Available typeschema subcommands:"); list(["generate Generate TypeSchema files from FHIR packages"]); console.log( "\nUse 'atomic-codegen typeschema --help' for more information about a subcommand.", @@ -37,10 +39,9 @@ export const typeschemaCommand: CommandModule = { return; } - // If unknown subcommand provided, show error and available commands if (argv.subcommand && !["generate", "validate", "merge"].includes(argv.subcommand)) { - error(`Unknown typeschema subcommand: ${argv.subcommand}\n`); - info("Available typeschema subcommands:"); + logger.error(`Unknown typeschema subcommand: ${argv.subcommand}`); + logger.info("Available typeschema subcommands:"); list([ "generate Generate TypeSchema files from FHIR packages", "validate Validate TypeSchema files for correctness and consistency", diff --git a/src/cli/commands/typeschema/generate.ts b/src/cli/commands/typeschema/generate.ts index efe290664..22cfc10e2 100644 --- a/src/cli/commands/typeschema/generate.ts +++ b/src/cli/commands/typeschema/generate.ts @@ -6,7 +6,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; -import { complete, list } from "@root/utils/codegen-logger"; +import { complete, list } from "@root/utils/cli-fmt"; import { makeLogger } from "@root/utils/logger"; import { generateTypeSchemas } from "@typeschema/index"; import { registerFromPackageMetas } from "@typeschema/register"; diff --git a/src/utils/cli-fmt.ts b/src/utils/cli-fmt.ts new file mode 100644 index 000000000..dc0b08ba4 --- /dev/null +++ b/src/utils/cli-fmt.ts @@ -0,0 +1,23 @@ +import pc from "picocolors"; + +export const header = (title: string): void => { + console.log(); + console.log(pc.cyan(pc.bold(`━━━ ${title} ━━━`))); +}; + +export const complete = (message: string, duration?: number, stats?: Record): void => { + let msg = message; + if (duration) msg += ` ${pc.gray(`(${duration}ms)`)}`; + console.log(`${pc.green("")} ${msg}`); + if (stats) { + for (const [key, value] of Object.entries(stats)) { + console.log(pc.gray(` ${key}: ${value}`)); + } + } +}; + +export const list = (items: string[], bullet = "•"): void => { + for (const item of items) { + console.log(pc.gray(` ${bullet} ${item}`)); + } +}; diff --git a/src/utils/codegen-logger.ts b/src/utils/codegen-logger.ts deleted file mode 100644 index f13c3a96a..000000000 --- a/src/utils/codegen-logger.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * CodeGen Logger - * - * Clean, colorful logging designed for code generation tools - */ - -import pc from "picocolors"; - -export enum LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - SILENT = 4, -} - -export type LogLevelString = keyof typeof LogLevel; - -export const parseLogLevel = (level: LogLevelString): LogLevel => { - switch (level.toUpperCase()) { - case "DEBUG": - return LogLevel.DEBUG; - case "INFO": - return LogLevel.INFO; - case "WARN": - return LogLevel.WARN; - case "ERROR": - return LogLevel.ERROR; - case "SILENT": - return LogLevel.SILENT; - default: - throw new Error(`Invalid log level: ${level}`); - } -}; - -export interface LogOptions { - prefix?: string; - timestamp?: boolean; - suppressLoggingLevel?: LogLevel[] | "all"; - /** Minimum log level to display. Messages below this level are suppressed. Default: INFO */ - level?: LogLevel; -} - -/** - * Simple code generation logger with pretty colors and clean formatting - */ -export class CodegenLogger { - private options: LogOptions; - private dryWarnSet: Set = new Set(); - - constructor(options: LogOptions = {}) { - this.options = { - timestamp: false, - level: LogLevel.INFO, - ...options, - }; - } - - /** - * Check if a message at the given level should be logged - */ - private shouldLog(messageLevel: LogLevel): boolean { - const currentLevel = this.options.level ?? LogLevel.INFO; - return messageLevel >= currentLevel; - } - - private static consoleLevelsMap: Record void> = { - [LogLevel.INFO]: console.log, - [LogLevel.WARN]: console.warn, - [LogLevel.ERROR]: console.error, - [LogLevel.DEBUG]: console.log, - [LogLevel.SILENT]: () => {}, - }; - - private formatMessage(level: string, message: string, color: (str: string) => string): string { - const timestamp = this.options.timestamp ? `${pc.gray(new Date().toLocaleTimeString())} ` : ""; - const prefix = this.options.prefix ? `${pc.cyan(`[${this.options.prefix}]`)} ` : ""; - return `${timestamp}${color(level)} ${prefix}${message}`; - } - - private isSuppressed(level: LogLevel): boolean { - return ( - this.options.suppressLoggingLevel === "all" || this.options.suppressLoggingLevel?.includes(level) || false - ); - } - - private tryWriteToConsole(level: LogLevel, formattedMessage: string): void { - if (this.isSuppressed(level)) return; - if (!this.shouldLog(level)) return; - const logFn = CodegenLogger.consoleLevelsMap[level] || console.log; - logFn(formattedMessage); - } - - /** - * Success message with checkmark - */ - success(message: string): void { - this.tryWriteToConsole(LogLevel.INFO, this.formatMessage("", message, pc.green)); - } - - /** - * Error message with X mark - */ - error(message: string, error?: Error): void { - if (this.isSuppressed(LogLevel.ERROR)) return; - if (!this.shouldLog(LogLevel.ERROR)) return; - console.error(this.formatMessage("X", message, pc.red)); - // Show error details if verbose or log level is DEBUG - const showDetails = this.options.level === LogLevel.DEBUG; - if (error && showDetails) { - console.error(pc.red(` ${error.message}`)); - if (error.stack) { - console.error(pc.gray(error.stack)); - } - } - } - - /** - * Warning message with warning sign - */ - warn(message: string): void { - this.tryWriteToConsole(LogLevel.WARN, this.formatMessage("!", message, pc.yellow)); - } - - dryWarn(message: string): void { - if (!this.dryWarnSet.has(message)) { - this.warn(message); - this.dryWarnSet.add(message); - } - } - - /** - * Info message with info icon - */ - info(message: string): void { - this.tryWriteToConsole(LogLevel.INFO, this.formatMessage("i", message, pc.blue)); - } - - /** - * Debug message (only shows when log level is DEBUG or verbose is true) - */ - debug(message: string): void { - // Debug shows if verbose is true OR log level allows DEBUG - if (this.shouldLog(LogLevel.DEBUG)) { - this.tryWriteToConsole(LogLevel.DEBUG, this.formatMessage("🐛", message, pc.magenta)); - } - } - - /** - * Step message with rocket - */ - step(message: string): void { - this.tryWriteToConsole(LogLevel.INFO, this.formatMessage("🚀", message, pc.cyan)); - } - - /** - * Progress message with clock - */ - progress(message: string): void { - this.tryWriteToConsole(LogLevel.INFO, this.formatMessage("⏳", message, pc.blue)); - } - - /** - * Plain message (no icon, just colored text) - */ - plain(message: string, color: (str: string) => string = (s) => s): void { - const timestamp = this.options.timestamp ? `${pc.gray(new Date().toLocaleTimeString())} ` : ""; - const prefix = this.options.prefix ? `${pc.cyan(`[${this.options.prefix}]`)} ` : ""; - this.tryWriteToConsole(LogLevel.INFO, `${timestamp}${prefix}${color(message)}`); - } - - /** - * Dimmed/gray text for less important info - */ - dim(message: string): void { - this.plain(message, pc.gray); - } - - /** - * Create a child logger with a prefix - */ - child(prefix: string): CodegenLogger { - return new CodegenLogger({ - ...this.options, - prefix: this.options.prefix ? `${this.options.prefix}:${prefix}` : prefix, - }); - } - - /** - * Update options - */ - configure(options: Partial): void { - this.options = { ...this.options, ...options }; - } - - getLevel(): LogLevel { - return this.options.level ?? LogLevel.INFO; - } - - setLevel(level: LogLevel): void { - this.options.level = level; - } -} - -/** - * Quick logging functions for simple usage - */ - -const defaultLogger = new CodegenLogger(); - -export function success(message: string): void { - defaultLogger.success(message); -} - -export function error(message: string, err?: Error): void { - defaultLogger.error(message, err); -} - -export function warn(message: string): void { - defaultLogger.warn(message); -} - -export function info(message: string): void { - defaultLogger.info(message); -} - -function _debug(message: string): void { - defaultLogger.debug(message); -} - -export function step(message: string): void { - defaultLogger.step(message); -} - -function _progress(message: string): void { - defaultLogger.progress(message); -} - -function _plain(message: string, color?: (str: string) => string): void { - defaultLogger.plain(message, color); -} - -export function dim(message: string): void { - defaultLogger.dim(message); -} - -/** - * Configure the default logger - */ -export function configure(options: Partial): void { - defaultLogger.configure(options); -} - -/** - * Create a new logger instance - */ -export function createLogger(options: LogOptions = {}): CodegenLogger { - return new CodegenLogger(options); -} - -/** - * Convenience functions for common CLI patterns - */ - -/** - * Show a command header with separator - */ -export function header(title: string): void { - console.log(); - console.log(pc.cyan(pc.bold(`━━━ ${title} ━━━`))); -} - -/** - * Show a section break - */ -function _section(title: string): void { - console.log(); - console.log(pc.bold(title)); -} - -/** - * Show completion message with stats - */ -export function complete(message: string, duration?: number, stats?: Record): void { - let msg = message; - if (duration) { - msg += ` ${pc.gray(`(${duration}ms)`)}`; - } - success(msg); - - if (stats) { - Object.entries(stats).forEach(([key, value]) => { - dim(` ${key}: ${value}`); - }); - } -} - -/** - * Show a list of items - */ -export function list(items: string[], bullet = "•"): void { - items.forEach((item) => { - console.log(pc.gray(` ${bullet} ${item}`)); - }); -} - -/** - * Show key-value pairs - */ -function _table(data: Record): void { - const maxKeyLength = Math.max(...Object.keys(data).map((k) => k.length)); - Object.entries(data).forEach(([key, value]) => { - const paddedKey = key.padEnd(maxKeyLength); - console.log(` ${pc.blue(paddedKey)} ${pc.gray("─")} ${value}`); - }); -} From ab8ee020f6094b2e473a9c4d149fa1599ab0f293 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 14:47:21 +0100 Subject: [PATCH 09/32] Add uppercase LogLevel variants, normalize internally to uppercase --- src/utils/logger.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index e14930239..f23fb42c7 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,7 +4,7 @@ type TagsOf = L extends Logger ? T : never; export type ExtendLogger> = Logger | Extra>; -export type LogLevel = "info" | "warn" | "error" | "debug" | "silent"; +export type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG" | "SILENT" | "info" | "warn" | "error" | "debug" | "silent"; export type LogEntry = { level: LogLevel; @@ -45,7 +45,10 @@ export type Logger = { bufferClear(): void; }; -const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; +type UpperLogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; +const normalizeLevel = (level: LogLevel): UpperLogLevel => level.toUpperCase() as UpperLogLevel; + +const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; export function makeLogger(opts: LoggerOptions = {}): Logger { const prefix = opts.prefix ?? ""; @@ -55,20 +58,21 @@ export function makeLogger(opts: LoggerOptions = {}): Logge const drySet = new Set(); let currentLevel: LogLevel = opts.level ?? "info"; - const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; + const shouldLog = (level: LogLevel): boolean => + LEVEL_PRIORITY[normalizeLevel(level)] >= LEVEL_PRIORITY[normalizeLevel(currentLevel)]; - const colorize: Record string> = { - debug: (s) => s, - info: (s) => s, - warn: pc.yellow, - error: pc.red, - silent: (s) => s, + const colorize: Record string> = { + DEBUG: (s) => s, + INFO: (s) => s, + WARN: pc.yellow, + ERROR: pc.red, + SILENT: (s) => s, }; const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { const pfx = prefix ? `[${prefix}] ` : ""; const tagStr = tag ? `[${tag}] ` : ""; - return colorize[level](`${icon} ${pfx}${tagStr}${msg}`); + return colorize[normalizeLevel(level)](`${icon} ${pfx}${tagStr}${msg}`); }; const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { From 65c1ce1e4adb589e6a5775a73e6ee7f21ef6bbc7 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 15:01:38 +0100 Subject: [PATCH 10/32] Simplify logger: tuple rest params for TaggedLogFn, Record for tagCounts --- src/utils/logger.ts | 29 +++++++++++------------------ test/unit/utils/logger.test.ts | 18 +++++++++--------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index f23fb42c7..878cf812b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -21,10 +21,7 @@ export type LoggerOptions = { level?: LogLevel; }; -export type TaggedLogFn = { - (msg: string): void; - (tag: T, msg: string): void; -}; +export type TaggedLogFn = (...args: [string] | [T, string]) => void; export type Logger = { warn: TaggedLogFn; @@ -38,7 +35,7 @@ export type Logger = { suppress(...tags: T[]): void; setLevel(level: LogLevel): void; - tagCounts(): ReadonlyMap; + tagCounts(): Readonly>; printSuppressedSummary(): void; buffer(): readonly LogEntry[]; @@ -53,7 +50,7 @@ const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: export function makeLogger(opts: LoggerOptions = {}): Logger { const prefix = opts.prefix ?? ""; const suppressedSet = new Set(opts.suppressTags ?? []); - const tagCountsMap = new Map(); + const tagCounts: Record = {}; const entries: LogEntry[] = []; const drySet = new Set(); let currentLevel: LogLevel = opts.level ?? "info"; @@ -79,20 +76,16 @@ export function makeLogger(opts: LoggerOptions = {}): Logge entries.push({ level, tag, message: msg, suppressed, prefix, timestamp: Date.now() }); }; - const parseArgs = (a: string, b?: string): { tag?: T; msg: string } => { - if (b !== undefined) return { tag: a as T, msg: b }; - return { msg: a }; - }; - const mkLogFn = ( level: LogLevel, icon: string, consoleFn: (...args: any[]) => void, dedupe = false, ): TaggedLogFn => { - return ((a: string, b?: string) => { - const { tag, msg } = parseArgs(a, b); - if (tag) tagCountsMap.set(tag, (tagCountsMap.get(tag) ?? 0) + 1); + return (...args: [string] | [T, string]) => { + const tag = args.length === 2 ? args[0] : undefined; + const msg = args.length === 2 ? args[1] : args[0]; + if (tag) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1; const isSuppressed = tag !== undefined && suppressedSet.has(tag); pushEntry(level, msg, tag, isSuppressed); if (isSuppressed) return; @@ -103,7 +96,7 @@ export function makeLogger(opts: LoggerOptions = {}): Logge drySet.add(key); } consoleFn(fmt(level, icon, msg, tag)); - }) as TaggedLogFn; + }; }; const logger: Logger = { @@ -135,12 +128,12 @@ export function makeLogger(opts: LoggerOptions = {}): Logge currentLevel = level; }, - tagCounts(): ReadonlyMap { - return tagCountsMap; + tagCounts(): Readonly> { + return tagCounts; }, printSuppressedSummary() { - const suppressed = [...tagCountsMap.entries()] + const suppressed = Object.entries(tagCounts) .filter(([tag]) => suppressedSet.has(tag)) .map(([tag, count]) => `${tag}: ${count}`); if (suppressed.length > 0) { diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts index d5a0b6741..f5a994a9b 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/logger.test.ts @@ -67,14 +67,14 @@ describe("makeLogger", () => { logger.warn("TAG_A", "one"); logger.warn("TAG_A", "two"); logger.info("TAG_B", "three"); - expect(logger.tagCounts().get("TAG_A")).toBe(2); - expect(logger.tagCounts().get("TAG_B")).toBe(1); - expect(logger.tagCounts().has("TAG_C")).toBe(false); + expect(logger.tagCounts()["TAG_A"]).toBe(2); + expect(logger.tagCounts()["TAG_B"]).toBe(1); + expect(logger.tagCounts()["TAG_C"]).toBeUndefined(); }); it("does not increment tag counts for untagged messages", () => { logger.info("no tag"); - expect(logger.tagCounts().size).toBe(0); + expect(Object.keys(logger.tagCounts())).toHaveLength(0); }); }); @@ -93,7 +93,7 @@ describe("makeLogger", () => { const l = makeLogger({ suppressTags: ["TAG_A"] }); l.warn("TAG_A", "one"); l.warn("TAG_A", "two"); - expect(l.tagCounts().get("TAG_A")).toBe(2); + expect(l.tagCounts()["TAG_A"]).toBe(2); }); it("suppress() adds tags at runtime", () => { @@ -116,7 +116,7 @@ describe("makeLogger", () => { // but only 1 was not suppressed (the first), the rest are deduped at console level // all are marked suppressed=false since TAG_A is not in suppressTags expect(bufferFilter(logger, { suppressed: false })).toHaveLength(3); - expect(logger.tagCounts().get("TAG_A")).toBe(3); + expect(logger.tagCounts()["TAG_A"]).toBe(3); }); it("different messages are not deduped", () => { @@ -185,8 +185,8 @@ describe("makeLogger", () => { logger.warn("TAG_A", "parent"); child.warn("TAG_A", "child"); child.warn("TAG_A", "child2"); - expect(logger.tagCounts().get("TAG_A")).toBe(1); - expect(child.tagCounts().get("TAG_A")).toBe(2); + expect(logger.tagCounts()["TAG_A"]).toBe(1); + expect(child.tagCounts()["TAG_A"]).toBe(2); }); it("narrows tag set on fork", () => { @@ -349,7 +349,7 @@ describe("makeLogger", () => { it("does not reset tag counts", () => { logger.warn("TAG_A", "msg"); logger.bufferClear(); - expect(logger.tagCounts().get("TAG_A")).toBe(1); + expect(logger.tagCounts()["TAG_A"]).toBe(1); }); }); From 247e851e620dc105f7d592c6f3e5a3ddb4323a23 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 15:30:59 +0100 Subject: [PATCH 11/32] Rename makeLogger to mkLogger for consistency with codebase convention --- examples/python/generate.ts | 4 +- src/api/builder.ts | 4 +- src/cli/commands/index.ts | 4 +- src/cli/commands/typeschema.ts | 4 +- src/cli/commands/typeschema/generate.ts | 4 +- src/utils/logger.ts | 4 +- test/api/write-generator/typescript.test.ts | 4 +- test/unit/typeschema/utils.ts | 4 +- test/unit/utils/logger.test.ts | 50 ++++++++++----------- 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/examples/python/generate.ts b/examples/python/generate.ts index babd98050..34b59a724 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,9 +1,9 @@ import { APIBuilder, prettyReport } from "../../src"; -import { type Logger, makeLogger } from "../../src/utils/logger"; +import { type Logger, mkLogger } from "../../src/utils/logger"; console.log("📦 Generating FHIR R4 Core Types..."); -const logger: Logger = makeLogger({ +const logger: Logger = mkLogger({ prefix: "API", suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], }); diff --git a/src/api/builder.ts b/src/api/builder.ts index 3b7e5d344..4d4029c8e 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type Logger, type LogLevel, makeLogger } from "@root/utils/logger"; +import { type Logger, type LogLevel, mkLogger } from "@root/utils/logger"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; @@ -173,7 +173,7 @@ export class APIBuilder { dropCache: userOpts.dropCanonicalManagerCache, preprocessPackage: userOpts.preprocessPackage, }); - this.logger = userOpts.logger ?? makeLogger({ prefix: "API", level: opts.logLevel }); + this.logger = userOpts.logger ?? mkLogger({ prefix: "API", level: opts.logLevel }); this.options = opts; } diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 7a4254f67..ad14ce099 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -8,7 +8,7 @@ import { header } from "@root/utils/cli-fmt"; import type { LogLevel } from "@root/utils/logger"; -import { makeLogger } from "@root/utils/logger"; +import { mkLogger } from "@root/utils/logger"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { typeschemaCommand } from "./typeschema"; @@ -22,7 +22,7 @@ export interface CLIArgv { logLevel?: "debug" | "info" | "warn" | "error" | "silent"; } -const cliLogger = makeLogger({ prefix: "cli" }); +const cliLogger = mkLogger({ prefix: "cli" }); async function setupLoggingMiddleware(argv: any) { const level: LogLevel = argv.logLevel ?? (argv.debug || argv.verbose ? "debug" : "info"); diff --git a/src/cli/commands/typeschema.ts b/src/cli/commands/typeschema.ts index 6dd22816f..931f561b8 100644 --- a/src/cli/commands/typeschema.ts +++ b/src/cli/commands/typeschema.ts @@ -5,14 +5,14 @@ */ import { list } from "@root/utils/cli-fmt"; -import { makeLogger } from "@root/utils/logger"; +import { mkLogger } from "@root/utils/logger"; import type { CommandModule } from "yargs"; import { generateTypeschemaCommand } from "./typeschema/generate"; /** * TypeSchema command group */ -const logger = makeLogger({ prefix: "typeschema" }); +const logger = mkLogger({ prefix: "typeschema" }); export const typeschemaCommand: CommandModule = { command: "typeschema [subcommand]", diff --git a/src/cli/commands/typeschema/generate.ts b/src/cli/commands/typeschema/generate.ts index 22cfc10e2..9277bdb8e 100644 --- a/src/cli/commands/typeschema/generate.ts +++ b/src/cli/commands/typeschema/generate.ts @@ -7,7 +7,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { complete, list } from "@root/utils/cli-fmt"; -import { makeLogger } from "@root/utils/logger"; +import { mkLogger } from "@root/utils/logger"; import { generateTypeSchemas } from "@typeschema/index"; import { registerFromPackageMetas } from "@typeschema/register"; import type { PackageMeta } from "@typeschema/types"; @@ -74,7 +74,7 @@ export const generateTypeschemaCommand: CommandModule, G }, }, handler: async (argv) => { - const logger = makeLogger({ + const logger = mkLogger({ prefix: "TypeSchema", }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 878cf812b..0a271f597 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -47,7 +47,7 @@ const normalizeLevel = (level: LogLevel): UpperLogLevel => level.toUpperCase() a const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; -export function makeLogger(opts: LoggerOptions = {}): Logger { +export function mkLogger(opts: LoggerOptions = {}): Logger { const prefix = opts.prefix ?? ""; const suppressedSet = new Set(opts.suppressTags ?? []); const tagCounts: Record = {}; @@ -109,7 +109,7 @@ export function makeLogger(opts: LoggerOptions = {}): Logge fork(childPrefix: string, childOpts?: Partial>): Logger { const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as C[]; - return makeLogger({ + return mkLogger({ prefix: fullPrefix, suppressTags: merged, level: childOpts?.level ?? currentLevel, diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 15cf5a532..205e1ae48 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { makeLogger } from "@root/utils/logger"; +import { mkLogger } from "@root/utils/logger"; import { ccdaManager, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { @@ -52,7 +52,7 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () => }); describe("TypeScript R4 Example (with generateProfile)", async () => { - const logger = makeLogger({ level: "error" }); + const logger = mkLogger({ level: "error" }); const result = await new APIBuilder({ register: r4Manager, logger }) .typescript({ diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index 69f3de1b9..748db3dd7 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,7 +2,7 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; -import { type Logger, makeLogger } from "@root/utils/logger"; +import { type Logger, mkLogger } from "@root/utils/logger"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; @@ -10,7 +10,7 @@ import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } export type PFS = Partial; export type PVS = Partial; -const logger = makeLogger({ prefix: "TEST" }); +const logger = mkLogger({ prefix: "TEST" }); export const mkIndex = async (register: Register, logger?: Logger) => { const { schemas } = await generateTypeSchemas(register, logger); diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts index f5a994a9b..1613b6f01 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/logger.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; -import { type ExtendLogger, type LogEntry, type Logger, makeLogger } from "@root/utils/logger"; +import { type ExtendLogger, type LogEntry, type Logger, mkLogger } from "@root/utils/logger"; type BufferFilter = { level?: string; tag?: T; suppressed?: boolean }; @@ -13,11 +13,11 @@ const bufferFilter = (logger: Logger, filter: BufferFilter< type TestTags = "TAG_A" | "TAG_B" | "TAG_C"; -describe("makeLogger", () => { +describe("mkLogger", () => { let logger: Logger; beforeEach(() => { - logger = makeLogger({ prefix: "test" }); + logger = mkLogger({ prefix: "test" }); mock.module("console", () => ({})); // silence console in tests }); @@ -34,7 +34,7 @@ describe("makeLogger", () => { }); it("untagged messages are never suppressed", () => { - const l = makeLogger({ suppressTags: ["TAG_A", "TAG_B", "TAG_C"] }); + const l = mkLogger({ suppressTags: ["TAG_A", "TAG_B", "TAG_C"] }); l.info("still visible"); l.warn("still visible"); l.error("still visible"); @@ -80,7 +80,7 @@ describe("makeLogger", () => { describe("suppression", () => { it("suppresses tagged messages matching suppressTags", () => { - const l = makeLogger({ suppressTags: ["TAG_A"] }); + const l = mkLogger({ suppressTags: ["TAG_A"] }); l.warn("TAG_A", "suppressed"); l.warn("TAG_B", "visible"); @@ -90,7 +90,7 @@ describe("makeLogger", () => { }); it("still counts suppressed tags", () => { - const l = makeLogger({ suppressTags: ["TAG_A"] }); + const l = mkLogger({ suppressTags: ["TAG_A"] }); l.warn("TAG_A", "one"); l.warn("TAG_A", "two"); expect(l.tagCounts()["TAG_A"]).toBe(2); @@ -147,21 +147,21 @@ describe("makeLogger", () => { }); it("creates child from root without parent prefix", () => { - const root = makeLogger({}); + const root = mkLogger({}); const child = root.fork("child"); child.info("hello"); expect(child.buffer()[0]?.prefix).toBe("child"); }); it("inherits parent suppressTags", () => { - const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const parent = mkLogger({ suppressTags: ["TAG_A"] }); const child = parent.fork("child"); child.warn("TAG_A", "inherited suppression"); expect(child.buffer()[0]?.suppressed).toBe(true); }); it("adds child-specific suppressTags", () => { - const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const parent = mkLogger({ suppressTags: ["TAG_A"] }); const child = parent.fork("child", { suppressTags: ["TAG_B"] }); child.warn("TAG_A", "from parent"); child.warn("TAG_B", "from child"); @@ -197,7 +197,7 @@ describe("makeLogger", () => { }); it("inherits runtime suppress() calls", () => { - const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const parent = mkLogger({ suppressTags: ["TAG_A"] }); parent.suppress("TAG_B"); const child = parent.fork("child"); child.warn("TAG_A", "from init"); @@ -218,7 +218,7 @@ describe("makeLogger", () => { }); it("narrowed logger inherits suppression from original", () => { - const parent = makeLogger({ suppressTags: ["TAG_A"] }); + const parent = mkLogger({ suppressTags: ["TAG_A"] }); type Narrow = "TAG_A"; const narrow = parent.as(); narrow.warn("TAG_A", "suppressed via parent"); @@ -240,7 +240,7 @@ describe("makeLogger", () => { type Combined = ExtendLogger>; it("extended logger accepts both base and extra tags", () => { - const l: Combined = makeLogger({}); + const l: Combined = mkLogger({}); l.warn("BASE_A", "base tag"); l.warn("EXTRA_X", "extra tag"); expect(l.buffer()).toHaveLength(2); @@ -249,7 +249,7 @@ describe("makeLogger", () => { }); it("extended logger suppresses both base and extra tags", () => { - const l: Combined = makeLogger({ + const l: Combined = mkLogger({ suppressTags: ["BASE_A", "EXTRA_X"], }); l.warn("BASE_A", "suppressed base"); @@ -261,7 +261,7 @@ describe("makeLogger", () => { }); it("base logger can be passed where extended is expected via as()", () => { - const base = makeLogger({}); + const base = mkLogger({}); const extended = base.as(); extended.warn("EXTRA_X", "works at runtime"); expect(base.buffer()).toHaveLength(1); @@ -269,7 +269,7 @@ describe("makeLogger", () => { }); it("fork from extended logger can narrow to base tags", () => { - const extended: Combined = makeLogger({ + const extended: Combined = mkLogger({ prefix: "root", suppressTags: ["BASE_A"], }); @@ -302,7 +302,7 @@ describe("makeLogger", () => { describe("bufferFilter", () => { beforeEach(() => { - const l = makeLogger({ prefix: "f", suppressTags: ["TAG_C"] }); + const l = mkLogger({ prefix: "f", suppressTags: ["TAG_C"] }); l.info("untagged info"); l.warn("TAG_A", "tagged warn"); l.error("TAG_B", "tagged error"); @@ -355,7 +355,7 @@ describe("makeLogger", () => { describe("printSuppressedSummary", () => { it("emits an info entry with suppressed counts", () => { - const l = makeLogger({ suppressTags: ["TAG_A", "TAG_B"] }); + const l = mkLogger({ suppressTags: ["TAG_A", "TAG_B"] }); l.warn("TAG_A", "a1"); l.warn("TAG_A", "a2"); l.warn("TAG_B", "b1"); @@ -377,7 +377,7 @@ describe("makeLogger", () => { describe("prefix", () => { it("uses empty prefix by default", () => { - const l = makeLogger({}); + const l = mkLogger({}); l.info("msg"); expect(l.buffer()[0]?.prefix).toBe(""); }); @@ -391,14 +391,14 @@ describe("makeLogger", () => { describe("log level filtering", () => { it("defaults to info level (debug messages not printed but buffered)", () => { - const l = makeLogger({}); + const l = mkLogger({}); l.debug("hidden"); l.info("visible"); expect(l.buffer()).toHaveLength(2); }); it("filters messages below configured level", () => { - const l = makeLogger({ level: "warn" }); + const l = mkLogger({ level: "warn" }); l.debug("d"); l.info("i"); l.warn("w"); @@ -408,7 +408,7 @@ describe("makeLogger", () => { }); it("setLevel changes level at runtime", () => { - const l = makeLogger({ level: "info" }); + const l = mkLogger({ level: "info" }); l.debug("before"); l.setLevel("debug"); l.debug("after"); @@ -417,7 +417,7 @@ describe("makeLogger", () => { }); it("fork inherits parent level", () => { - const parent = makeLogger({ level: "warn" }); + const parent = mkLogger({ level: "warn" }); const child = parent.fork("child"); child.debug("d"); child.info("i"); @@ -426,14 +426,14 @@ describe("makeLogger", () => { }); it("fork can override parent level", () => { - const parent = makeLogger({ level: "warn" }); + const parent = mkLogger({ level: "warn" }); const child = parent.fork("child", { level: "debug" }); child.debug("d"); expect(child.buffer()).toHaveLength(1); }); it("level filtering works alongside tag suppression", () => { - const l = makeLogger({ level: "warn", suppressTags: ["TAG_A"] }); + const l = mkLogger({ level: "warn", suppressTags: ["TAG_A"] }); l.info("TAG_A", "suppressed + below level"); l.warn("TAG_A", "suppressed at level"); l.warn("TAG_B", "visible"); @@ -442,7 +442,7 @@ describe("makeLogger", () => { }); it("silent level suppresses all console output but still buffers", () => { - const l = makeLogger({ level: "silent" }); + const l = mkLogger({ level: "silent" }); l.debug("d"); l.info("i"); l.warn("w"); From 2296316ec86aa5b0813be2230dcadc31323a1549 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 15:47:10 +0100 Subject: [PATCH 12/32] Remove lowercase LogLevel variants and UpperLogLevel duplication --- examples/mustache/mustache-java-r4-gen.ts | 2 +- src/api/builder.ts | 2 +- src/cli/commands/index.ts | 8 ++-- src/utils/logger.ts | 26 +++++------- test/api/mustache.test.ts | 2 +- test/api/write-generator/csharp.test.ts | 2 +- .../api/write-generator/introspection.test.ts | 8 ++-- .../write-generator/multi-package/cda.test.ts | 6 +-- .../multi-package/local-package.test.ts | 6 +-- .../multi-package/sql-on-fhir.test.ts | 6 +-- test/api/write-generator/python.test.ts | 2 +- test/api/write-generator/typescript.test.ts | 8 ++-- test/unit/utils/logger.test.ts | 40 +++++++++---------- 13 files changed, 57 insertions(+), 61 deletions(-) diff --git a/examples/mustache/mustache-java-r4-gen.ts b/examples/mustache/mustache-java-r4-gen.ts index d96a4c6ff..942bcac3f 100644 --- a/examples/mustache/mustache-java-r4-gen.ts +++ b/examples/mustache/mustache-java-r4-gen.ts @@ -4,7 +4,7 @@ if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .setLogLevel("debug") + .setLogLevel("DEBUG") .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .outputTo("./examples/mustache/mustache-java-r4-output") diff --git a/src/api/builder.ts b/src/api/builder.ts index 4d4029c8e..360d880b1 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -137,7 +137,7 @@ export class APIBuilder { treeShake: undefined, promoteLogical: undefined, registry: undefined, - logLevel: "info", + logLevel: "INFO", dropCanonicalManagerCache: false, }; const opts: APIBuilderOptions = { diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index ad14ce099..14cc2552a 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -19,13 +19,13 @@ import { typeschemaCommand } from "./typeschema"; export interface CLIArgv { verbose?: boolean; debug?: boolean; - logLevel?: "debug" | "info" | "warn" | "error" | "silent"; + logLevel?: LogLevel; } const cliLogger = mkLogger({ prefix: "cli" }); async function setupLoggingMiddleware(argv: any) { - const level: LogLevel = argv.logLevel ?? (argv.debug || argv.verbose ? "debug" : "info"); + const level: LogLevel = argv.logLevel ?? (argv.debug || argv.verbose ? "DEBUG" : "INFO"); cliLogger.setLevel(level); } @@ -55,8 +55,8 @@ export function createCLI() { .option("log-level", { alias: "l", type: "string", - choices: ["debug", "info", "warn", "error", "silent"] as const, - description: "Set the log level (default: info)", + choices: ["DEBUG", "INFO", "WARN", "ERROR", "SILENT"] as const, + description: "Set the log level (default: INFO)", global: true, }) .demandCommand(0) // Allow 0 commands so we can handle it ourselves diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 0a271f597..6b53381c5 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,7 +4,7 @@ type TagsOf = L extends Logger ? T : never; export type ExtendLogger> = Logger | Extra>; -export type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG" | "SILENT" | "info" | "warn" | "error" | "debug" | "silent"; +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; export type LogEntry = { level: LogLevel; @@ -42,10 +42,7 @@ export type Logger = { bufferClear(): void; }; -type UpperLogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; -const normalizeLevel = (level: LogLevel): UpperLogLevel => level.toUpperCase() as UpperLogLevel; - -const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; +const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; export function mkLogger(opts: LoggerOptions = {}): Logger { const prefix = opts.prefix ?? ""; @@ -53,12 +50,11 @@ export function mkLogger(opts: LoggerOptions = {}): Logger< const tagCounts: Record = {}; const entries: LogEntry[] = []; const drySet = new Set(); - let currentLevel: LogLevel = opts.level ?? "info"; + let currentLevel: LogLevel = opts.level ?? "INFO"; - const shouldLog = (level: LogLevel): boolean => - LEVEL_PRIORITY[normalizeLevel(level)] >= LEVEL_PRIORITY[normalizeLevel(currentLevel)]; + const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; - const colorize: Record string> = { + const colorize: Record string> = { DEBUG: (s) => s, INFO: (s) => s, WARN: pc.yellow, @@ -69,7 +65,7 @@ export function mkLogger(opts: LoggerOptions = {}): Logger< const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { const pfx = prefix ? `[${prefix}] ` : ""; const tagStr = tag ? `[${tag}] ` : ""; - return colorize[normalizeLevel(level)](`${icon} ${pfx}${tagStr}${msg}`); + return colorize[level](`${icon} ${pfx}${tagStr}${msg}`); }; const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { @@ -100,11 +96,11 @@ export function mkLogger(opts: LoggerOptions = {}): Logger< }; const logger: Logger = { - warn: mkLogFn("warn", "!", console.warn), - dryWarn: mkLogFn("warn", "!", console.warn, true), - info: mkLogFn("info", "i", console.log), - error: mkLogFn("error", "X", console.error), - debug: mkLogFn("debug", "D", console.log), + warn: mkLogFn("WARN", "!", console.warn), + dryWarn: mkLogFn("WARN", "!", console.warn, true), + info: mkLogFn("INFO", "i", console.log), + error: mkLogFn("ERROR", "X", console.error), + debug: mkLogFn("DEBUG", "D", console.log), fork(childPrefix: string, childOpts?: Partial>): Logger { const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; diff --git a/test/api/mustache.test.ts b/test/api/mustache.test.ts index b1548ed29..d0585139d 100644 --- a/test/api/mustache.test.ts +++ b/test/api/mustache.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("Mustache Template Based Generation", async () => { const report = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .mustache("./examples/mustache/java", { debug: "COMPACT", inMemoryOnly: true, diff --git a/test/api/write-generator/csharp.test.ts b/test/api/write-generator/csharp.test.ts index 9964d1978..111388682 100644 --- a/test/api/write-generator/csharp.test.ts +++ b/test/api/write-generator/csharp.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("C# Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .csharp({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/introspection.test.ts b/test/api/write-generator/introspection.test.ts index f68cae92f..af1116f6a 100644 --- a/test/api/write-generator/introspection.test.ts +++ b/test/api/write-generator/introspection.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("IntrospectionWriter - Fhir Schema Output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .introspection({ fhirSchemas: "introspection" }) .introspection({ fhirSchemas: "introspection.ndjson" }) .generate(); @@ -28,7 +28,7 @@ describe("IntrospectionWriter - Fhir Schema Output", async () => { describe("IntrospectionWriter - TypeSchema output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -68,7 +68,7 @@ describe("IntrospectionWriter - TypeSchema output", async () => { describe("IntrospectionWriter - typeTree", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -94,7 +94,7 @@ describe("IntrospectionWriter - typeTree", async () => { describe("IntrospectionWriter - StructureDefinition output", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .typeSchema({ treeShake: { "hl7.fhir.r4.core": { diff --git a/test/api/write-generator/multi-package/cda.test.ts b/test/api/write-generator/multi-package/cda.test.ts index f9cff5e13..f1daf1a27 100644 --- a/test/api/write-generator/multi-package/cda.test.ts +++ b/test/api/write-generator/multi-package/cda.test.ts @@ -19,7 +19,7 @@ describe("CDA", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -46,7 +46,7 @@ describe("CDA", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -70,7 +70,7 @@ describe("CDA", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/local-package.test.ts b/test/api/write-generator/multi-package/local-package.test.ts index 9628e056e..e3a8e9594 100644 --- a/test/api/write-generator/multi-package/local-package.test.ts +++ b/test/api/write-generator/multi-package/local-package.test.ts @@ -30,7 +30,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -62,7 +62,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -99,7 +99,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/sql-on-fhir.test.ts b/test/api/write-generator/multi-package/sql-on-fhir.test.ts index 4fd1a3f93..29228e42d 100644 --- a/test/api/write-generator/multi-package/sql-on-fhir.test.ts +++ b/test/api/write-generator/multi-package/sql-on-fhir.test.ts @@ -21,7 +21,7 @@ describe("SQL-on-FHIR", async () => { describe("TypeScript Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -55,7 +55,7 @@ describe("SQL-on-FHIR", async () => { describe("Python Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -86,7 +86,7 @@ describe("SQL-on-FHIR", async () => { describe("C# Generation", async () => { const result = await new APIBuilder() - .setLogLevel("silent") + .setLogLevel("SILENT") .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/python.test.ts b/test/api/write-generator/python.test.ts index a288417e1..6b1f37077 100644 --- a/test/api/write-generator/python.test.ts +++ b/test/api/write-generator/python.test.ts @@ -4,7 +4,7 @@ import { r4Manager } from "@typeschema-test/utils"; describe("Python Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .python({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 205e1ae48..3289c3a38 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -6,7 +6,7 @@ import { ccdaManager, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("error") + .setLogLevel("ERROR") .typescript({ inMemoryOnly: true, }) @@ -30,7 +30,7 @@ describe("TypeScript Writer Generator", async () => { describe("TypeScript CDA with Logical Model Promotion to Resource", async () => { const result = await new APIBuilder({ register: ccdaManager }) - .setLogLevel("error") + .setLogLevel("ERROR") .typeSchema({ promoteLogical: { "hl7.cda.uv.core": ["http://hl7.org/cda/stds/core/StructureDefinition/Material" as CanonicalUrl], @@ -52,7 +52,7 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () => }); describe("TypeScript R4 Example (with generateProfile)", async () => { - const logger = mkLogger({ level: "error" }); + const logger = mkLogger({ level: "ERROR" }); const result = await new APIBuilder({ register: r4Manager, logger }) .typescript({ @@ -70,7 +70,7 @@ describe("TypeScript R4 Example (with generateProfile)", async () => { it("file rewrite warnings", () => { const rewriteWarnings = logger .buffer() - .filter((e) => e.level === "warn" && e.message.includes("File will be rewritten")) + .filter((e) => e.level === "WARN" && e.message.includes("File will be rewritten")) .map((e) => e.message); expect(rewriteWarnings).toMatchSnapshot(); }); diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/logger.test.ts index 1613b6f01..6c09a0488 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/logger.test.ts @@ -26,7 +26,7 @@ describe("mkLogger", () => { logger.info("hello"); const entry = logger.buffer()[0]; expect(entry).toBeDefined(); - expect(entry?.level).toBe("info"); + expect(entry?.level).toBe("INFO"); expect(entry?.message).toBe("hello"); expect(entry?.tag).toBeUndefined(); expect(entry?.suppressed).toBe(false); @@ -51,7 +51,7 @@ describe("mkLogger", () => { expect(entry).toBeDefined(); expect(entry?.tag).toBe("TAG_A"); expect(entry?.message).toBe("tagged info"); - expect(entry?.level).toBe("info"); + expect(entry?.level).toBe("INFO"); }); it("works for all log levels", () => { @@ -59,7 +59,7 @@ describe("mkLogger", () => { logger.warn("TAG_B", "w"); logger.error("TAG_C", "e"); logger.debug("TAG_A", "d"); - expect(logger.buffer().map((e) => e.level)).toEqual(["info", "warn", "error", "debug"]); + expect(logger.buffer().map((e) => e.level)).toEqual(["INFO", "WARN", "ERROR", "DEBUG"]); expect(logger.buffer().every((e) => e.tag !== undefined)).toBe(true); }); @@ -312,10 +312,10 @@ describe("mkLogger", () => { }); it("filters by level", () => { - expect(bufferFilter(logger, { level: "info" })).toHaveLength(2); - expect(bufferFilter(logger, { level: "warn" })).toHaveLength(1); - expect(bufferFilter(logger, { level: "error" })).toHaveLength(1); - expect(bufferFilter(logger, { level: "debug" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "INFO" })).toHaveLength(2); + expect(bufferFilter(logger, { level: "WARN" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "ERROR" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "DEBUG" })).toHaveLength(1); }); it("filters by tag", () => { @@ -330,10 +330,10 @@ describe("mkLogger", () => { }); it("combines filters", () => { - expect(bufferFilter(logger, { level: "info", suppressed: true })).toHaveLength(1); - expect(bufferFilter(logger, { level: "info", suppressed: false })).toHaveLength(1); - expect(bufferFilter(logger, { level: "warn", tag: "TAG_A" })).toHaveLength(1); - expect(bufferFilter(logger, { level: "warn", tag: "TAG_B" })).toHaveLength(0); + expect(bufferFilter(logger, { level: "INFO", suppressed: true })).toHaveLength(1); + expect(bufferFilter(logger, { level: "INFO", suppressed: false })).toHaveLength(1); + expect(bufferFilter(logger, { level: "WARN", tag: "TAG_A" })).toHaveLength(1); + expect(bufferFilter(logger, { level: "WARN", tag: "TAG_B" })).toHaveLength(0); }); }); @@ -361,7 +361,7 @@ describe("mkLogger", () => { l.warn("TAG_B", "b1"); l.printSuppressedSummary(); - const summaryEntries = bufferFilter(l, { level: "info" }); + const summaryEntries = bufferFilter(l, { level: "INFO" }); expect(summaryEntries).toHaveLength(1); expect(summaryEntries[0]?.message).toContain("TAG_A: 2"); expect(summaryEntries[0]?.message).toContain("TAG_B: 1"); @@ -398,7 +398,7 @@ describe("mkLogger", () => { }); it("filters messages below configured level", () => { - const l = mkLogger({ level: "warn" }); + const l = mkLogger({ level: "WARN" }); l.debug("d"); l.info("i"); l.warn("w"); @@ -408,16 +408,16 @@ describe("mkLogger", () => { }); it("setLevel changes level at runtime", () => { - const l = mkLogger({ level: "info" }); + const l = mkLogger({ level: "INFO" }); l.debug("before"); - l.setLevel("debug"); + l.setLevel("DEBUG"); l.debug("after"); // both buffered regardless expect(l.buffer()).toHaveLength(2); }); it("fork inherits parent level", () => { - const parent = mkLogger({ level: "warn" }); + const parent = mkLogger({ level: "WARN" }); const child = parent.fork("child"); child.debug("d"); child.info("i"); @@ -426,14 +426,14 @@ describe("mkLogger", () => { }); it("fork can override parent level", () => { - const parent = mkLogger({ level: "warn" }); - const child = parent.fork("child", { level: "debug" }); + const parent = mkLogger({ level: "WARN" }); + const child = parent.fork("child", { level: "DEBUG" }); child.debug("d"); expect(child.buffer()).toHaveLength(1); }); it("level filtering works alongside tag suppression", () => { - const l = mkLogger({ level: "warn", suppressTags: ["TAG_A"] }); + const l = mkLogger({ level: "WARN", suppressTags: ["TAG_A"] }); l.info("TAG_A", "suppressed + below level"); l.warn("TAG_A", "suppressed at level"); l.warn("TAG_B", "visible"); @@ -442,7 +442,7 @@ describe("mkLogger", () => { }); it("silent level suppresses all console output but still buffers", () => { - const l = mkLogger({ level: "silent" }); + const l = mkLogger({ level: "SILENT" }); l.debug("d"); l.info("i"); l.warn("w"); From c1d75ed79deef90dd128dad1aa21b7324e914956 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 16:25:58 +0100 Subject: [PATCH 13/32] Split Logger into Log and LogManager, rename logger.ts to log.ts Log is the simple interface (warn, info, error, debug, dryWarn) used by most consumers. LogManager extends Log with lifecycle features (fork, as, suppress, setLevel, tagCounts, buffer). --- examples/python/generate.ts | 4 ++-- src/api/builder.ts | 8 +++---- src/api/index.ts | 2 +- src/api/writer-generator/mustache.ts | 4 ++-- src/api/writer-generator/writer.ts | 6 ++--- src/cli/commands/index.ts | 4 ++-- src/cli/commands/typeschema.ts | 2 +- src/cli/commands/typeschema/generate.ts | 2 +- src/typeschema/core/binding.ts | 12 +++++----- src/typeschema/core/field-builder.ts | 6 ++--- src/typeschema/core/nested-types.ts | 6 ++--- src/typeschema/core/profile-extensions.ts | 10 ++++---- src/typeschema/core/transformer.ts | 10 ++++---- src/typeschema/index.ts | 6 ++--- src/typeschema/ir/tree-shake.ts | 4 ++-- src/typeschema/register.ts | 10 ++++---- src/typeschema/types.ts | 2 +- src/typeschema/utils.ts | 4 ++-- src/utils/{logger.ts => log.ts} | 24 +++++++++++-------- test/api/write-generator/typescript.test.ts | 2 +- test/unit/typeschema/utils.ts | 4 ++-- .../utils/{logger.test.ts => log.test.ts} | 10 ++++---- 22 files changed, 73 insertions(+), 69 deletions(-) rename src/utils/{logger.ts => log.ts} (87%) rename test/unit/utils/{logger.test.ts => log.test.ts} (97%) diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 34b59a724..17c97d007 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,9 +1,9 @@ import { APIBuilder, prettyReport } from "../../src"; -import { type Logger, mkLogger } from "../../src/utils/logger"; +import { type LogManager, mkLogger } from "../../src/utils/log"; console.log("📦 Generating FHIR R4 Core Types..."); -const logger: Logger = mkLogger({ +const logger: LogManager = mkLogger({ prefix: "API", suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], }); diff --git a/src/api/builder.ts b/src/api/builder.ts index 360d880b1..23c107ea0 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type Logger, type LogLevel, mkLogger } from "@root/utils/logger"; +import { type LogManager, type LogLevel, mkLogger } from "@root/utils/log"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; @@ -94,7 +94,7 @@ export interface LocalStructureDefinitionConfig { dependencies?: PackageMeta[]; } -const cleanup = async (opts: APIBuilderOptions, logger: Logger): Promise => { +const cleanup = async (opts: APIBuilderOptions, logger: LogManager): Promise => { logger.info(`Cleaning outputs...`); try { logger.info(`Clean ${opts.outputDir}`); @@ -119,7 +119,7 @@ export class APIBuilder { localSDs: LocalPackageConfig[]; localTgzPackages: TgzPackageConfig[]; }; - private logger: Logger; + private logger: LogManager; private generators: { name: string; writer: FileSystemWriter }[] = []; constructor( @@ -127,7 +127,7 @@ export class APIBuilder { manager?: ReturnType; register?: Register; preprocessPackage?: (context: PreprocessContext) => PreprocessContext; - logger?: Logger; + logger?: LogManager; } = {}, ) { const defaultOpts: APIBuilderOptions = { diff --git a/src/api/index.ts b/src/api/index.ts index 1e8d9afa4..dc3e11a55 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,7 +8,7 @@ */ export type { IrConf, LogicalPromotionConf, TreeShakeConf } from "../typeschema/ir/types"; -export type { LogLevel } from "../utils/logger"; +export type { LogLevel } from "../utils/log"; export type { APIBuilderOptions, LocalStructureDefinitionConfig } from "./builder"; export { APIBuilder, prettyReport } from "./builder"; export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts index 754db89ab..3a2d794a5 100644 --- a/src/api/writer-generator/mustache.ts +++ b/src/api/writer-generator/mustache.ts @@ -22,7 +22,7 @@ import type { ViewModel, } from "@mustache/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import { default as Mustache } from "mustache"; import { FileSystemWriter, type FileSystemWriterOptions } from "./writer"; @@ -57,7 +57,7 @@ export type MustacheGeneratorOptions = FileSystemWriterOptions & export function loadMustacheGeneratorConfig( templatePath: string, - logger?: Logger, + logger?: Log, ): Partial { const filePath = Path.resolve(templatePath, "config.json"); try { diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index 0a7a1029b..d704aa465 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -2,12 +2,12 @@ import * as fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import * as Path from "node:path"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; export type FileSystemWriterOptions = { outputDir: string; inMemoryOnly?: boolean; - logger?: Logger; + logger?: Log; resolveAssets?: (fn: string) => string; }; @@ -36,7 +36,7 @@ export abstract class FileSystemWriter item.code !== undefined) @@ -102,7 +102,7 @@ export function buildEnum( register: Register, fhirSchema: RichFHIRSchema, element: FHIRSchemaElement, - logger?: Logger, + logger?: Log, ): EnumDefinition | undefined { if (!element.binding) return undefined; @@ -143,7 +143,7 @@ function generateBindingSchema( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Logger, + logger?: Log, ): BindingTypeSchema | undefined { if (!element.binding?.valueSet) return undefined; @@ -168,7 +168,7 @@ function generateBindingSchema( export function collectBindingSchemas( register: Register, fhirSchema: RichFHIRSchema, - logger?: Logger, + logger?: Log, ): BindingTypeSchema[] { const processedPaths = new Set(); if (!fhirSchema.elements) return []; diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 2f15edc8b..de673e4c0 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import { packageMetaToFhir } from "@typeschema/types"; import type { BindingIdentifier, @@ -224,7 +224,7 @@ export function buildFieldType( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Logger, + logger?: Log, ): Identifier | undefined { if (element.elementReference) { const refPath = element.elementReference @@ -259,7 +259,7 @@ export const mkField = ( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Logger, + logger?: Log, rawElement?: FHIRSchemaElement, ): Field => { let binding: BindingIdentifier | undefined; diff --git a/src/typeschema/core/nested-types.ts b/src/typeschema/core/nested-types.ts index fa3254932..e8bef31b0 100644 --- a/src/typeschema/core/nested-types.ts +++ b/src/typeschema/core/nested-types.ts @@ -6,7 +6,7 @@ import type { FHIRSchema, FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { mergeFsElementProps, type Register, resolveFsElementGenealogy } from "@root/typeschema/register"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import type { CanonicalUrl, Field, Identifier, Name, NestedIdentifier, NestedType, RichFHIRSchema } from "../types"; import { mkField, mkNestedField } from "./field-builder"; @@ -114,7 +114,7 @@ function transformNestedElements( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record, - logger?: Logger, + logger?: Log, ): Record { const fields: Record = {}; @@ -148,7 +148,7 @@ function transformNestedElements( export function mkNestedTypes( register: Register, fhirSchema: RichFHIRSchema, - logger?: Logger, + logger?: Log, ): NestedType[] | undefined { if (!fhirSchema.elements) return undefined; diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index 5f7db6987..d835aa119 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -7,7 +7,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import { type CanonicalUrl, concatIdentifiers, @@ -24,7 +24,7 @@ const extractExtensionValueTypes = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: Logger, + logger?: Log, ): Identifier[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -42,7 +42,7 @@ const extractExtensionValueTypes = ( const extractLegacySubExtensions = ( register: Register, extensionSchema: RichFHIRSchema, - logger?: Logger, + logger?: Log, ): ExtensionSubField[] => { const subExtensions: ExtensionSubField[] = []; if (!extensionSchema.elements) return subExtensions; @@ -114,7 +114,7 @@ const extractSubExtensions = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: Logger, + logger?: Log, ): ExtensionSubField[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -129,7 +129,7 @@ const extractSubExtensions = ( export const extractProfileExtensions = ( register: Register, fhirSchema: RichFHIRSchema, - logger?: Logger, + logger?: Log, ): ProfileExtension[] | undefined => { const extensions: ProfileExtension[] = []; diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 289f4cc96..45ae672fa 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { shouldSkipCanonical } from "@root/typeschema/skip-hack"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import type { Register } from "@typeschema/register"; import { concatIdentifiers, @@ -33,7 +33,7 @@ export function mkFields( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record | undefined, - logger?: Logger, + logger?: Log, ): Record | undefined { if (!elements) return undefined; @@ -77,7 +77,7 @@ function extractFieldDependencies(fields: Record): Identifier[] { export async function transformValueSet( register: Register, valueSet: RichValueSet, - logger?: Logger, + logger?: Log, ): Promise { if (!valueSet.url) throw new Error("ValueSet URL is required"); @@ -114,7 +114,7 @@ export function extractDependencies( return concatIdentifiers(filtered); } -function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSchema, logger?: Logger): TypeSchema[] { +function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSchema, logger?: Log): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); let base: Identifier | undefined; @@ -155,7 +155,7 @@ function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSch export async function transformFhirSchema( register: Register, fhirSchema: RichFHIRSchema, - logger?: Logger, + logger?: Log, ): Promise { return transformFhirSchemaResource(register, fhirSchema, logger); } diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 46a1d32b4..3bd4f7298 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -10,7 +10,7 @@ * - Validating TypeSchema documents */ -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import { transformFhirSchema, transformValueSet } from "./core/transformer"; import type { TypeSchemaCollisions } from "./ir/types"; import type { Register } from "./register"; @@ -33,7 +33,7 @@ type SchemaWithSource = { sourceCanonical: CanonicalUrl; }; -const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Logger): GenerateTypeSchemasResult => { +const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Log): GenerateTypeSchemasResult => { // key -> hash const groups: Record> = {}; @@ -74,7 +74,7 @@ const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Log return { schemas, collisions }; }; -export const generateTypeSchemas = async (register: Register, logger?: Logger): Promise => { +export const generateTypeSchemas = async (register: Register, logger?: Log): Promise => { const schemasWithSources: { schema: TypeSchema; sourcePackage: PkgName; sourceCanonical: CanonicalUrl }[] = []; for (const fhirSchema of register.allFs()) { diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index b13229af4..661f423e9 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import { extractDependencies } from "../core/transformer"; import { type CanonicalUrl, @@ -174,7 +174,7 @@ const mutableFillReport = (report: TreeShakeReport, tsIndex: TypeSchemaIndex, sh } }; -export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: Logger): TypeSchema => { +export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: Log): TypeSchema => { schema = JSON.parse(JSON.stringify(schema)); if (isPrimitiveTypeSchema(schema) || isValueSetTypeSchema(schema) || isBindingSchema(schema)) return schema; diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index cfaed0916..4cdd35192 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -7,7 +7,7 @@ import { type StructureDefinition, } from "@atomic-ehr/fhirschema"; import { type CodeSystem, isCodeSystem, isValueSet, type ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import type { CanonicalUrl, Name, @@ -86,7 +86,7 @@ const mkPackageAwareResolver = async ( pkg: PackageMeta, deep: number, acc: PackageAwareResolver, - logger?: Logger, + logger?: Log, ): Promise => { const pkgId = packageMetaToFhir(pkg); logger?.info(`${" ".repeat(deep * 2)}+ ${pkgId}`); @@ -119,7 +119,7 @@ const mkPackageAwareResolver = async ( return index; }; -const enrichResolver = (resolver: PackageAwareResolver, logger?: Logger) => { +const enrichResolver = (resolver: PackageAwareResolver, logger?: Log) => { for (const { pkg, canonicalResolution } of Object.values(resolver)) { const pkgId = packageMetaToFhir(pkg); if (!resolver[pkgId]) throw new Error(`Package ${pkgId} not found`); @@ -145,7 +145,7 @@ const enrichResolver = (resolver: PackageAwareResolver, logger?: Logger) => { } }; -const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: CanonicalUrl, _logger?: Logger) => { +const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: CanonicalUrl, _logger?: Log) => { const options = Object.values(resolver).flatMap((pkg) => pkg.canonicalResolution[url]); if (!options) throw new Error(`No canonical resolution found for ${url} in any package`); // if (options.length > 1) @@ -160,7 +160,7 @@ const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: Ca }; export type RegisterConfig = { - logger?: Logger; + logger?: Log; focusedPackages?: PackageMeta[]; /** Custom FHIR package registry URL */ registry?: string; diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index f4aa8c3cb..724750770 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -362,7 +362,7 @@ export const enrichValueSet = (vs: ValueSet, packageMeta: PackageMeta): RichValu /////////////////////////////////////////////////////////// export interface TypeschemaGeneratorOptions { - logger?: import("../utils/logger").Logger; + logger?: import("../utils/log").Log; treeshake?: string[]; manager: ReturnType; /** Custom FHIR package registry URL */ diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index bb3921309..43915c3cc 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -1,6 +1,6 @@ import * as afs from "node:fs/promises"; import * as Path from "node:path"; -import type { Logger } from "@root/utils/logger"; +import type { Log } from "@root/utils/log"; import * as YAML from "yaml"; import type { IrReport } from "./ir/types"; import type { Register } from "./register"; @@ -199,7 +199,7 @@ export const mkTypeSchemaIndex = ( irReport = {}, }: { register?: Register; - logger?: Logger; + logger?: Log; irReport?: IrReport; }, ): TypeSchemaIndex => { diff --git a/src/utils/logger.ts b/src/utils/log.ts similarity index 87% rename from src/utils/logger.ts rename to src/utils/log.ts index 6b53381c5..e10fc57dc 100644 --- a/src/utils/logger.ts +++ b/src/utils/log.ts @@ -1,8 +1,10 @@ import pc from "picocolors"; -type TagsOf = L extends Logger ? T : never; +type TagsOf = L extends LogManager ? T : never; -export type ExtendLogger> = Logger | Extra>; +export type ExtendLogManager> = LogManager< + TagsOf | Extra +>; export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; @@ -23,15 +25,17 @@ export type LoggerOptions = { export type TaggedLogFn = (...args: [string] | [T, string]) => void; -export type Logger = { +export type Log = { warn: TaggedLogFn; dryWarn: TaggedLogFn; info: TaggedLogFn; error: TaggedLogFn; debug: TaggedLogFn; +}; - fork(prefix: string, opts?: Partial>): Logger; - as(): Logger; +export type LogManager = Log & { + fork(prefix: string, opts?: Partial>): LogManager; + as(): LogManager; suppress(...tags: T[]): void; setLevel(level: LogLevel): void; @@ -44,7 +48,7 @@ export type Logger = { const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; -export function mkLogger(opts: LoggerOptions = {}): Logger { +export function mkLogger(opts: LoggerOptions = {}): LogManager { const prefix = opts.prefix ?? ""; const suppressedSet = new Set(opts.suppressTags ?? []); const tagCounts: Record = {}; @@ -95,14 +99,14 @@ export function mkLogger(opts: LoggerOptions = {}): Logger< }; }; - const logger: Logger = { + const logger: LogManager = { warn: mkLogFn("WARN", "!", console.warn), dryWarn: mkLogFn("WARN", "!", console.warn, true), info: mkLogFn("INFO", "i", console.log), error: mkLogFn("ERROR", "X", console.error), debug: mkLogFn("DEBUG", "D", console.log), - fork(childPrefix: string, childOpts?: Partial>): Logger { + fork(childPrefix: string, childOpts?: Partial>): LogManager { const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as C[]; return mkLogger({ @@ -112,8 +116,8 @@ export function mkLogger(opts: LoggerOptions = {}): Logger< }); }, - as(): Logger { - return logger as unknown as Logger; + as(): LogManager { + return logger as unknown as LogManager; }, suppress(...tags: T[]) { diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 3289c3a38..2c7198c34 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { mkLogger } from "@root/utils/logger"; +import { mkLogger } from "@root/utils/log"; import { ccdaManager, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index 748db3dd7..764d5b6fe 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,7 +2,7 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; -import { type Logger, mkLogger } from "@root/utils/logger"; +import { type Log, mkLogger } from "@root/utils/log"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; @@ -12,7 +12,7 @@ export type PVS = Partial; const logger = mkLogger({ prefix: "TEST" }); -export const mkIndex = async (register: Register, logger?: Logger) => { +export const mkIndex = async (register: Register, logger?: Log) => { const { schemas } = await generateTypeSchemas(register, logger); return mkTypeSchemaIndex(schemas, { register, logger }); }; diff --git a/test/unit/utils/logger.test.ts b/test/unit/utils/log.test.ts similarity index 97% rename from test/unit/utils/logger.test.ts rename to test/unit/utils/log.test.ts index 6c09a0488..faf93d5ae 100644 --- a/test/unit/utils/logger.test.ts +++ b/test/unit/utils/log.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; -import { type ExtendLogger, type LogEntry, type Logger, mkLogger } from "@root/utils/logger"; +import { type ExtendLogManager, type LogEntry, type LogManager, mkLogger } from "@root/utils/log"; type BufferFilter = { level?: string; tag?: T; suppressed?: boolean }; -const bufferFilter = (logger: Logger, filter: BufferFilter): LogEntry[] => +const bufferFilter = (logger: LogManager, filter: BufferFilter): LogEntry[] => logger.buffer().filter((e) => { if (filter.level !== undefined && e.level !== filter.level) return false; if (filter.tag !== undefined && e.tag !== filter.tag) return false; @@ -14,7 +14,7 @@ const bufferFilter = (logger: Logger, filter: BufferFilter< type TestTags = "TAG_A" | "TAG_B" | "TAG_C"; describe("mkLogger", () => { - let logger: Logger; + let logger: LogManager; beforeEach(() => { logger = mkLogger({ prefix: "test" }); @@ -234,10 +234,10 @@ describe("mkLogger", () => { }); }); - describe("ExtendLogger (extending)", () => { + describe("ExtendLogManager (extending)", () => { type BaseTags = "BASE_A" | "BASE_B"; type ExtraTags = "EXTRA_X" | "EXTRA_Y"; - type Combined = ExtendLogger>; + type Combined = ExtendLogManager>; it("extended logger accepts both base and extra tags", () => { const l: Combined = mkLogger({}); From d08d04eea25d419c75e4f45c2f4b133096f74b1c Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 6 Mar 2026 17:30:55 +0100 Subject: [PATCH 14/32] Unexport TaggedLogFn and LoggerOptions, reorder types-first layout --- src/utils/log.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/log.ts b/src/utils/log.ts index e10fc57dc..59c456a9c 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,11 +1,5 @@ import pc from "picocolors"; -type TagsOf = L extends LogManager ? T : never; - -export type ExtendLogManager> = LogManager< - TagsOf | Extra ->; - export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; export type LogEntry = { @@ -17,14 +11,6 @@ export type LogEntry = { timestamp: number; }; -export type LoggerOptions = { - prefix?: string; - suppressTags?: T[]; - level?: LogLevel; -}; - -export type TaggedLogFn = (...args: [string] | [T, string]) => void; - export type Log = { warn: TaggedLogFn; dryWarn: TaggedLogFn; @@ -46,6 +32,20 @@ export type LogManager = Log & { bufferClear(): void; }; +type TagsOf = L extends LogManager ? T : never; + +export type ExtendLogManager> = LogManager< + TagsOf | Extra +>; + +type TaggedLogFn = (...args: [string] | [T, string]) => void; + +type LoggerOptions = { + prefix?: string; + suppressTags?: T[]; + level?: LogLevel; +}; + const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; export function mkLogger(opts: LoggerOptions = {}): LogManager { From b75aad2fbc8c47a51cc2a2f92b2f031fa8407ad9 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 11:22:45 +0100 Subject: [PATCH 15/32] Add CodegenTag union and CodegenLog type alias, migrate consumers from generic Log Centralizes all 7 log tags into a CodegenTag union in utils/types.ts so passing an invalid tag string is now a type error. Replaces bare Log with CodegenLog across typeschema, api, and writer-generator modules. --- src/api/builder.ts | 2 +- src/api/writer-generator/mustache.ts | 4 ++-- src/api/writer-generator/writer.ts | 6 +++--- src/typeschema/core/binding.ts | 16 ++++++++++------ src/typeschema/core/field-builder.ts | 6 +++--- src/typeschema/core/nested-types.ts | 6 +++--- src/typeschema/core/profile-extensions.ts | 10 +++++----- src/typeschema/core/transformer.ts | 14 +++++++++----- src/typeschema/index.ts | 9 ++++++--- src/typeschema/ir/tree-shake.ts | 4 ++-- src/typeschema/register.ts | 10 +++++----- src/typeschema/types.ts | 2 +- src/typeschema/utils.ts | 4 ++-- src/utils/log.ts | 4 +--- src/utils/types.ts | 14 ++++++++++++++ test/unit/utils/log.test.ts | 16 ++++++++-------- 16 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/api/builder.ts b/src/api/builder.ts index 23c107ea0..9d36f0109 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type LogManager, type LogLevel, mkLogger } from "@root/utils/log"; +import { type LogLevel, type LogManager, mkLogger } from "@root/utils/log"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts index 3a2d794a5..16d72067d 100644 --- a/src/api/writer-generator/mustache.ts +++ b/src/api/writer-generator/mustache.ts @@ -22,7 +22,7 @@ import type { ViewModel, } from "@mustache/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import { default as Mustache } from "mustache"; import { FileSystemWriter, type FileSystemWriterOptions } from "./writer"; @@ -57,7 +57,7 @@ export type MustacheGeneratorOptions = FileSystemWriterOptions & export function loadMustacheGeneratorConfig( templatePath: string, - logger?: Log, + logger?: CodegenLog, ): Partial { const filePath = Path.resolve(templatePath, "config.json"); try { diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index d704aa465..26a8d05c0 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -2,12 +2,12 @@ import * as fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import * as Path from "node:path"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; export type FileSystemWriterOptions = { outputDir: string; inMemoryOnly?: boolean; - logger?: Log; + logger?: CodegenLog; resolveAssets?: (fn: string) => string; }; @@ -36,7 +36,7 @@ export abstract class FileSystemWriter item.code !== undefined) @@ -102,7 +106,7 @@ export function buildEnum( register: Register, fhirSchema: RichFHIRSchema, element: FHIRSchemaElement, - logger?: Log, + logger?: CodegenLog, ): EnumDefinition | undefined { if (!element.binding) return undefined; @@ -143,7 +147,7 @@ function generateBindingSchema( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Log, + logger?: CodegenLog, ): BindingTypeSchema | undefined { if (!element.binding?.valueSet) return undefined; @@ -168,7 +172,7 @@ function generateBindingSchema( export function collectBindingSchemas( register: Register, fhirSchema: RichFHIRSchema, - logger?: Log, + logger?: CodegenLog, ): BindingTypeSchema[] { const processedPaths = new Set(); if (!fhirSchema.elements) return []; diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index de673e4c0..8c194e189 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import { packageMetaToFhir } from "@typeschema/types"; import type { BindingIdentifier, @@ -224,7 +224,7 @@ export function buildFieldType( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Log, + logger?: CodegenLog, ): Identifier | undefined { if (element.elementReference) { const refPath = element.elementReference @@ -259,7 +259,7 @@ export const mkField = ( fhirSchema: RichFHIRSchema, path: string[], element: FHIRSchemaElement, - logger?: Log, + logger?: CodegenLog, rawElement?: FHIRSchemaElement, ): Field => { let binding: BindingIdentifier | undefined; diff --git a/src/typeschema/core/nested-types.ts b/src/typeschema/core/nested-types.ts index e8bef31b0..f9250a22f 100644 --- a/src/typeschema/core/nested-types.ts +++ b/src/typeschema/core/nested-types.ts @@ -6,7 +6,7 @@ import type { FHIRSchema, FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { mergeFsElementProps, type Register, resolveFsElementGenealogy } from "@root/typeschema/register"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import type { CanonicalUrl, Field, Identifier, Name, NestedIdentifier, NestedType, RichFHIRSchema } from "../types"; import { mkField, mkNestedField } from "./field-builder"; @@ -114,7 +114,7 @@ function transformNestedElements( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record, - logger?: Log, + logger?: CodegenLog, ): Record { const fields: Record = {}; @@ -148,7 +148,7 @@ function transformNestedElements( export function mkNestedTypes( register: Register, fhirSchema: RichFHIRSchema, - logger?: Log, + logger?: CodegenLog, ): NestedType[] | undefined { if (!fhirSchema.elements) return undefined; diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index d835aa119..d5bcaeed2 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -7,7 +7,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import { type CanonicalUrl, concatIdentifiers, @@ -24,7 +24,7 @@ const extractExtensionValueTypes = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: Log, + logger?: CodegenLog, ): Identifier[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -42,7 +42,7 @@ const extractExtensionValueTypes = ( const extractLegacySubExtensions = ( register: Register, extensionSchema: RichFHIRSchema, - logger?: Log, + logger?: CodegenLog, ): ExtensionSubField[] => { const subExtensions: ExtensionSubField[] = []; if (!extensionSchema.elements) return subExtensions; @@ -114,7 +114,7 @@ const extractSubExtensions = ( register: Register, fhirSchema: RichFHIRSchema, extensionUrl: CanonicalUrl, - logger?: Log, + logger?: CodegenLog, ): ExtensionSubField[] | undefined => { const extensionSchema = register.resolveFs(fhirSchema.package_meta, extensionUrl); if (!extensionSchema?.elements) return undefined; @@ -129,7 +129,7 @@ const extractSubExtensions = ( export const extractProfileExtensions = ( register: Register, fhirSchema: RichFHIRSchema, - logger?: Log, + logger?: CodegenLog, ): ProfileExtension[] | undefined => { const extensions: ProfileExtension[] = []; diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 45ae672fa..d51366ef9 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { shouldSkipCanonical } from "@root/typeschema/skip-hack"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import type { Register } from "@typeschema/register"; import { concatIdentifiers, @@ -33,7 +33,7 @@ export function mkFields( fhirSchema: RichFHIRSchema, parentPath: string[], elements: Record | undefined, - logger?: Log, + logger?: CodegenLog, ): Record | undefined { if (!elements) return undefined; @@ -77,7 +77,7 @@ function extractFieldDependencies(fields: Record): Identifier[] { export async function transformValueSet( register: Register, valueSet: RichValueSet, - logger?: Log, + logger?: CodegenLog, ): Promise { if (!valueSet.url) throw new Error("ValueSet URL is required"); @@ -114,7 +114,11 @@ export function extractDependencies( return concatIdentifiers(filtered); } -function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSchema, logger?: Log): TypeSchema[] { +function transformFhirSchemaResource( + register: Register, + fhirSchema: RichFHIRSchema, + logger?: CodegenLog, +): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); let base: Identifier | undefined; @@ -155,7 +159,7 @@ function transformFhirSchemaResource(register: Register, fhirSchema: RichFHIRSch export async function transformFhirSchema( register: Register, fhirSchema: RichFHIRSchema, - logger?: Log, + logger?: CodegenLog, ): Promise { return transformFhirSchemaResource(register, fhirSchema, logger); } diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 3bd4f7298..6bd7bb60f 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -10,7 +10,7 @@ * - Validating TypeSchema documents */ -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import { transformFhirSchema, transformValueSet } from "./core/transformer"; import type { TypeSchemaCollisions } from "./ir/types"; import type { Register } from "./register"; @@ -33,7 +33,7 @@ type SchemaWithSource = { sourceCanonical: CanonicalUrl; }; -const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Log): GenerateTypeSchemasResult => { +const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: CodegenLog): GenerateTypeSchemasResult => { // key -> hash const groups: Record> = {}; @@ -74,7 +74,10 @@ const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Log return { schemas, collisions }; }; -export const generateTypeSchemas = async (register: Register, logger?: Log): Promise => { +export const generateTypeSchemas = async ( + register: Register, + logger?: CodegenLog, +): Promise => { const schemasWithSources: { schema: TypeSchema; sourcePackage: PkgName; sourceCanonical: CanonicalUrl }[] = []; for (const fhirSchema of register.allFs()) { diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index 661f423e9..5f060f16f 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import { extractDependencies } from "../core/transformer"; import { type CanonicalUrl, @@ -174,7 +174,7 @@ const mutableFillReport = (report: TreeShakeReport, tsIndex: TypeSchemaIndex, sh } }; -export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: Log): TypeSchema => { +export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _logger?: CodegenLog): TypeSchema => { schema = JSON.parse(JSON.stringify(schema)); if (isPrimitiveTypeSchema(schema) || isValueSetTypeSchema(schema) || isBindingSchema(schema)) return schema; diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index 4cdd35192..d53f802b1 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -7,7 +7,7 @@ import { type StructureDefinition, } from "@atomic-ehr/fhirschema"; import { type CodeSystem, isCodeSystem, isValueSet, type ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import type { CanonicalUrl, Name, @@ -86,7 +86,7 @@ const mkPackageAwareResolver = async ( pkg: PackageMeta, deep: number, acc: PackageAwareResolver, - logger?: Log, + logger?: CodegenLog, ): Promise => { const pkgId = packageMetaToFhir(pkg); logger?.info(`${" ".repeat(deep * 2)}+ ${pkgId}`); @@ -119,7 +119,7 @@ const mkPackageAwareResolver = async ( return index; }; -const enrichResolver = (resolver: PackageAwareResolver, logger?: Log) => { +const enrichResolver = (resolver: PackageAwareResolver, logger?: CodegenLog) => { for (const { pkg, canonicalResolution } of Object.values(resolver)) { const pkgId = packageMetaToFhir(pkg); if (!resolver[pkgId]) throw new Error(`Package ${pkgId} not found`); @@ -145,7 +145,7 @@ const enrichResolver = (resolver: PackageAwareResolver, logger?: Log) => { } }; -const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: CanonicalUrl, _logger?: Log) => { +const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: CanonicalUrl, _logger?: CodegenLog) => { const options = Object.values(resolver).flatMap((pkg) => pkg.canonicalResolution[url]); if (!options) throw new Error(`No canonical resolution found for ${url} in any package`); // if (options.length > 1) @@ -160,7 +160,7 @@ const packageAgnosticResolveCanonical = (resolver: PackageAwareResolver, url: Ca }; export type RegisterConfig = { - logger?: Log; + logger?: CodegenLog; focusedPackages?: PackageMeta[]; /** Custom FHIR package registry URL */ registry?: string; diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 724750770..dee64696c 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -362,7 +362,7 @@ export const enrichValueSet = (vs: ValueSet, packageMeta: PackageMeta): RichValu /////////////////////////////////////////////////////////// export interface TypeschemaGeneratorOptions { - logger?: import("../utils/log").Log; + logger?: import("../utils/types").CodegenLog; treeshake?: string[]; manager: ReturnType; /** Custom FHIR package registry URL */ diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 43915c3cc..9cfbf7cd9 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -1,6 +1,6 @@ import * as afs from "node:fs/promises"; import * as Path from "node:path"; -import type { Log } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/types"; import * as YAML from "yaml"; import type { IrReport } from "./ir/types"; import type { Register } from "./register"; @@ -199,7 +199,7 @@ export const mkTypeSchemaIndex = ( irReport = {}, }: { register?: Register; - logger?: Log; + logger?: CodegenLog; irReport?: IrReport; }, ): TypeSchemaIndex => { diff --git a/src/utils/log.ts b/src/utils/log.ts index 59c456a9c..c73098631 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -34,9 +34,7 @@ export type LogManager = Log & { type TagsOf = L extends LogManager ? T : never; -export type ExtendLogManager> = LogManager< - TagsOf | Extra ->; +export type ExtendLogManager> = LogManager | Extra>; type TaggedLogFn = (...args: [string] | [T, string]) => void; diff --git a/src/utils/types.ts b/src/utils/types.ts index d13500452..2b578417a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,5 +1,19 @@ +import type { Log, LogManager } from "./log"; + export type CapitalizeFirst = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; export type IsPrefixed = `is${CapitalizeFirst}`; export type PartialBy = Omit & Partial>; + +export type CodegenTag = + | "BINDING" + | "LARGE_VALUESET" + | "FIELD_TYPE_NOT_FOUND" + | "SKIP_CANONICAL" + | "DUPLICATE_SCHEMA" + | "DUPLICATE_CANONICAL" + | "RESOLVE_BASE"; + +export type CodegenLog = Log; +export type CodegenLogManager = LogManager; diff --git a/test/unit/utils/log.test.ts b/test/unit/utils/log.test.ts index faf93d5ae..c65e15ba5 100644 --- a/test/unit/utils/log.test.ts +++ b/test/unit/utils/log.test.ts @@ -67,9 +67,9 @@ describe("mkLogger", () => { logger.warn("TAG_A", "one"); logger.warn("TAG_A", "two"); logger.info("TAG_B", "three"); - expect(logger.tagCounts()["TAG_A"]).toBe(2); - expect(logger.tagCounts()["TAG_B"]).toBe(1); - expect(logger.tagCounts()["TAG_C"]).toBeUndefined(); + expect(logger.tagCounts().TAG_A).toBe(2); + expect(logger.tagCounts().TAG_B).toBe(1); + expect(logger.tagCounts().TAG_C).toBeUndefined(); }); it("does not increment tag counts for untagged messages", () => { @@ -93,7 +93,7 @@ describe("mkLogger", () => { const l = mkLogger({ suppressTags: ["TAG_A"] }); l.warn("TAG_A", "one"); l.warn("TAG_A", "two"); - expect(l.tagCounts()["TAG_A"]).toBe(2); + expect(l.tagCounts().TAG_A).toBe(2); }); it("suppress() adds tags at runtime", () => { @@ -116,7 +116,7 @@ describe("mkLogger", () => { // but only 1 was not suppressed (the first), the rest are deduped at console level // all are marked suppressed=false since TAG_A is not in suppressTags expect(bufferFilter(logger, { suppressed: false })).toHaveLength(3); - expect(logger.tagCounts()["TAG_A"]).toBe(3); + expect(logger.tagCounts().TAG_A).toBe(3); }); it("different messages are not deduped", () => { @@ -185,8 +185,8 @@ describe("mkLogger", () => { logger.warn("TAG_A", "parent"); child.warn("TAG_A", "child"); child.warn("TAG_A", "child2"); - expect(logger.tagCounts()["TAG_A"]).toBe(1); - expect(child.tagCounts()["TAG_A"]).toBe(2); + expect(logger.tagCounts().TAG_A).toBe(1); + expect(child.tagCounts().TAG_A).toBe(2); }); it("narrows tag set on fork", () => { @@ -349,7 +349,7 @@ describe("mkLogger", () => { it("does not reset tag counts", () => { logger.warn("TAG_A", "msg"); logger.bufferClear(); - expect(logger.tagCounts()["TAG_A"]).toBe(1); + expect(logger.tagCounts().TAG_A).toBe(1); }); }); From 94f447dbc3d7ce4e149c476a4cc9e70e77142aa4 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 11:22:54 +0100 Subject: [PATCH 16/32] fix: Use string literal for LogLevel in local-package-folder example --- examples/local-package-folder/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/local-package-folder/generate.ts b/examples/local-package-folder/generate.ts index fb24b17ec..7b1ca4212 100644 --- a/examples/local-package-folder/generate.ts +++ b/examples/local-package-folder/generate.ts @@ -1,12 +1,12 @@ import * as Path from "node:path"; import { fileURLToPath } from "node:url"; -import { APIBuilder, LogLevel, prettyReport } from "../../src/api"; +import { APIBuilder, prettyReport } from "../../src/api"; const __dirname = Path.dirname(fileURLToPath(import.meta.url)); async function generateFromLocalPackageFolder() { const builder = new APIBuilder({ - logLevel: LogLevel.INFO, + logLevel: "INFO", }); const report = await builder From bb9d514a9821fb787f10628ca380e6db52e8ad90 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:02:17 +0100 Subject: [PATCH 17/32] Use CodegenLogManager/CodegenTag consistently across builder and tests --- examples/python/generate.ts | 5 +++-- src/api/builder.ts | 9 +++++---- test/api/write-generator/typescript.test.ts | 3 ++- test/unit/typeschema/utils.ts | 7 ++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 17c97d007..2b4f29fb9 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,9 +1,10 @@ +import type { CodegenLog } from "@root/utils/types"; import { APIBuilder, prettyReport } from "../../src"; -import { type LogManager, mkLogger } from "../../src/utils/log"; +import { mkLogger } from "../../src/utils/log"; console.log("📦 Generating FHIR R4 Core Types..."); -const logger: LogManager = mkLogger({ +const logger: CodegenLog = mkLogger({ prefix: "API", suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], }); diff --git a/src/api/builder.ts b/src/api/builder.ts index 9d36f0109..04dd03fef 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,8 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type LogLevel, type LogManager, mkLogger } from "@root/utils/log"; +import { type LogLevel, mkLogger } from "@root/utils/log"; +import type { CodegenLogManager } from "@root/utils/types"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; @@ -94,7 +95,7 @@ export interface LocalStructureDefinitionConfig { dependencies?: PackageMeta[]; } -const cleanup = async (opts: APIBuilderOptions, logger: LogManager): Promise => { +const cleanup = async (opts: APIBuilderOptions, logger: CodegenLogManager): Promise => { logger.info(`Cleaning outputs...`); try { logger.info(`Clean ${opts.outputDir}`); @@ -119,7 +120,7 @@ export class APIBuilder { localSDs: LocalPackageConfig[]; localTgzPackages: TgzPackageConfig[]; }; - private logger: LogManager; + private logger: CodegenLogManager; private generators: { name: string; writer: FileSystemWriter }[] = []; constructor( @@ -127,7 +128,7 @@ export class APIBuilder { manager?: ReturnType; register?: Register; preprocessPackage?: (context: PreprocessContext) => PreprocessContext; - logger?: LogManager; + logger?: CodegenLogManager; } = {}, ) { const defaultOpts: APIBuilderOptions = { diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 2c7198c34..501debeed 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; import { mkLogger } from "@root/utils/log"; +import type { CodegenTag } from "@root/utils/types"; import { ccdaManager, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { @@ -52,7 +53,7 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () => }); describe("TypeScript R4 Example (with generateProfile)", async () => { - const logger = mkLogger({ level: "ERROR" }); + const logger = mkLogger({ level: "ERROR" }); const result = await new APIBuilder({ register: r4Manager, logger }) .typescript({ diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index 764d5b6fe..378e84a12 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,7 +2,8 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; -import { type Log, mkLogger } from "@root/utils/log"; +import { mkLogger } from "@root/utils/log"; +import type { CodegenLog, CodegenTag } from "@root/utils/types"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; @@ -10,9 +11,9 @@ import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } export type PFS = Partial; export type PVS = Partial; -const logger = mkLogger({ prefix: "TEST" }); +const logger = mkLogger({ prefix: "TEST" }); -export const mkIndex = async (register: Register, logger?: Log) => { +export const mkIndex = async (register: Register, logger?: CodegenLog) => { const { schemas } = await generateTypeSchemas(register, logger); return mkTypeSchemaIndex(schemas, { register, logger }); }; From 3c207e01b9ca7297282c16963b57fece5c1b6124 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:07:58 +0100 Subject: [PATCH 18/32] fix: Use mkLogger in python example to satisfy type constraint --- examples/python/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 2b4f29fb9..1a2b06fac 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,10 +1,10 @@ -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenTag } from "@root/utils/types"; import { APIBuilder, prettyReport } from "../../src"; import { mkLogger } from "../../src/utils/log"; console.log("📦 Generating FHIR R4 Core Types..."); -const logger: CodegenLog = mkLogger({ +const logger = mkLogger({ prefix: "API", suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], }); From a9d85504dabdcdd1949b7212778d172bbfc42408 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:57:33 +0100 Subject: [PATCH 19/32] Improve log formatting: hashtag tags, dimmed suffix, slash prefixes, tag summary - Rename tags to hashtag camelCase (#binding, #fieldTypeNotFound, etc.) - Move tag display to dimmed suffix: "message (#tag)" instead of "#tag message" - Use "/" separator for forked prefixes: api/reg, api/ts - Replace printSuppressedSummary with printTagSummary that reports both emitted and suppressed tag counts separately - Add fork() overload to preserve parent tag type with suppressTags --- src/utils/log.ts | 32 +++++++++++++++++++++----------- src/utils/types.ts | 14 +++++++------- test/unit/utils/log.test.ts | 31 ++++++++++++++----------------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/utils/log.ts b/src/utils/log.ts index c73098631..8d3fe4814 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -20,13 +20,14 @@ export type Log = { }; export type LogManager = Log & { - fork(prefix: string, opts?: Partial>): LogManager; + fork(prefix: string, opts?: Partial>): LogManager; + fork(prefix: string, opts?: Partial>): LogManager; as(): LogManager; suppress(...tags: T[]): void; setLevel(level: LogLevel): void; tagCounts(): Readonly>; - printSuppressedSummary(): void; + printTagSummary(): void; buffer(): readonly LogEntry[]; bufferClear(): void; @@ -65,9 +66,9 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana }; const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { - const pfx = prefix ? `[${prefix}] ` : ""; - const tagStr = tag ? `[${tag}] ` : ""; - return colorize[level](`${icon} ${pfx}${tagStr}${msg}`); + const pfx = prefix ? `${prefix}: ` : ""; + const tagSuffix = tag ? ` ${pc.dim(`(${tag})`)}` : ""; + return colorize[level](`${icon} ${pfx}${msg}`) + tagSuffix; }; const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { @@ -105,7 +106,7 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana debug: mkLogFn("DEBUG", "D", console.log), fork(childPrefix: string, childOpts?: Partial>): LogManager { - const fullPrefix = prefix ? `${prefix}:${childPrefix}` : childPrefix; + const fullPrefix = prefix ? `${prefix}/${childPrefix}` : childPrefix; const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as C[]; return mkLogger({ prefix: fullPrefix, @@ -130,12 +131,21 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana return tagCounts; }, - printSuppressedSummary() { - const suppressed = Object.entries(tagCounts) - .filter(([tag]) => suppressedSet.has(tag)) - .map(([tag, count]) => `${tag}: ${count}`); + printTagSummary() { + const allTags = Object.entries(tagCounts); + if (allTags.length === 0) return; + const pfx = prefix ? `${prefix}: ` : ""; + const emitted = allTags.filter(([tag]) => !suppressedSet.has(tag)); + const suppressed = allTags.filter(([tag]) => suppressedSet.has(tag)); + if (emitted.length > 0) { + const total = emitted.reduce((sum, [, c]) => sum + c, 0); + const detail = emitted.map(([tag, c]) => `${tag}: ${c}`).join(", "); + console.warn(pc.yellow(`! ${pfx}${total} warnings (${detail})`)); + } if (suppressed.length > 0) { - logger.info(`Suppressed: ${suppressed.join(", ")}`); + const total = suppressed.reduce((sum, [, c]) => sum + c, 0); + const detail = suppressed.map(([tag, c]) => `${tag}: ${c}`).join(", "); + console.log(pc.dim(`i ${pfx}${total} suppressed (${detail})`)); } }, diff --git a/src/utils/types.ts b/src/utils/types.ts index 2b578417a..4ae50b9b2 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -7,13 +7,13 @@ export type IsPrefixed = `is${CapitalizeFirst}`; export type PartialBy = Omit & Partial>; export type CodegenTag = - | "BINDING" - | "LARGE_VALUESET" - | "FIELD_TYPE_NOT_FOUND" - | "SKIP_CANONICAL" - | "DUPLICATE_SCHEMA" - | "DUPLICATE_CANONICAL" - | "RESOLVE_BASE"; + | "#binding" + | "#largeValueSet" + | "#fieldTypeNotFound" + | "#skipCanonical" + | "#duplicateSchema" + | "#duplicateCanonical" + | "#resolveBase"; export type CodegenLog = Log; export type CodegenLogManager = LogManager; diff --git a/test/unit/utils/log.test.ts b/test/unit/utils/log.test.ts index c65e15ba5..3c6b57038 100644 --- a/test/unit/utils/log.test.ts +++ b/test/unit/utils/log.test.ts @@ -143,7 +143,7 @@ describe("mkLogger", () => { it("creates child with combined prefix", () => { const child = logger.fork("child"); child.info("hello"); - expect(child.buffer()[0]?.prefix).toBe("test:child"); + expect(child.buffer()[0]?.prefix).toBe("test/child"); }); it("creates child from root without parent prefix", () => { @@ -278,7 +278,7 @@ describe("mkLogger", () => { child.warn("BASE_B", "visible"); expect(bufferFilter(child, { suppressed: true })).toHaveLength(1); expect(child.buffer()[0]?.tag).toBe("BASE_A"); - expect(child.buffer()[1]?.prefix).toBe("root:child"); + expect(child.buffer()[1]?.prefix).toBe("root/child"); }); }); @@ -353,25 +353,22 @@ describe("mkLogger", () => { }); }); - describe("printSuppressedSummary", () => { - it("emits an info entry with suppressed counts", () => { - const l = mkLogger({ suppressTags: ["TAG_A", "TAG_B"] }); + describe("printTagSummary", () => { + it("prints warning summary with all tag counts", () => { + const l = mkLogger({ prefix: "test", suppressTags: ["TAG_A"] }); l.warn("TAG_A", "a1"); l.warn("TAG_A", "a2"); l.warn("TAG_B", "b1"); - l.printSuppressedSummary(); - - const summaryEntries = bufferFilter(l, { level: "INFO" }); - expect(summaryEntries).toHaveLength(1); - expect(summaryEntries[0]?.message).toContain("TAG_A: 2"); - expect(summaryEntries[0]?.message).toContain("TAG_B: 1"); + l.printTagSummary(); + // printTagSummary writes directly to console, doesn't buffer + expect(l.tagCounts().TAG_A).toBe(2); + expect(l.tagCounts().TAG_B).toBe(1); }); - it("does nothing when no tags are suppressed", () => { - logger.warn("TAG_A", "visible"); - const countBefore = logger.buffer().length; - logger.printSuppressedSummary(); - expect(logger.buffer()).toHaveLength(countBefore); + it("does nothing when no tags exist", () => { + logger.info("no tags"); + logger.printTagSummary(); + // no crash, no extra output }); }); @@ -385,7 +382,7 @@ describe("mkLogger", () => { it("nests prefixes through multiple forks", () => { const child = logger.fork("a").fork("b"); child.info("deep"); - expect(child.buffer()[0]?.prefix).toBe("test:a:b"); + expect(child.buffer()[0]?.prefix).toBe("test/a/b"); }); }); From daf84b5380ff69cebcd2ca7297e98bacfd70ebd4 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:57:52 +0100 Subject: [PATCH 20/32] Rename tag strings to hashtag camelCase across typeschema consumers --- src/typeschema/core/binding.ts | 7 +++++-- src/typeschema/core/field-builder.ts | 4 ++-- src/typeschema/core/transformer.ts | 2 +- src/typeschema/index.ts | 4 ++-- src/typeschema/register.ts | 2 +- src/typeschema/utils.ts | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/typeschema/core/binding.ts b/src/typeschema/core/binding.ts index c8aed0439..2cb8ab064 100644 --- a/src/typeschema/core/binding.ts +++ b/src/typeschema/core/binding.ts @@ -115,7 +115,10 @@ export function buildEnum( if (!valueSetUrl) return undefined; if (!BINDABLE_TYPES.has(element.type ?? "")) { - logger?.dryWarn("BINDING", `eld-11: Binding on non-bindable type '${element.type}' (valueSet: ${valueSetUrl})`); + logger?.dryWarn( + "#binding", + `eld-11: Binding on non-bindable type '${element.type}' (valueSet: ${valueSetUrl})`, + ); return undefined; } @@ -132,7 +135,7 @@ export function buildEnum( if (codes.length > MAX_ENUM_LENGTH) { logger?.dryWarn( - "LARGE_VALUESET", + "#largeValueSet", `Value set ${valueSetUrl} has ${codes.length} which is more than ${MAX_ENUM_LENGTH} codes, which may cause issues with code generation.`, ); return undefined; diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 8c194e189..f7162adb0 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -247,7 +247,7 @@ export function buildFieldType( // Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions // Log a warning but continue processing instead of throwing logger?.dryWarn( - "FIELD_TYPE_NOT_FOUND", + "#fieldTypeNotFound", `Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`, ); return undefined; @@ -276,7 +276,7 @@ export const mkField = ( // TODO: should be an exception if (!fieldType) logger?.dryWarn( - "FIELD_TYPE_NOT_FOUND", + "#fieldTypeNotFound", `Field type not found for '${fhirSchema.url}#${path.join(".")}' (${fhirSchema.derivation})`, ); diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index d51366ef9..8fd2899bb 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -44,7 +44,7 @@ export function mkFields( const fcurl = elemSnapshot.type ? register.ensureSpecializationCanonicalUrl(elemSnapshot.type) : undefined; if (fcurl && shouldSkipCanonical(fhirSchema.package_meta, fcurl).shouldSkip) { logger?.warn( - "SKIP_CANONICAL", + "#skipCanonical", `Skipping field ${path} for ${fcurl} due to skip hack ${shouldSkipCanonical(fhirSchema.package_meta, fcurl).reason}`, ); continue; diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 6bd7bb60f..5d8909b9b 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -59,7 +59,7 @@ const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Cod if (sorted.length > 1) { const pkg = best.typeSchema.identifier.package; const url = best.typeSchema.identifier.url; - logger?.dryWarn("DUPLICATE_SCHEMA", `'${url}' from '${pkg}'' has ${sorted.length} versions`); + logger?.dryWarn("#duplicateSchema", `'${url}' from '${pkg}'' has ${sorted.length} versions`); collisions[pkg] ??= {}; collisions[pkg][url] = sorted.flatMap((v) => v.sources.map((s) => ({ @@ -85,7 +85,7 @@ export const generateTypeSchemas = async ( const skipCheck = shouldSkipCanonical(fhirSchema.package_meta, fhirSchema.url); if (skipCheck.shouldSkip) { - logger?.dryWarn("SKIP_CANONICAL", `Skip ${fhirSchema.url} from ${pkgId}. Reason: ${skipCheck.reason}`); + logger?.dryWarn("#skipCanonical", `Skip ${fhirSchema.url} from ${pkgId}. Reason: ${skipCheck.reason}`); continue; } diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index d53f802b1..21a4821cf 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -99,7 +99,7 @@ const mkPackageAwareResolver = async ( if (!(isStructureDefinition(resource) || isValueSet(resource) || isCodeSystem(resource))) continue; const url = rawUrl as CanonicalUrl; if (index.canonicalResolution[url]) - logger?.dryWarn("DUPLICATE_CANONICAL", `Duplicate canonical URL: ${url} at ${pkgId}.`); + logger?.dryWarn("#duplicateCanonical", `Duplicate canonical URL: ${url} at ${pkgId}.`); index.canonicalResolution[url] = [{ deep, pkg: pkg, pkgId, resource: resource as FocusedResource }]; } diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 9cfbf7cd9..41d4add67 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -274,7 +274,7 @@ export const mkTypeSchemaIndex = ( const resolved = resolve(base); if (!resolved) { logger?.warn( - "RESOLVE_BASE", + "#resolveBase", `Failed to resolve base type: ${res.map((e) => `${e.identifier.url} (${e.identifier.kind})`).join(", ")}`, ); return undefined; From 8ec7feb0963d1732ffc2b88ef37b153f5b309431 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:58:08 +0100 Subject: [PATCH 21/32] Fork logger for register and typeschema stages, print tag summary before generators --- src/api/builder.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/builder.ts b/src/api/builder.ts index 04dd03fef..2695d3fed 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -174,7 +174,7 @@ export class APIBuilder { dropCache: userOpts.dropCanonicalManagerCache, preprocessPackage: userOpts.preprocessPackage, }); - this.logger = userOpts.logger ?? mkLogger({ prefix: "API", level: opts.logLevel }); + this.logger = userOpts.logger ?? mkLogger({ prefix: "api", level: opts.logLevel }); this.options = opts; } @@ -418,22 +418,26 @@ export class APIBuilder { const packageMetas = Object.values(ref2meta); register = await registerFromManager(this.manager, { - logger: this.logger, + logger: this.logger.fork("reg"), focusedPackages: packageMetas, }); } - const { schemas: typeSchemas, collisions } = await generateTypeSchemas(register, this.logger); + const tsLogger = this.logger.fork("ts"); + + const { schemas: typeSchemas, collisions } = await generateTypeSchemas(register, tsLogger); const tsIndexOpts = { register, - logger: this.logger, + logger: tsLogger, irReport: Object.keys(collisions).length > 0 ? { collisions } : {}, }; let tsIndex = mkTypeSchemaIndex(typeSchemas, tsIndexOpts); if (this.options.treeShake) tsIndex = treeShake(tsIndex, this.options.treeShake); if (this.options.promoteLogical) tsIndex = promoteLogical(tsIndex, this.options.promoteLogical); + tsLogger.printTagSummary(); + this.logger.debug(`Executing ${this.generators.length} generators`); await this.executeGenerators(result, tsIndex); From 4da8f75cf19c9b96794bfd0beee5c8e0f6c82059 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 12:58:14 +0100 Subject: [PATCH 22/32] Suppress #fieldTypeNotFound in r4 examples --- examples/python/generate.ts | 2 +- examples/typescript-r4/generate.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 1a2b06fac..18e36d23c 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -6,7 +6,7 @@ console.log("📦 Generating FHIR R4 Core Types..."); const logger = mkLogger({ prefix: "API", - suppressTags: ["FIELD_TYPE_NOT_FOUND", "LARGE_VALUESET"], + suppressTags: ["#fieldTypeNotFound", "#largeValueSet"], }); const builder = new APIBuilder({ logger }) diff --git a/examples/typescript-r4/generate.ts b/examples/typescript-r4/generate.ts index 173407301..6116cb4d8 100644 --- a/examples/typescript-r4/generate.ts +++ b/examples/typescript-r4/generate.ts @@ -2,11 +2,13 @@ // bun run scripts/generate-fhir-types.ts import { APIBuilder, prettyReport } from "../../src/api/builder"; +import { mkLogger } from "../../src/utils/log"; +import type { CodegenTag } from "../../src/utils/types"; if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); - const builder = new APIBuilder() + const builder = new APIBuilder({ logger: mkLogger({ suppressTags: ["#fieldTypeNotFound"] }) }) .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .typescript({ From 6b51a6333ff8dd35d62ca6fc69ff071cfcc7af51 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:14:45 +0100 Subject: [PATCH 23/32] Remove setLogLevel from APIBuilder, pass pre-configured loggers instead --- CLAUDE.md | 2 +- docs/guides/testing-generators.md | 8 +++----- examples/mustache/mustache-java-r4-gen.ts | 4 ++-- src/api/builder.ts | 12 ++---------- test/api/mustache.test.ts | 5 ++--- test/api/write-generator/csharp.test.ts | 5 ++--- test/api/write-generator/introspection.test.ts | 14 +++++--------- test/api/write-generator/multi-package/cda.test.ts | 10 ++++------ .../multi-package/local-package.test.ts | 10 ++++------ .../multi-package/sql-on-fhir.test.ts | 10 ++++------ test/api/write-generator/python.test.ts | 5 ++--- test/api/write-generator/typescript.test.ts | 8 +++----- test/unit/typeschema/utils.ts | 2 ++ 13 files changed, 36 insertions(+), 59 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5bfce572f..6aaddc08d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -241,7 +241,7 @@ assets/api/writer-generator/ 1. Use `builder.introspection({ typeSchemas: "./debug-schemas" })` to inspect intermediate output 2. Check `src/typeschema/types.ts` for TypeSchema structure 3. Review `src/typeschema/core/transformer.ts` for transformation logic -4. Enable verbose logging with `builder.setLogLevel("DEBUG")` +4. Enable verbose logging by passing `mkLogger({ level: "DEBUG" })` to the builder ### Testing Generated Code 1. Use `builder.build()` instead of `generate()` to avoid file I/O diff --git a/docs/guides/testing-generators.md b/docs/guides/testing-generators.md index 4f34fb059..6087ed868 100644 --- a/docs/guides/testing-generators.md +++ b/docs/guides/testing-generators.md @@ -56,11 +56,10 @@ All generator tests follow this basic structure: ```typescript import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { r4Manager } from "@typeschema-test/utils"; +import { silentLogger, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { - const result = await new APIBuilder({ manager: r4Manager }) - .setLogLevel("SILENT") + const result = await new APIBuilder({ manager: r4Manager, logger: silentLogger }) .typescript({ inMemoryOnly: true, }) @@ -79,8 +78,7 @@ describe("TypeScript Writer Generator", async () => { ### Key Components **APIBuilder Setup:** -- Initialize with test manager: `new APIBuilder({ manager: r4Manager })` -- Set log level to SILENT to reduce noise: `.setLogLevel("SILENT")` +- Initialize with test manager and silent logger: `new APIBuilder({ manager: r4Manager, logger: silentLogger })` - Choose generator method: `.typescript()`, `.python()`, `.csharp()`, `.mustache()` - Enable in-memory mode: `inMemoryOnly: true` (no file I/O) diff --git a/examples/mustache/mustache-java-r4-gen.ts b/examples/mustache/mustache-java-r4-gen.ts index 942bcac3f..69d0548d5 100644 --- a/examples/mustache/mustache-java-r4-gen.ts +++ b/examples/mustache/mustache-java-r4-gen.ts @@ -1,10 +1,10 @@ import { APIBuilder, prettyReport } from "../../src/api/builder"; +import { mkLogger } from "../../src/utils/log"; if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); - const builder = new APIBuilder() - .setLogLevel("DEBUG") + const builder = new APIBuilder({ logger: mkLogger({ level: "DEBUG" }) }) .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .outputTo("./examples/mustache/mustache-java-r4-output") diff --git a/src/api/builder.ts b/src/api/builder.ts index 2695d3fed..789979bbb 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type LogLevel, mkLogger } from "@root/utils/log"; +import { mkLogger } from "@root/utils/log"; import type { CodegenLogManager } from "@root/utils/types"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; @@ -42,8 +42,6 @@ export interface APIBuilderOptions { treeShake: TreeShakeConf | undefined; promoteLogical: LogicalPromotionConf | undefined; - /** Log level for the logger. Default: INFO */ - logLevel: LogLevel; /** Custom FHIR package registry URL (default: https://fs.get-ig.org/pkgs/) */ registry: string | undefined; /** Drop the canonical manager cache */ @@ -138,7 +136,6 @@ export class APIBuilder { treeShake: undefined, promoteLogical: undefined, registry: undefined, - logLevel: "INFO", dropCanonicalManagerCache: false, }; const opts: APIBuilderOptions = { @@ -174,7 +171,7 @@ export class APIBuilder { dropCache: userOpts.dropCanonicalManagerCache, preprocessPackage: userOpts.preprocessPackage, }); - this.logger = userOpts.logger ?? mkLogger({ prefix: "api", level: opts.logLevel }); + this.logger = userOpts.logger ?? mkLogger({ prefix: "api" }); this.options = opts; } @@ -335,11 +332,6 @@ export class APIBuilder { return this; } - setLogLevel(level: LogLevel): APIBuilder { - this.logger?.setLevel(level); - return this; - } - throwException(enabled = true): APIBuilder { this.options.throwException = enabled; return this; diff --git a/test/api/mustache.test.ts b/test/api/mustache.test.ts index d0585139d..95ed874c2 100644 --- a/test/api/mustache.test.ts +++ b/test/api/mustache.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { r4Manager } from "@typeschema-test/utils"; +import { errorLogger, r4Manager } from "@typeschema-test/utils"; describe("Mustache Template Based Generation", async () => { - const report = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const report = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .mustache("./examples/mustache/java", { debug: "COMPACT", inMemoryOnly: true, diff --git a/test/api/write-generator/csharp.test.ts b/test/api/write-generator/csharp.test.ts index 111388682..20decb78f 100644 --- a/test/api/write-generator/csharp.test.ts +++ b/test/api/write-generator/csharp.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { r4Manager } from "@typeschema-test/utils"; +import { errorLogger, r4Manager } from "@typeschema-test/utils"; describe("C# Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .csharp({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/introspection.test.ts b/test/api/write-generator/introspection.test.ts index af1116f6a..ed801b0e5 100644 --- a/test/api/write-generator/introspection.test.ts +++ b/test/api/write-generator/introspection.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { r4Manager } from "@typeschema-test/utils"; +import { errorLogger, r4Manager } from "@typeschema-test/utils"; describe("IntrospectionWriter - Fhir Schema Output", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .introspection({ fhirSchemas: "introspection" }) .introspection({ fhirSchemas: "introspection.ndjson" }) .generate(); @@ -27,8 +26,7 @@ describe("IntrospectionWriter - Fhir Schema Output", async () => { }); describe("IntrospectionWriter - TypeSchema output", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -67,8 +65,7 @@ describe("IntrospectionWriter - TypeSchema output", async () => { }); describe("IntrospectionWriter - typeTree", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -93,8 +90,7 @@ describe("IntrospectionWriter - typeTree", async () => { }); describe("IntrospectionWriter - StructureDefinition output", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { diff --git a/test/api/write-generator/multi-package/cda.test.ts b/test/api/write-generator/multi-package/cda.test.ts index f1daf1a27..d16714385 100644 --- a/test/api/write-generator/multi-package/cda.test.ts +++ b/test/api/write-generator/multi-package/cda.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; +import { silentLogger } from "@typeschema-test/utils"; /** * Tests for CDA package generation. @@ -18,8 +19,7 @@ describe("CDA", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -45,8 +45,7 @@ describe("CDA", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -69,8 +68,7 @@ describe("CDA", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/local-package.test.ts b/test/api/write-generator/multi-package/local-package.test.ts index e3a8e9594..56bd377e0 100644 --- a/test/api/write-generator/multi-package/local-package.test.ts +++ b/test/api/write-generator/multi-package/local-package.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test"; import * as Path from "node:path"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; +import { silentLogger } from "@typeschema-test/utils"; const LOCAL_PACKAGE_PATH = Path.join(__dirname, "../../../assets/local-package/structure-definitions"); @@ -29,8 +30,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -61,8 +61,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -98,8 +97,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/sql-on-fhir.test.ts b/test/api/write-generator/multi-package/sql-on-fhir.test.ts index 29228e42d..6a707ddd1 100644 --- a/test/api/write-generator/multi-package/sql-on-fhir.test.ts +++ b/test/api/write-generator/multi-package/sql-on-fhir.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; +import { silentLogger } from "@typeschema-test/utils"; /** * Tests for SQL-on-FHIR package. @@ -20,8 +21,7 @@ describe("SQL-on-FHIR", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -54,8 +54,7 @@ describe("SQL-on-FHIR", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -85,8 +84,7 @@ describe("SQL-on-FHIR", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder() - .setLogLevel("SILENT") + const result = await new APIBuilder({ logger: silentLogger }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/python.test.ts b/test/api/write-generator/python.test.ts index 6b1f37077..633041688 100644 --- a/test/api/write-generator/python.test.ts +++ b/test/api/write-generator/python.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { r4Manager } from "@typeschema-test/utils"; +import { errorLogger, r4Manager } from "@typeschema-test/utils"; describe("Python Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .python({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 501debeed..e9480c069 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -3,11 +3,10 @@ import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; import { mkLogger } from "@root/utils/log"; import type { CodegenTag } from "@root/utils/types"; -import { ccdaManager, r4Manager } from "@typeschema-test/utils"; +import { ccdaManager, errorLogger, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) .typescript({ inMemoryOnly: true, }) @@ -30,8 +29,7 @@ describe("TypeScript Writer Generator", async () => { }); describe("TypeScript CDA with Logical Model Promotion to Resource", async () => { - const result = await new APIBuilder({ register: ccdaManager }) - .setLogLevel("ERROR") + const result = await new APIBuilder({ register: ccdaManager, logger: errorLogger }) .typeSchema({ promoteLogical: { "hl7.cda.uv.core": ["http://hl7.org/cda/stds/core/StructureDefinition/Material" as CanonicalUrl], diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index 378e84a12..d0f07100e 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -12,6 +12,8 @@ export type PFS = Partial; export type PVS = Partial; const logger = mkLogger({ prefix: "TEST" }); +export const errorLogger = mkLogger({ level: "ERROR" }); +export const silentLogger = mkLogger({ level: "SILENT" }); export const mkIndex = async (register: Register, logger?: CodegenLog) => { const { schemas } = await generateTypeSchemas(register, logger); From 302572f47a4a32d9f0a8ea03f201925fa5b2661f Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:27:42 +0100 Subject: [PATCH 24/32] Remove generic fork overload, keep only fork preserving parent tag type --- src/utils/log.ts | 7 +++---- test/unit/utils/log.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/utils/log.ts b/src/utils/log.ts index 8d3fe4814..9f5a48206 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -21,7 +21,6 @@ export type Log = { export type LogManager = Log & { fork(prefix: string, opts?: Partial>): LogManager; - fork(prefix: string, opts?: Partial>): LogManager; as(): LogManager; suppress(...tags: T[]): void; @@ -105,10 +104,10 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana error: mkLogFn("ERROR", "X", console.error), debug: mkLogFn("DEBUG", "D", console.log), - fork(childPrefix: string, childOpts?: Partial>): LogManager { + fork(childPrefix: string, childOpts?: Partial>): LogManager { const fullPrefix = prefix ? `${prefix}/${childPrefix}` : childPrefix; - const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as C[]; - return mkLogger({ + const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as T[]; + return mkLogger({ prefix: fullPrefix, suppressTags: merged, level: childOpts?.level ?? currentLevel, diff --git a/test/unit/utils/log.test.ts b/test/unit/utils/log.test.ts index 3c6b57038..496b6a34c 100644 --- a/test/unit/utils/log.test.ts +++ b/test/unit/utils/log.test.ts @@ -162,7 +162,7 @@ describe("mkLogger", () => { it("adds child-specific suppressTags", () => { const parent = mkLogger({ suppressTags: ["TAG_A"] }); - const child = parent.fork("child", { suppressTags: ["TAG_B"] }); + const child = parent.fork("child", { suppressTags: ["TAG_B"] }); child.warn("TAG_A", "from parent"); child.warn("TAG_B", "from child"); child.warn("TAG_C", "not suppressed"); @@ -189,9 +189,9 @@ describe("mkLogger", () => { expect(child.tagCounts().TAG_A).toBe(2); }); - it("narrows tag set on fork", () => { + it("narrows tag set via as()", () => { type Narrow = "TAG_A"; - const child = logger.fork("narrow"); + const child = logger.fork("narrow").as(); child.warn("TAG_A", "valid"); expect(child.buffer()[0]?.tag).toBe("TAG_A"); }); @@ -273,7 +273,7 @@ describe("mkLogger", () => { prefix: "root", suppressTags: ["BASE_A"], }); - const child = extended.fork("child"); + const child = extended.fork("child").as(); child.warn("BASE_A", "suppressed from parent"); child.warn("BASE_B", "visible"); expect(bufferFilter(child, { suppressed: true })).toHaveLength(1); From 97606a28283e8467cf4177faf3f65c61f07b1c15 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:30:46 +0100 Subject: [PATCH 25/32] Remove suppress() and setLevel() from LogManager, configure via mkLogger/fork only --- src/api/builder.ts | 2 +- src/cli/commands/index.ts | 4 ++-- src/utils/log.ts | 12 +----------- test/unit/utils/log.test.ts | 37 ------------------------------------- 4 files changed, 4 insertions(+), 51 deletions(-) diff --git a/src/api/builder.ts b/src/api/builder.ts index 789979bbb..298353566 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,7 +23,7 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { mkLogger } from "@root/utils/log"; +import { type LogLevel, mkLogger } from "@root/utils/log"; import type { CodegenLogManager } from "@root/utils/types"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index d1f1b1bf8..c33d26d34 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -22,11 +22,11 @@ export interface CLIArgv { logLevel?: LogLevel; } -const cliLogger = mkLogger({ prefix: "cli" }); +let cliLogger = mkLogger({ prefix: "cli" }); async function setupLoggingMiddleware(argv: any) { const level: LogLevel = argv.logLevel ?? (argv.debug || argv.verbose ? "DEBUG" : "INFO"); - cliLogger.setLevel(level); + cliLogger = mkLogger({ prefix: "cli", level }); } /** diff --git a/src/utils/log.ts b/src/utils/log.ts index 9f5a48206..a33577d9c 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -23,8 +23,6 @@ export type LogManager = Log & { fork(prefix: string, opts?: Partial>): LogManager; as(): LogManager; - suppress(...tags: T[]): void; - setLevel(level: LogLevel): void; tagCounts(): Readonly>; printTagSummary(): void; @@ -52,7 +50,7 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana const tagCounts: Record = {}; const entries: LogEntry[] = []; const drySet = new Set(); - let currentLevel: LogLevel = opts.level ?? "INFO"; + const currentLevel: LogLevel = opts.level ?? "INFO"; const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; @@ -118,14 +116,6 @@ export function mkLogger(opts: LoggerOptions = {}): LogMana return logger as unknown as LogManager; }, - suppress(...tags: T[]) { - for (const tag of tags) suppressedSet.add(tag); - }, - - setLevel(level: LogLevel) { - currentLevel = level; - }, - tagCounts(): Readonly> { return tagCounts; }, diff --git a/test/unit/utils/log.test.ts b/test/unit/utils/log.test.ts index 496b6a34c..66fe7044a 100644 --- a/test/unit/utils/log.test.ts +++ b/test/unit/utils/log.test.ts @@ -95,15 +95,6 @@ describe("mkLogger", () => { l.warn("TAG_A", "two"); expect(l.tagCounts().TAG_A).toBe(2); }); - - it("suppress() adds tags at runtime", () => { - logger.warn("TAG_B", "before"); - expect(logger.buffer()[0]?.suppressed).toBe(false); - - logger.suppress("TAG_B"); - logger.warn("TAG_B", "after"); - expect(logger.buffer()[1]?.suppressed).toBe(true); - }); }); describe("dryWarn deduplication", () => { @@ -195,17 +186,6 @@ describe("mkLogger", () => { child.warn("TAG_A", "valid"); expect(child.buffer()[0]?.tag).toBe("TAG_A"); }); - - it("inherits runtime suppress() calls", () => { - const parent = mkLogger({ suppressTags: ["TAG_A"] }); - parent.suppress("TAG_B"); - const child = parent.fork("child"); - child.warn("TAG_A", "from init"); - child.warn("TAG_B", "from runtime suppress"); - child.warn("TAG_C", "not suppressed"); - expect(bufferFilter(child, { suppressed: true })).toHaveLength(2); - expect(child.buffer()[2]?.suppressed).toBe(false); - }); }); describe("as (narrowing)", () => { @@ -224,14 +204,6 @@ describe("mkLogger", () => { narrow.warn("TAG_A", "suppressed via parent"); expect(narrow.buffer()[0]?.suppressed).toBe(true); }); - - it("suppress on narrowed logger affects original", () => { - type Narrow = "TAG_A" | "TAG_B"; - const narrow = logger.as(); - narrow.suppress("TAG_A"); - logger.warn("TAG_A", "should be suppressed"); - expect(logger.buffer()[0]?.suppressed).toBe(true); - }); }); describe("ExtendLogManager (extending)", () => { @@ -404,15 +376,6 @@ describe("mkLogger", () => { expect(l.buffer()).toHaveLength(4); }); - it("setLevel changes level at runtime", () => { - const l = mkLogger({ level: "INFO" }); - l.debug("before"); - l.setLevel("DEBUG"); - l.debug("after"); - // both buffered regardless - expect(l.buffer()).toHaveLength(2); - }); - it("fork inherits parent level", () => { const parent = mkLogger({ level: "WARN" }); const child = parent.fork("child"); From 374f6ca368fefef959c0db03d2d7109b9de3a166 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:37:22 +0100 Subject: [PATCH 26/32] Export logger from package API, add mkCodegenLogger for examples --- examples/mustache/mustache-java-r4-gen.ts | 5 ++--- examples/python/generate.ts | 6 ++---- examples/typescript-r4/generate.ts | 6 ++---- src/api/index.ts | 5 ++++- src/utils/types.ts | 6 +++++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/mustache/mustache-java-r4-gen.ts b/examples/mustache/mustache-java-r4-gen.ts index 69d0548d5..4484436c7 100644 --- a/examples/mustache/mustache-java-r4-gen.ts +++ b/examples/mustache/mustache-java-r4-gen.ts @@ -1,10 +1,9 @@ -import { APIBuilder, prettyReport } from "../../src/api/builder"; -import { mkLogger } from "../../src/utils/log"; +import { APIBuilder, mkCodegenLogger, prettyReport } from "../../src"; if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); - const builder = new APIBuilder({ logger: mkLogger({ level: "DEBUG" }) }) + const builder = new APIBuilder({ logger: mkCodegenLogger({ level: "DEBUG" }) }) .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .outputTo("./examples/mustache/mustache-java-r4-output") diff --git a/examples/python/generate.ts b/examples/python/generate.ts index 18e36d23c..e12f7782f 100644 --- a/examples/python/generate.ts +++ b/examples/python/generate.ts @@ -1,10 +1,8 @@ -import type { CodegenTag } from "@root/utils/types"; -import { APIBuilder, prettyReport } from "../../src"; -import { mkLogger } from "../../src/utils/log"; +import { APIBuilder, mkCodegenLogger, prettyReport } from "../../src"; console.log("📦 Generating FHIR R4 Core Types..."); -const logger = mkLogger({ +const logger = mkCodegenLogger({ prefix: "API", suppressTags: ["#fieldTypeNotFound", "#largeValueSet"], }); diff --git a/examples/typescript-r4/generate.ts b/examples/typescript-r4/generate.ts index 6116cb4d8..81b31c6d8 100644 --- a/examples/typescript-r4/generate.ts +++ b/examples/typescript-r4/generate.ts @@ -1,14 +1,12 @@ // Run this script using Bun CLI with: // bun run scripts/generate-fhir-types.ts -import { APIBuilder, prettyReport } from "../../src/api/builder"; -import { mkLogger } from "../../src/utils/log"; -import type { CodegenTag } from "../../src/utils/types"; +import { APIBuilder, mkCodegenLogger, prettyReport } from "../../src"; if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); - const builder = new APIBuilder({ logger: mkLogger({ suppressTags: ["#fieldTypeNotFound"] }) }) + const builder = new APIBuilder({ logger: mkCodegenLogger({ suppressTags: ["#fieldTypeNotFound"] }) }) .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .typescript({ diff --git a/src/api/index.ts b/src/api/index.ts index dc3e11a55..d92ac77b7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,7 +8,10 @@ */ export type { IrConf, LogicalPromotionConf, TreeShakeConf } from "../typeschema/ir/types"; -export type { LogLevel } from "../utils/log"; +export type { Log, LogEntry, LogLevel, LogManager } from "../utils/log"; +export { mkLogger } from "../utils/log"; +export type { CodegenLog, CodegenLogManager, CodegenTag } from "../utils/types"; +export { mkCodegenLogger } from "../utils/types"; export type { APIBuilderOptions, LocalStructureDefinitionConfig } from "./builder"; export { APIBuilder, prettyReport } from "./builder"; export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; diff --git a/src/utils/types.ts b/src/utils/types.ts index 4ae50b9b2..0f66f4d45 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,4 +1,4 @@ -import type { Log, LogManager } from "./log"; +import { type Log, type LogManager, mkLogger } from "./log"; export type CapitalizeFirst = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; @@ -17,3 +17,7 @@ export type CodegenTag = export type CodegenLog = Log; export type CodegenLogManager = LogManager; + +export const mkCodegenLogger = ( + opts: { prefix?: string; suppressTags?: CodegenTag[]; level?: "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT" } = {}, +) => mkLogger(opts); From 6847111cdcc834f92245399ac0dcf34a6834d4fa Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:41:09 +0100 Subject: [PATCH 27/32] Clean up package exports: hide Log/LogEntry/LogManager/mkLogger, reuse LoggerOptions in mkCodegenLogger --- src/api/index.ts | 3 +-- src/utils/log.ts | 2 +- src/utils/types.ts | 6 ++---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index d92ac77b7..bce31703e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,8 +8,7 @@ */ export type { IrConf, LogicalPromotionConf, TreeShakeConf } from "../typeschema/ir/types"; -export type { Log, LogEntry, LogLevel, LogManager } from "../utils/log"; -export { mkLogger } from "../utils/log"; +export type { LogLevel } from "../utils/log"; export type { CodegenLog, CodegenLogManager, CodegenTag } from "../utils/types"; export { mkCodegenLogger } from "../utils/types"; export type { APIBuilderOptions, LocalStructureDefinitionConfig } from "./builder"; diff --git a/src/utils/log.ts b/src/utils/log.ts index a33577d9c..9cd24581d 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -36,7 +36,7 @@ export type ExtendLogManager = (...args: [string] | [T, string]) => void; -type LoggerOptions = { +export type LoggerOptions = { prefix?: string; suppressTags?: T[]; level?: LogLevel; diff --git a/src/utils/types.ts b/src/utils/types.ts index 0f66f4d45..00aadd60b 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,4 +1,4 @@ -import { type Log, type LogManager, mkLogger } from "./log"; +import { type Log, type LogManager, type LoggerOptions, mkLogger } from "./log"; export type CapitalizeFirst = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; @@ -18,6 +18,4 @@ export type CodegenTag = export type CodegenLog = Log; export type CodegenLogManager = LogManager; -export const mkCodegenLogger = ( - opts: { prefix?: string; suppressTags?: CodegenTag[]; level?: "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT" } = {}, -) => mkLogger(opts); +export const mkCodegenLogger = (opts: LoggerOptions = {}) => mkLogger(opts); From 4151cf7fd34a4eac48ec67fb167934b0d5e61640 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:46:39 +0100 Subject: [PATCH 28/32] Fix stale logger references in CLAUDE.md and writer-generator guide --- CLAUDE.md | 2 +- docs/guides/writer-generator.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6aaddc08d..96409a5e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -241,7 +241,7 @@ assets/api/writer-generator/ 1. Use `builder.introspection({ typeSchemas: "./debug-schemas" })` to inspect intermediate output 2. Check `src/typeschema/types.ts` for TypeSchema structure 3. Review `src/typeschema/core/transformer.ts` for transformation logic -4. Enable verbose logging by passing `mkLogger({ level: "DEBUG" })` to the builder +4. Enable verbose logging by passing `mkCodegenLogger({ level: "DEBUG" })` to the builder ### Testing Generated Code 1. Use `builder.build()` instead of `generate()` to avoid file I/O diff --git a/docs/guides/writer-generator.md b/docs/guides/writer-generator.md index ed46e0c12..2e683298b 100644 --- a/docs/guides/writer-generator.md +++ b/docs/guides/writer-generator.md @@ -77,7 +77,7 @@ The `FileSystemWriter` class handles file I/O and directory management. It requi export type FileSystemWriterOptions = { outputDir: string; // Where to write files inMemoryOnly?: boolean; // Don't write to disk (for testing) - logger?: CodegenLogger; // Optional logging + logger?: CodegenLogManager; // Optional logging resolveAssets?: (fn: string) => string; // Asset resolution function }; ``` From 1ed16b9caa5427e7a2b42d51fb1213f323a9dcfa Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 13:52:04 +0100 Subject: [PATCH 29/32] =?UTF-8?q?Rename=20utils/log.ts=20=E2=86=92=20utils?= =?UTF-8?q?/common-log.ts,=20extract=20codegen=20logging=20to=20utils/log.?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move generic logger engine to common-log.ts and codegen-specific logging (CodegenTag, CodegenLog, CodegenLogManager, mkCodegenLogger) from utils/types.ts into utils/log.ts. utils/types.ts now only contains generic utility types. --- src/api/builder.ts | 4 +- src/api/index.ts | 5 +- src/api/writer-generator/mustache.ts | 2 +- src/api/writer-generator/writer.ts | 2 +- src/typeschema/core/binding.ts | 2 +- src/typeschema/core/field-builder.ts | 2 +- src/typeschema/core/nested-types.ts | 2 +- src/typeschema/core/profile-extensions.ts | 2 +- src/typeschema/core/transformer.ts | 2 +- src/typeschema/index.ts | 2 +- src/typeschema/ir/tree-shake.ts | 2 +- src/typeschema/register.ts | 2 +- src/typeschema/types.ts | 3 +- src/typeschema/utils.ts | 2 +- src/utils/common-log.ts | 151 +++++++++++++++++ src/utils/log.ts | 176 +++----------------- src/utils/types.ts | 16 -- test/api/write-generator/typescript.test.ts | 2 +- test/unit/typeschema/utils.ts | 2 +- 19 files changed, 195 insertions(+), 186 deletions(-) create mode 100644 src/utils/common-log.ts diff --git a/src/api/builder.ts b/src/api/builder.ts index 298353566..5ae84a4d5 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -23,8 +23,8 @@ import type { IrConf, LogicalPromotionConf, TreeShakeConf } from "@root/typesche import { type Register, registerFromManager } from "@root/typeschema/register"; import { type PackageMeta, packageMetaToNpm } from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; -import { type LogLevel, mkLogger } from "@root/utils/log"; -import type { CodegenLogManager } from "@root/utils/types"; +import type { CodegenLogManager } from "@root/utils/log"; +import { mkLogger } from "@root/utils/log"; import { IntrospectionWriter, type IntrospectionWriterOptions } from "./writer-generator/introspection"; import { IrReportWriterWriter, type IrReportWriterWriterOptions } from "./writer-generator/ir-report"; import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; diff --git a/src/api/index.ts b/src/api/index.ts index bce31703e..d507869e2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,9 +8,8 @@ */ export type { IrConf, LogicalPromotionConf, TreeShakeConf } from "../typeschema/ir/types"; -export type { LogLevel } from "../utils/log"; -export type { CodegenLog, CodegenLogManager, CodegenTag } from "../utils/types"; -export { mkCodegenLogger } from "../utils/types"; +export type { CodegenLog, CodegenLogManager, CodegenTag, LogLevel } from "../utils/log"; +export { mkCodegenLogger } from "../utils/log"; export type { APIBuilderOptions, LocalStructureDefinitionConfig } from "./builder"; export { APIBuilder, prettyReport } from "./builder"; export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts index 16d72067d..81b26744a 100644 --- a/src/api/writer-generator/mustache.ts +++ b/src/api/writer-generator/mustache.ts @@ -22,7 +22,7 @@ import type { ViewModel, } from "@mustache/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import { default as Mustache } from "mustache"; import { FileSystemWriter, type FileSystemWriterOptions } from "./writer"; diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index 26a8d05c0..81c946d1b 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import * as Path from "node:path"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; export type FileSystemWriterOptions = { outputDir: string; diff --git a/src/typeschema/core/binding.ts b/src/typeschema/core/binding.ts index 2cb8ab064..2e120779d 100644 --- a/src/typeschema/core/binding.ts +++ b/src/typeschema/core/binding.ts @@ -7,7 +7,7 @@ import assert from "node:assert"; import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { CodeSystem, CodeSystemConcept } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import type { Register } from "@typeschema/register"; import type { BindingTypeSchema, diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index f7162adb0..3b8acbf4e 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import { packageMetaToFhir } from "@typeschema/types"; import type { BindingIdentifier, diff --git a/src/typeschema/core/nested-types.ts b/src/typeschema/core/nested-types.ts index f9250a22f..957c4dee8 100644 --- a/src/typeschema/core/nested-types.ts +++ b/src/typeschema/core/nested-types.ts @@ -6,7 +6,7 @@ import type { FHIRSchema, FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { mergeFsElementProps, type Register, resolveFsElementGenealogy } from "@root/typeschema/register"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import type { CanonicalUrl, Field, Identifier, Name, NestedIdentifier, NestedType, RichFHIRSchema } from "../types"; import { mkField, mkNestedField } from "./field-builder"; diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index d5bcaeed2..484effae2 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -7,7 +7,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import type { Register } from "@root/typeschema/register"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import { type CanonicalUrl, concatIdentifiers, diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 8fd2899bb..6e759bfa0 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -6,7 +6,7 @@ import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { shouldSkipCanonical } from "@root/typeschema/skip-hack"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import type { Register } from "@typeschema/register"; import { concatIdentifiers, diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index 5d8909b9b..a4d9d1f39 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -10,7 +10,7 @@ * - Validating TypeSchema documents */ -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import { transformFhirSchema, transformValueSet } from "./core/transformer"; import type { TypeSchemaCollisions } from "./ir/types"; import type { Register } from "./register"; diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index 5f060f16f..cc5a0bc70 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import { extractDependencies } from "../core/transformer"; import { type CanonicalUrl, diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index 21a4821cf..39e8f3f19 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -7,7 +7,7 @@ import { type StructureDefinition, } from "@atomic-ehr/fhirschema"; import { type CodeSystem, isCodeSystem, isValueSet, type ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import type { CanonicalUrl, Name, diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index dee64696c..b452817b7 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -7,6 +7,7 @@ import { createHash } from "node:crypto"; import type { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager"; import type * as FS from "@atomic-ehr/fhirschema"; import type { StructureDefinition, ValueSet, ValueSetCompose } from "@root/fhir-types/hl7-fhir-r4-core"; +import type { CodegenLog } from "@root/utils/log"; export type Name = string & { readonly __brand: unique symbol }; export type CanonicalUrl = string & { readonly __brand: unique symbol }; @@ -362,7 +363,7 @@ export const enrichValueSet = (vs: ValueSet, packageMeta: PackageMeta): RichValu /////////////////////////////////////////////////////////// export interface TypeschemaGeneratorOptions { - logger?: import("../utils/types").CodegenLog; + logger?: CodegenLog; treeshake?: string[]; manager: ReturnType; /** Custom FHIR package registry URL */ diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 41d4add67..f2dda9007 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -1,6 +1,6 @@ import * as afs from "node:fs/promises"; import * as Path from "node:path"; -import type { CodegenLog } from "@root/utils/types"; +import type { CodegenLog } from "@root/utils/log"; import * as YAML from "yaml"; import type { IrReport } from "./ir/types"; import type { Register } from "./register"; diff --git a/src/utils/common-log.ts b/src/utils/common-log.ts new file mode 100644 index 000000000..9cd24581d --- /dev/null +++ b/src/utils/common-log.ts @@ -0,0 +1,151 @@ +import pc from "picocolors"; + +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; + +export type LogEntry = { + level: LogLevel; + tag?: T; + message: string; + suppressed: boolean; + prefix: string; + timestamp: number; +}; + +export type Log = { + warn: TaggedLogFn; + dryWarn: TaggedLogFn; + info: TaggedLogFn; + error: TaggedLogFn; + debug: TaggedLogFn; +}; + +export type LogManager = Log & { + fork(prefix: string, opts?: Partial>): LogManager; + as(): LogManager; + + tagCounts(): Readonly>; + printTagSummary(): void; + + buffer(): readonly LogEntry[]; + bufferClear(): void; +}; + +type TagsOf = L extends LogManager ? T : never; + +export type ExtendLogManager> = LogManager | Extra>; + +type TaggedLogFn = (...args: [string] | [T, string]) => void; + +export type LoggerOptions = { + prefix?: string; + suppressTags?: T[]; + level?: LogLevel; +}; + +const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; + +export function mkLogger(opts: LoggerOptions = {}): LogManager { + const prefix = opts.prefix ?? ""; + const suppressedSet = new Set(opts.suppressTags ?? []); + const tagCounts: Record = {}; + const entries: LogEntry[] = []; + const drySet = new Set(); + const currentLevel: LogLevel = opts.level ?? "INFO"; + + const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; + + const colorize: Record string> = { + DEBUG: (s) => s, + INFO: (s) => s, + WARN: pc.yellow, + ERROR: pc.red, + SILENT: (s) => s, + }; + + const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { + const pfx = prefix ? `${prefix}: ` : ""; + const tagSuffix = tag ? ` ${pc.dim(`(${tag})`)}` : ""; + return colorize[level](`${icon} ${pfx}${msg}`) + tagSuffix; + }; + + const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { + entries.push({ level, tag, message: msg, suppressed, prefix, timestamp: Date.now() }); + }; + + const mkLogFn = ( + level: LogLevel, + icon: string, + consoleFn: (...args: any[]) => void, + dedupe = false, + ): TaggedLogFn => { + return (...args: [string] | [T, string]) => { + const tag = args.length === 2 ? args[0] : undefined; + const msg = args.length === 2 ? args[1] : args[0]; + if (tag) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1; + const isSuppressed = tag !== undefined && suppressedSet.has(tag); + pushEntry(level, msg, tag, isSuppressed); + if (isSuppressed) return; + if (!shouldLog(level)) return; + if (dedupe) { + const key = `${level}::${tag ?? ""}::${msg}`; + if (drySet.has(key)) return; + drySet.add(key); + } + consoleFn(fmt(level, icon, msg, tag)); + }; + }; + + const logger: LogManager = { + warn: mkLogFn("WARN", "!", console.warn), + dryWarn: mkLogFn("WARN", "!", console.warn, true), + info: mkLogFn("INFO", "i", console.log), + error: mkLogFn("ERROR", "X", console.error), + debug: mkLogFn("DEBUG", "D", console.log), + + fork(childPrefix: string, childOpts?: Partial>): LogManager { + const fullPrefix = prefix ? `${prefix}/${childPrefix}` : childPrefix; + const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as T[]; + return mkLogger({ + prefix: fullPrefix, + suppressTags: merged, + level: childOpts?.level ?? currentLevel, + }); + }, + + as(): LogManager { + return logger as unknown as LogManager; + }, + + tagCounts(): Readonly> { + return tagCounts; + }, + + printTagSummary() { + const allTags = Object.entries(tagCounts); + if (allTags.length === 0) return; + const pfx = prefix ? `${prefix}: ` : ""; + const emitted = allTags.filter(([tag]) => !suppressedSet.has(tag)); + const suppressed = allTags.filter(([tag]) => suppressedSet.has(tag)); + if (emitted.length > 0) { + const total = emitted.reduce((sum, [, c]) => sum + c, 0); + const detail = emitted.map(([tag, c]) => `${tag}: ${c}`).join(", "); + console.warn(pc.yellow(`! ${pfx}${total} warnings (${detail})`)); + } + if (suppressed.length > 0) { + const total = suppressed.reduce((sum, [, c]) => sum + c, 0); + const detail = suppressed.map(([tag, c]) => `${tag}: ${c}`).join(", "); + console.log(pc.dim(`i ${pfx}${total} suppressed (${detail})`)); + } + }, + + buffer(): readonly LogEntry[] { + return entries; + }, + + bufferClear() { + entries.length = 0; + }, + }; + + return logger; +} diff --git a/src/utils/log.ts b/src/utils/log.ts index 9cd24581d..f9b1407bd 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,151 +1,25 @@ -import pc from "picocolors"; - -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "SILENT"; - -export type LogEntry = { - level: LogLevel; - tag?: T; - message: string; - suppressed: boolean; - prefix: string; - timestamp: number; -}; - -export type Log = { - warn: TaggedLogFn; - dryWarn: TaggedLogFn; - info: TaggedLogFn; - error: TaggedLogFn; - debug: TaggedLogFn; -}; - -export type LogManager = Log & { - fork(prefix: string, opts?: Partial>): LogManager; - as(): LogManager; - - tagCounts(): Readonly>; - printTagSummary(): void; - - buffer(): readonly LogEntry[]; - bufferClear(): void; -}; - -type TagsOf = L extends LogManager ? T : never; - -export type ExtendLogManager> = LogManager | Extra>; - -type TaggedLogFn = (...args: [string] | [T, string]) => void; - -export type LoggerOptions = { - prefix?: string; - suppressTags?: T[]; - level?: LogLevel; -}; - -const LEVEL_PRIORITY: Record = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, SILENT: 4 }; - -export function mkLogger(opts: LoggerOptions = {}): LogManager { - const prefix = opts.prefix ?? ""; - const suppressedSet = new Set(opts.suppressTags ?? []); - const tagCounts: Record = {}; - const entries: LogEntry[] = []; - const drySet = new Set(); - const currentLevel: LogLevel = opts.level ?? "INFO"; - - const shouldLog = (level: LogLevel): boolean => LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel]; - - const colorize: Record string> = { - DEBUG: (s) => s, - INFO: (s) => s, - WARN: pc.yellow, - ERROR: pc.red, - SILENT: (s) => s, - }; - - const fmt = (level: LogLevel, icon: string, msg: string, tag?: string) => { - const pfx = prefix ? `${prefix}: ` : ""; - const tagSuffix = tag ? ` ${pc.dim(`(${tag})`)}` : ""; - return colorize[level](`${icon} ${pfx}${msg}`) + tagSuffix; - }; - - const pushEntry = (level: LogLevel, msg: string, tag?: T, suppressed = false) => { - entries.push({ level, tag, message: msg, suppressed, prefix, timestamp: Date.now() }); - }; - - const mkLogFn = ( - level: LogLevel, - icon: string, - consoleFn: (...args: any[]) => void, - dedupe = false, - ): TaggedLogFn => { - return (...args: [string] | [T, string]) => { - const tag = args.length === 2 ? args[0] : undefined; - const msg = args.length === 2 ? args[1] : args[0]; - if (tag) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1; - const isSuppressed = tag !== undefined && suppressedSet.has(tag); - pushEntry(level, msg, tag, isSuppressed); - if (isSuppressed) return; - if (!shouldLog(level)) return; - if (dedupe) { - const key = `${level}::${tag ?? ""}::${msg}`; - if (drySet.has(key)) return; - drySet.add(key); - } - consoleFn(fmt(level, icon, msg, tag)); - }; - }; - - const logger: LogManager = { - warn: mkLogFn("WARN", "!", console.warn), - dryWarn: mkLogFn("WARN", "!", console.warn, true), - info: mkLogFn("INFO", "i", console.log), - error: mkLogFn("ERROR", "X", console.error), - debug: mkLogFn("DEBUG", "D", console.log), - - fork(childPrefix: string, childOpts?: Partial>): LogManager { - const fullPrefix = prefix ? `${prefix}/${childPrefix}` : childPrefix; - const merged = [...suppressedSet, ...(childOpts?.suppressTags ?? [])] as T[]; - return mkLogger({ - prefix: fullPrefix, - suppressTags: merged, - level: childOpts?.level ?? currentLevel, - }); - }, - - as(): LogManager { - return logger as unknown as LogManager; - }, - - tagCounts(): Readonly> { - return tagCounts; - }, - - printTagSummary() { - const allTags = Object.entries(tagCounts); - if (allTags.length === 0) return; - const pfx = prefix ? `${prefix}: ` : ""; - const emitted = allTags.filter(([tag]) => !suppressedSet.has(tag)); - const suppressed = allTags.filter(([tag]) => suppressedSet.has(tag)); - if (emitted.length > 0) { - const total = emitted.reduce((sum, [, c]) => sum + c, 0); - const detail = emitted.map(([tag, c]) => `${tag}: ${c}`).join(", "); - console.warn(pc.yellow(`! ${pfx}${total} warnings (${detail})`)); - } - if (suppressed.length > 0) { - const total = suppressed.reduce((sum, [, c]) => sum + c, 0); - const detail = suppressed.map(([tag, c]) => `${tag}: ${c}`).join(", "); - console.log(pc.dim(`i ${pfx}${total} suppressed (${detail})`)); - } - }, - - buffer(): readonly LogEntry[] { - return entries; - }, - - bufferClear() { - entries.length = 0; - }, - }; - - return logger; -} +import { type Log, type LoggerOptions, type LogManager, mkLogger } from "./common-log"; + +export type { + ExtendLogManager, + Log, + LogEntry, + LoggerOptions, + LogLevel, + LogManager, +} from "./common-log"; +export { mkLogger } from "./common-log"; + +export type CodegenTag = + | "#binding" + | "#largeValueSet" + | "#fieldTypeNotFound" + | "#skipCanonical" + | "#duplicateSchema" + | "#duplicateCanonical" + | "#resolveBase"; + +export type CodegenLog = Log; +export type CodegenLogManager = LogManager; + +export const mkCodegenLogger = (opts: LoggerOptions = {}) => mkLogger(opts); diff --git a/src/utils/types.ts b/src/utils/types.ts index 00aadd60b..d13500452 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,21 +1,5 @@ -import { type Log, type LogManager, type LoggerOptions, mkLogger } from "./log"; - export type CapitalizeFirst = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; export type IsPrefixed = `is${CapitalizeFirst}`; export type PartialBy = Omit & Partial>; - -export type CodegenTag = - | "#binding" - | "#largeValueSet" - | "#fieldTypeNotFound" - | "#skipCanonical" - | "#duplicateSchema" - | "#duplicateCanonical" - | "#resolveBase"; - -export type CodegenLog = Log; -export type CodegenLogManager = LogManager; - -export const mkCodegenLogger = (opts: LoggerOptions = {}) => mkLogger(opts); diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index e9480c069..86ba6c131 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; +import type { CodegenTag } from "@root/utils/log"; import { mkLogger } from "@root/utils/log"; -import type { CodegenTag } from "@root/utils/types"; import { ccdaManager, errorLogger, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index d0f07100e..2615f7815 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,8 +2,8 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; +import type { CodegenLog, CodegenTag } from "@root/utils/log"; import { mkLogger } from "@root/utils/log"; -import type { CodegenLog, CodegenTag } from "@root/utils/types"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; From 30d03c6066a9dfa863c4761cfd962798545e1244 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 14:18:24 +0100 Subject: [PATCH 30/32] Replace shared test loggers with per-test factory functions - Replace module-level errorLogger/silentLogger with mkErrorLogger()/mkSilentLogger() factories - Make logger a required param in registerFsAndMkTs and resolveTs - Each test describe block now creates its own logger via mkTestLogger() --- test/api/mustache.test.ts | 4 +- test/api/write-generator/csharp.test.ts | 4 +- .../api/write-generator/introspection.test.ts | 10 ++--- .../write-generator/multi-package/cda.test.ts | 8 ++-- .../multi-package/local-package.test.ts | 8 ++-- .../multi-package/sql-on-fhir.test.ts | 8 ++-- test/api/write-generator/python.test.ts | 4 +- test/api/write-generator/typescript.test.ts | 11 ++--- test/unit/typeschema/ir/tree-shake.test.ts | 4 +- test/unit/typeschema/r4.test.ts | 9 ++-- test/unit/typeschema/snapshot.test.ts | 25 +++++------ test/unit/typeschema/transformer.test.ts | 41 ++++++++++--------- .../typeschema/transformer/binding.test.ts | 5 ++- test/unit/typeschema/transformer/ccda.test.ts | 9 ++-- .../transformer/choice-types.test.ts | 11 ++--- .../typeschema/transformer/constraint.test.ts | 15 +++---- .../typeschema/transformer/hierarchy.test.ts | 14 ++++--- test/unit/typeschema/transformer/r4.test.ts | 9 ++-- test/unit/typeschema/transformer/r5.test.ts | 7 ++-- test/unit/typeschema/utils.ts | 19 +++++---- 20 files changed, 120 insertions(+), 105 deletions(-) diff --git a/test/api/mustache.test.ts b/test/api/mustache.test.ts index 95ed874c2..7e99626d1 100644 --- a/test/api/mustache.test.ts +++ b/test/api/mustache.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { errorLogger, r4Manager } from "@typeschema-test/utils"; +import { mkErrorLogger, r4Manager } from "@typeschema-test/utils"; describe("Mustache Template Based Generation", async () => { - const report = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const report = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .mustache("./examples/mustache/java", { debug: "COMPACT", inMemoryOnly: true, diff --git a/test/api/write-generator/csharp.test.ts b/test/api/write-generator/csharp.test.ts index 20decb78f..43f6c5236 100644 --- a/test/api/write-generator/csharp.test.ts +++ b/test/api/write-generator/csharp.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { errorLogger, r4Manager } from "@typeschema-test/utils"; +import { mkErrorLogger, r4Manager } from "@typeschema-test/utils"; describe("C# Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .csharp({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/introspection.test.ts b/test/api/write-generator/introspection.test.ts index ed801b0e5..5d180afd5 100644 --- a/test/api/write-generator/introspection.test.ts +++ b/test/api/write-generator/introspection.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { errorLogger, r4Manager } from "@typeschema-test/utils"; +import { mkErrorLogger, r4Manager } from "@typeschema-test/utils"; describe("IntrospectionWriter - Fhir Schema Output", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .introspection({ fhirSchemas: "introspection" }) .introspection({ fhirSchemas: "introspection.ndjson" }) .generate(); @@ -26,7 +26,7 @@ describe("IntrospectionWriter - Fhir Schema Output", async () => { }); describe("IntrospectionWriter - TypeSchema output", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -65,7 +65,7 @@ describe("IntrospectionWriter - TypeSchema output", async () => { }); describe("IntrospectionWriter - typeTree", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { @@ -90,7 +90,7 @@ describe("IntrospectionWriter - typeTree", async () => { }); describe("IntrospectionWriter - StructureDefinition output", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .typeSchema({ treeShake: { "hl7.fhir.r4.core": { diff --git a/test/api/write-generator/multi-package/cda.test.ts b/test/api/write-generator/multi-package/cda.test.ts index d16714385..7f8bb01ce 100644 --- a/test/api/write-generator/multi-package/cda.test.ts +++ b/test/api/write-generator/multi-package/cda.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { silentLogger } from "@typeschema-test/utils"; +import { mkSilentLogger } from "@typeschema-test/utils"; /** * Tests for CDA package generation. @@ -19,7 +19,7 @@ describe("CDA", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -45,7 +45,7 @@ describe("CDA", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -68,7 +68,7 @@ describe("CDA", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/local-package.test.ts b/test/api/write-generator/multi-package/local-package.test.ts index 56bd377e0..24e168245 100644 --- a/test/api/write-generator/multi-package/local-package.test.ts +++ b/test/api/write-generator/multi-package/local-package.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import * as Path from "node:path"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { silentLogger } from "@typeschema-test/utils"; +import { mkSilentLogger } from "@typeschema-test/utils"; const LOCAL_PACKAGE_PATH = Path.join(__dirname, "../../../assets/local-package/structure-definitions"); @@ -30,7 +30,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -61,7 +61,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -97,7 +97,7 @@ describe("Local Package Folder - Multi-Package Generation", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .localStructureDefinitions(localPackageConfig) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/multi-package/sql-on-fhir.test.ts b/test/api/write-generator/multi-package/sql-on-fhir.test.ts index 6a707ddd1..e447a661c 100644 --- a/test/api/write-generator/multi-package/sql-on-fhir.test.ts +++ b/test/api/write-generator/multi-package/sql-on-fhir.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import { silentLogger } from "@typeschema-test/utils"; +import { mkSilentLogger } from "@typeschema-test/utils"; /** * Tests for SQL-on-FHIR package. @@ -21,7 +21,7 @@ describe("SQL-on-FHIR", async () => { }; describe("TypeScript Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig }) .typescript({ inMemoryOnly: true }) @@ -54,7 +54,7 @@ describe("SQL-on-FHIR", async () => { }); describe("Python Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .python({ inMemoryOnly: true }) @@ -84,7 +84,7 @@ describe("SQL-on-FHIR", async () => { }); describe("C# Generation", async () => { - const result = await new APIBuilder({ logger: silentLogger }) + const result = await new APIBuilder({ logger: mkSilentLogger() }) .fromPackageRef(packageUrl) .typeSchema({ treeShake: treeShakeConfig, promoteLogical: promoteLogicalConfig }) .csharp({ inMemoryOnly: true }) diff --git a/test/api/write-generator/python.test.ts b/test/api/write-generator/python.test.ts index 633041688..5aba924d7 100644 --- a/test/api/write-generator/python.test.ts +++ b/test/api/write-generator/python.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; -import { errorLogger, r4Manager } from "@typeschema-test/utils"; +import { mkErrorLogger, r4Manager } from "@typeschema-test/utils"; describe("Python Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .python({ inMemoryOnly: true, }) diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 86ba6c131..1975d23b8 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -1,12 +1,10 @@ import { describe, expect, it } from "bun:test"; import { APIBuilder } from "@root/api/builder"; import type { CanonicalUrl } from "@root/typeschema/types"; -import type { CodegenTag } from "@root/utils/log"; -import { mkLogger } from "@root/utils/log"; -import { ccdaManager, errorLogger, r4Manager } from "@typeschema-test/utils"; +import { ccdaManager, mkErrorLogger, r4Manager } from "@typeschema-test/utils"; describe("TypeScript Writer Generator", async () => { - const result = await new APIBuilder({ register: r4Manager, logger: errorLogger }) + const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() }) .typescript({ inMemoryOnly: true, }) @@ -29,7 +27,7 @@ describe("TypeScript Writer Generator", async () => { }); describe("TypeScript CDA with Logical Model Promotion to Resource", async () => { - const result = await new APIBuilder({ register: ccdaManager, logger: errorLogger }) + const result = await new APIBuilder({ register: ccdaManager, logger: mkErrorLogger() }) .typeSchema({ promoteLogical: { "hl7.cda.uv.core": ["http://hl7.org/cda/stds/core/StructureDefinition/Material" as CanonicalUrl], @@ -51,8 +49,7 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () => }); describe("TypeScript R4 Example (with generateProfile)", async () => { - const logger = mkLogger({ level: "ERROR" }); - + const logger = mkErrorLogger(); const result = await new APIBuilder({ register: r4Manager, logger }) .typescript({ inMemoryOnly: true, diff --git a/test/unit/typeschema/ir/tree-shake.test.ts b/test/unit/typeschema/ir/tree-shake.test.ts index 940d3ea0b..60c6b8b64 100644 --- a/test/unit/typeschema/ir/tree-shake.test.ts +++ b/test/unit/typeschema/ir/tree-shake.test.ts @@ -8,7 +8,7 @@ import { } from "@root/typeschema/ir/tree-shake"; import { registerFromPackageMetas } from "@root/typeschema/register"; import type { CanonicalUrl, RegularTypeSchema } from "@root/typeschema/types"; -import { mkIndex, mkR4Register, r4Package, r5Package, resolveTs } from "@typeschema-test/utils"; +import { mkIndex, mkR4Register, mkTestLogger, r4Package, r5Package, resolveTs } from "@typeschema-test/utils"; describe("treeShake specific TypeSchema", async () => { const manager = await registerFromPackageMetas([r4Package, r5Package], {}); @@ -55,10 +55,12 @@ describe("treeShake specific TypeSchema", async () => { describe("treeShake specific TypeSchema", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); const patientTss = await resolveTs( r4, r4Package, "http://hl7.org/fhir/StructureDefinition/Patient" as CanonicalUrl, + logger, ); const patientOrigin = patientTss[0] as RegularTypeSchema; assert(patientOrigin !== undefined); diff --git a/test/unit/typeschema/r4.test.ts b/test/unit/typeschema/r4.test.ts index ccedd05bd..fd7c8f133 100644 --- a/test/unit/typeschema/r4.test.ts +++ b/test/unit/typeschema/r4.test.ts @@ -1,17 +1,18 @@ import { describe, expect, it } from "bun:test"; import { generateTypeSchemas } from "@root/typeschema"; import type { CanonicalUrl, Name, RegularTypeSchema } from "@root/typeschema/types"; -import { mkR4Register, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema R4 generation", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); it("Bundle and elementReference", async () => { const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl); if (!profile) { throw new Error("Bundle profile not found"); } - const ts = (await registerFsAndMkTs(r4, profile))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, profile, logger))[0] as RegularTypeSchema; expect(ts?.nested).toHaveLength(5); expect(ts).toMatchObject({ identifier: { kind: "resource", url: "http://hl7.org/fhir/StructureDefinition/Bundle" }, @@ -54,7 +55,7 @@ describe("TypeSchema R4 generation", async () => { if (!md) { throw new Error("markdown type not found"); } - const ts = (await registerFsAndMkTs(r4, md))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, md, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { kind: "primitive-type", @@ -77,7 +78,7 @@ describe("TypeSchema R4 generation", async () => { if (!parameters) { throw new Error("Parameters resource not found"); } - const ts = (await registerFsAndMkTs(r4, parameters))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, parameters, logger))[0] as RegularTypeSchema; expect(ts.dependencies).toBeDefined(); expect(ts.dependencies).toContainEqual({ kind: "primitive-type", diff --git a/test/unit/typeschema/snapshot.test.ts b/test/unit/typeschema/snapshot.test.ts index 4e3909af9..fdd8c5d4c 100644 --- a/test/unit/typeschema/snapshot.test.ts +++ b/test/unit/typeschema/snapshot.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from "bun:test"; import type { PFS, PVS } from "@typeschema-test/utils"; -import { mkR4Register, r4Package, registerFsAndMkTs, transformVS } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, r4Package, registerFsAndMkTs, transformVS } from "@typeschema-test/utils"; const r4 = await mkR4Register(); +const logger = mkTestLogger(); describe("ValueSet to Type Schema (snapshot)", async () => { it("administrative-gender", async () => { @@ -27,56 +28,56 @@ describe("ValueSet to Type Schema (snapshot)", async () => { describe("FHIR Schema to Type Schema (snapshot)", async () => { it("with cardinality", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/with-cardinality.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with string", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-string.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with code", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-code.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with codable concept", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-codable-concept.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with choice", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-choice.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with nested type", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-nested-type.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("with resource with nested type 2", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/resource-with-nested-type-2.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); describe("Real world examples", async () => { it("coding primitive type", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/coding.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("string primitive type", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/string.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); }); @@ -84,12 +85,12 @@ describe("FHIR Schema to Type Schema (snapshot)", async () => { describe("Custom resource", async () => { it("TutorNotificationTemplate", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/TutorNotificationTemplate.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); it("TutorNotification", async () => { const fs: PFS = await Bun.file("test/assets/fhir-schemas/TutorNotification.fs.json").json(); - const ts = await registerFsAndMkTs(r4, fs); + const ts = await registerFsAndMkTs(r4, fs, logger); expect(JSON.stringify(ts, null, 2)).toMatchSnapshot(); }); }); diff --git a/test/unit/typeschema/transformer.test.ts b/test/unit/typeschema/transformer.test.ts index 956c846a8..8551200a4 100644 --- a/test/unit/typeschema/transformer.test.ts +++ b/test/unit/typeschema/transformer.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from "bun:test"; import type { Name, PackageMeta, RegularField, RegularTypeSchema } from "@typeschema/types"; import type { PFS } from "@typeschema-test/utils"; -import { mkR4Register, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema Transformer Core Logic", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); const _basePackageInfo: PackageMeta = { name: "test.package", @@ -26,7 +27,7 @@ describe("TypeSchema Transformer Core Logic", async () => { class: "", }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); expect(result).toHaveLength(1); expect(result[0]?.identifier.name).toBe("TestResource" as Name); @@ -44,7 +45,7 @@ describe("TypeSchema Transformer Core Logic", async () => { class: "", }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -61,7 +62,7 @@ describe("TypeSchema Transformer Core Logic", async () => { class: "", }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); expect(result).toHaveLength(1); expect(result[0]?.identifier.kind).toBe("primitive-type"); @@ -81,7 +82,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -102,7 +103,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -127,7 +128,7 @@ describe("TypeSchema Transformer Core Logic", async () => { class: "", }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -147,7 +148,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -169,7 +170,7 @@ describe("TypeSchema Transformer Core Logic", async () => { elements: {}, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); expect(result).toHaveLength(1); expect(result[0]?.identifier.kind).toBe("profile"); @@ -187,7 +188,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -208,7 +209,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -230,7 +231,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -254,7 +255,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result.length).toBeGreaterThanOrEqual(1); @@ -275,7 +276,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -290,7 +291,7 @@ describe("TypeSchema Transformer Core Logic", async () => { url: "http://example.org/EmptyResource", }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -312,7 +313,7 @@ describe("TypeSchema Transformer Core Logic", async () => { package_meta: customPackageInfo, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); expect(result).toHaveLength(1); expect(result[0]?.identifier.package).toBe("custom.package"); @@ -331,7 +332,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); @@ -356,7 +357,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; // Binding schemas are also generated @@ -375,7 +376,7 @@ describe("TypeSchema Transformer Core Logic", async () => { elements: {}, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); expect(result).toHaveLength(1); // Extension detection may vary based on URL pattern @@ -403,7 +404,7 @@ describe("TypeSchema Transformer Core Logic", async () => { }, }; - const result = await registerFsAndMkTs(r4, fhirSchema); + const result = await registerFsAndMkTs(r4, fhirSchema, logger); const schema = result[0] as RegularTypeSchema; expect(result).toHaveLength(1); diff --git a/test/unit/typeschema/transformer/binding.test.ts b/test/unit/typeschema/transformer/binding.test.ts index 0fa9700dc..aa779448c 100644 --- a/test/unit/typeschema/transformer/binding.test.ts +++ b/test/unit/typeschema/transformer/binding.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; import type { PFS } from "@typeschema-test/utils"; -import { mkR4Register, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, registerFsAndMkTs } from "@typeschema-test/utils"; describe("Type Schema generator > Binding", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); const A: PFS = { description: "description", derivation: "specialization", @@ -29,7 +30,7 @@ describe("Type Schema generator > Binding", async () => { version: "4.0.1", }; it("Generate nested type for resource", async () => { - expect(await registerFsAndMkTs(r4, A)).toMatchObject([ + expect(await registerFsAndMkTs(r4, A, logger)).toMatchObject([ { identifier: { package: "mypackage", url: "http://example.io/fhir/WithCode" }, base: { package: "hl7.fhir.r4.core", url: "http://hl7.org/fhir/StructureDefinition/DomainResource" }, diff --git a/test/unit/typeschema/transformer/ccda.test.ts b/test/unit/typeschema/transformer/ccda.test.ts index 5a240e5db..dd2f4f65c 100644 --- a/test/unit/typeschema/transformer/ccda.test.ts +++ b/test/unit/typeschema/transformer/ccda.test.ts @@ -1,11 +1,12 @@ import { describe, expect, it } from "bun:test"; import type { CanonicalUrl, RegularTypeSchema } from "@root/typeschema/types"; -import { ccdaPackage, mkCCDARegister, registerFsAndMkTs } from "@typeschema-test/utils"; +import { ccdaPackage, mkCCDARegister, mkTestLogger, registerFsAndMkTs } from "@typeschema-test/utils"; const skipMe = false; describe("TypeSchema CCDA generation", async () => { const ccda = await mkCCDARegister(); + const logger = mkTestLogger(); it.skipIf(skipMe)("http://hl7.org/fhir/StructureDefinition/workflow-protectiveFactor", async () => { const resource = ccda.resolveFs( @@ -15,7 +16,7 @@ describe("TypeSchema CCDA generation", async () => { if (!resource) { throw new Error("workflow-protectiveFactor not found"); } - const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(ccda, resource, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { kind: "profile", @@ -74,7 +75,7 @@ describe("TypeSchema CCDA generation", async () => { if (!resource) { throw new Error("ON StructureDefinition not found"); } - const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(ccda, resource, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { kind: "logical", @@ -166,7 +167,7 @@ describe("TypeSchema CCDA generation", async () => { if (!resource) { throw new Error("ehrsrle-auditevent not found"); } - const ts = (await registerFsAndMkTs(ccda, resource))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(ccda, resource, logger))[0] as RegularTypeSchema; // console.log(JSON.stringify(ts, null, 2)); // NOTE: problem: canonical manager recomend us to use R5, but we failing on R4 AuditEvent. expect(ts).toMatchObject({ diff --git a/test/unit/typeschema/transformer/choice-types.test.ts b/test/unit/typeschema/transformer/choice-types.test.ts index af7539bf7..a984c4d17 100644 --- a/test/unit/typeschema/transformer/choice-types.test.ts +++ b/test/unit/typeschema/transformer/choice-types.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; import type { PFS } from "@typeschema-test/utils"; -import { mkR4Register, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema choice type generation", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); it("Simple choice type generation (optional)", async () => { const fs: PFS = { @@ -15,7 +16,7 @@ describe("TypeSchema choice type generation", async () => { deceasedBoolean: { type: "boolean" }, }, }; - expect(await registerFsAndMkTs(r4, fs)).toMatchObject([ + expect(await registerFsAndMkTs(r4, fs, logger)).toMatchObject([ { identifier: { kind: "resource", url: "uri::OptionalChoice" }, fields: { @@ -54,7 +55,7 @@ describe("TypeSchema choice type generation", async () => { deceasedBoolean: { choiceOf: "deceased", type: "boolean" }, }, }; - expect(await registerFsAndMkTs(r4, fs)).toMatchObject([ + expect(await registerFsAndMkTs(r4, fs, logger)).toMatchObject([ { identifier: { url: "uri::RequiredChoice" }, fields: { @@ -97,7 +98,7 @@ describe("TypeSchema choice type generation", async () => { }, }; - expect(await registerFsAndMkTs(r4, fs)).toMatchObject([ + expect(await registerFsAndMkTs(r4, fs, logger)).toMatchObject([ { identifier: { kind: "resource", url: "uri::RequiredChoiceLimited" }, base: { url: "uri::RequiredChoice" }, @@ -132,7 +133,7 @@ describe("TypeSchema choice type generation", async () => { }, }; - expect(await registerFsAndMkTs(r4, fs)).toMatchObject([ + expect(await registerFsAndMkTs(r4, fs, logger)).toMatchObject([ { identifier: { kind: "resource", url: "uri::RequiredChoiceLimited" }, base: { url: "uri::RequiredChoice" }, diff --git a/test/unit/typeschema/transformer/constraint.test.ts b/test/unit/typeschema/transformer/constraint.test.ts index 79584c7d5..72bb6f37a 100644 --- a/test/unit/typeschema/transformer/constraint.test.ts +++ b/test/unit/typeschema/transformer/constraint.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; import type { CanonicalUrl } from "@typeschema/types"; -import { mkR4Register, type PFS, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, type PFS, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema Processing constraint generation", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); const A: PFS = { url: "uri::A", derivation: "specialization", @@ -18,7 +19,7 @@ describe("TypeSchema Processing constraint generation", async () => { }, }; it("Generate nested type for resource", async () => { - expect(await registerFsAndMkTs(r4, A)).toMatchObject([ + expect(await registerFsAndMkTs(r4, A, logger)).toMatchObject([ { identifier: { kind: "resource", name: "a", url: "uri::A" }, fields: { @@ -47,7 +48,7 @@ describe("TypeSchema Processing constraint generation", async () => { elements: { foo: { min: 1 } }, }; it("Constraint nested type for resource in profile", async () => { - expect(await registerFsAndMkTs(r4, B)).toMatchObject([ + expect(await registerFsAndMkTs(r4, B, logger)).toMatchObject([ { identifier: { kind: "profile", name: "b", url: "uri::B" }, base: { kind: "resource", name: "a", url: "uri::A" }, @@ -72,7 +73,7 @@ describe("TypeSchema Processing constraint generation", async () => { }; it("Constraint nested type for resource in profile", async () => { - expect(await registerFsAndMkTs(r4, C)).toMatchObject([ + expect(await registerFsAndMkTs(r4, C, logger)).toMatchObject([ { identifier: { kind: "profile", name: "c", url: "uri::C" }, base: { kind: "profile", name: "b", url: "uri::B" }, @@ -117,8 +118,8 @@ describe("TypeSchema Processing constraint generation", async () => { }, }; it("Constraint profile nested type includes all inherited sub-elements", async () => { - await registerFsAndMkTs(r4, D); - expect(await registerFsAndMkTs(r4, E)).toMatchObject([ + await registerFsAndMkTs(r4, D, logger); + expect(await registerFsAndMkTs(r4, E, logger)).toMatchObject([ { identifier: { kind: "profile", name: "e", url: "uri::E" }, base: { kind: "resource", name: "d", url: "uri::D" }, @@ -151,7 +152,7 @@ describe("TypeSchema Processing constraint generation", async () => { if (!profile) { throw new Error("shareablecodesystem profile not found"); } - expect(await registerFsAndMkTs(r4, profile)).toMatchObject([ + expect(await registerFsAndMkTs(r4, profile, logger)).toMatchObject([ { base: { kind: "resource", url: "http://hl7.org/fhir/StructureDefinition/CodeSystem" }, identifier: { kind: "profile", url: "http://hl7.org/fhir/StructureDefinition/shareablecodesystem" }, diff --git a/test/unit/typeschema/transformer/hierarchy.test.ts b/test/unit/typeschema/transformer/hierarchy.test.ts index 1d6fd112c..10283ba24 100644 --- a/test/unit/typeschema/transformer/hierarchy.test.ts +++ b/test/unit/typeschema/transformer/hierarchy.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; import type { PFS } from "@typeschema-test/utils"; -import { mkR4Register, mkR5Register, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkR5Register, mkTestLogger, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema: Nested types", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); describe("A with array field", () => { const A: PFS = { url: "uri::A", @@ -13,7 +14,7 @@ describe("TypeSchema: Nested types", async () => { }, }; it("Base", async () => { - expect(await registerFsAndMkTs(r4, A)).toMatchObject([ + expect(await registerFsAndMkTs(r4, A, logger)).toMatchObject([ { identifier: { url: "uri::A" }, fields: { @@ -42,7 +43,7 @@ describe("TypeSchema: Nested types", async () => { }, }; - expect(await registerFsAndMkTs(r4, B)).toMatchObject([ + expect(await registerFsAndMkTs(r4, B, logger)).toMatchObject([ { identifier: { url: "uri::B" }, base: { url: "uri::A" }, @@ -89,8 +90,8 @@ describe("TypeSchema: Nested types", async () => { }; it("Check optional choice fields", async () => { // Register B first since C depends on it - await registerFsAndMkTs(r4, B); - expect(await registerFsAndMkTs(r4, C)).toMatchObject([ + await registerFsAndMkTs(r4, B, logger); + expect(await registerFsAndMkTs(r4, C, logger)).toMatchObject([ { identifier: { url: "uri::C" }, base: { url: "uri::B" }, @@ -160,8 +161,9 @@ const viewDefinitionSD = { describe("TypeSchema: Nested types", async () => { const r5 = await mkR5Register(); + const logger = mkTestLogger(); it("Check recursive nested types", async () => { - const tss = await registerFsAndMkTs(r5, viewDefinitionSD); + const tss = await registerFsAndMkTs(r5, viewDefinitionSD, logger); expect(tss).toMatchObject([ { nested: [ diff --git a/test/unit/typeschema/transformer/r4.test.ts b/test/unit/typeschema/transformer/r4.test.ts index cff2c5051..32fde5e1d 100644 --- a/test/unit/typeschema/transformer/r4.test.ts +++ b/test/unit/typeschema/transformer/r4.test.ts @@ -1,17 +1,18 @@ import { describe, expect, it } from "bun:test"; import { generateTypeSchemas } from "@root/typeschema"; import type { CanonicalUrl, Name, RegularTypeSchema } from "@typeschema/types"; -import { mkR4Register, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR4Register, mkTestLogger, r4Package, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema R4 generation", async () => { const r4 = await mkR4Register(); + const logger = mkTestLogger(); it("Bundle and elementReference", async () => { const profile = r4.resolveFs(r4Package, "http://hl7.org/fhir/StructureDefinition/Bundle" as CanonicalUrl); if (!profile) { throw new Error("Bundle profile not found"); } - const ts = (await registerFsAndMkTs(r4, profile))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, profile, logger))[0] as RegularTypeSchema; expect(ts?.nested).toHaveLength(5); expect(ts).toMatchObject({ identifier: { kind: "resource", url: "http://hl7.org/fhir/StructureDefinition/Bundle" }, @@ -54,7 +55,7 @@ describe("TypeSchema R4 generation", async () => { if (!md) { throw new Error("markdown type not found"); } - const ts = (await registerFsAndMkTs(r4, md))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, md, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { kind: "primitive-type", @@ -77,7 +78,7 @@ describe("TypeSchema R4 generation", async () => { if (!parameters) { throw new Error("Parameters resource not found"); } - const ts = (await registerFsAndMkTs(r4, parameters))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r4, parameters, logger))[0] as RegularTypeSchema; expect(ts.dependencies).toBeDefined(); expect(ts.dependencies).toContainEqual({ kind: "primitive-type", diff --git a/test/unit/typeschema/transformer/r5.test.ts b/test/unit/typeschema/transformer/r5.test.ts index 53236fb2d..211fa6eb0 100644 --- a/test/unit/typeschema/transformer/r5.test.ts +++ b/test/unit/typeschema/transformer/r5.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; import type { CanonicalUrl, RegularTypeSchema } from "@root/typeschema/types"; -import { mkR5Register, r5Package, registerFsAndMkTs } from "@typeschema-test/utils"; +import { mkR5Register, mkTestLogger, r5Package, registerFsAndMkTs } from "@typeschema-test/utils"; describe("TypeSchema R5 generation", async () => { const r5 = await mkR5Register(); + const logger = mkTestLogger(); it("http://hl7.org/fhir/StructureDefinition/shareablecodesystem", async () => { const fs = r5.resolveFs( @@ -12,7 +13,7 @@ describe("TypeSchema R5 generation", async () => { ); expect(fs).toBeDefined(); if (!fs) throw new Error("fs is undefined"); - const ts = (await registerFsAndMkTs(r5, fs))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r5, fs, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { @@ -76,7 +77,7 @@ describe("TypeSchema R5 generation", async () => { const fs = r5.resolveFs(r5Package, "http://hl7.org/fhir/StructureDefinition/Extension" as CanonicalUrl); expect(fs).toBeDefined(); if (!fs) throw new Error("Failed to resolve fs"); - const ts = (await registerFsAndMkTs(r5, fs))[0] as RegularTypeSchema; + const ts = (await registerFsAndMkTs(r5, fs, logger))[0] as RegularTypeSchema; expect(ts).toMatchObject({ identifier: { kind: "complex-type", diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index 2615f7815..f6f00ea2a 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -2,8 +2,8 @@ import type { FHIRSchema } from "@atomic-ehr/fhirschema"; import type { ValueSet } from "@root/fhir-types/hl7-fhir-r4-core"; import { generateTypeSchemas } from "@root/typeschema"; import { mkTypeSchemaIndex } from "@root/typeschema/utils"; -import type { CodegenLog, CodegenTag } from "@root/utils/log"; -import { mkLogger } from "@root/utils/log"; +import type { CodegenLog } from "@root/utils/log"; +import { mkCodegenLogger } from "@root/utils/log"; import { transformFhirSchema, transformValueSet } from "@typeschema/core/transformer"; import { type Register, registerFromPackageMetas } from "@typeschema/register"; import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } from "@typeschema/types"; @@ -11,9 +11,9 @@ import { type CanonicalUrl, enrichFHIRSchema, enrichValueSet, type PackageMeta } export type PFS = Partial; export type PVS = Partial; -const logger = mkLogger({ prefix: "TEST" }); -export const errorLogger = mkLogger({ level: "ERROR" }); -export const silentLogger = mkLogger({ level: "SILENT" }); +export const mkTestLogger = () => mkCodegenLogger({ prefix: "TEST" }); +export const mkErrorLogger = () => mkCodegenLogger({ level: "ERROR" }); +export const mkSilentLogger = () => mkCodegenLogger({ level: "SILENT" }); export const mkIndex = async (register: Register, logger?: CodegenLog) => { const { schemas } = await generateTypeSchemas(register, logger); @@ -52,13 +52,18 @@ export const registerFs = (register: Register, fs: PFS) => { return rfs; }; -export const resolveTs = async (register: Register, pkgMeta: PackageMeta, url: string | CanonicalUrl) => { +export const resolveTs = async ( + register: Register, + pkgMeta: PackageMeta, + url: string | CanonicalUrl, + logger: CodegenLog, +) => { const rfs = register.resolveFs(pkgMeta, url as CanonicalUrl); if (!rfs) throw new Error("Failed to resolve registered FHIR schema"); return await transformFhirSchema(register, rfs, logger); }; -export const registerFsAndMkTs = async (register: Register, fs: PFS) => { +export const registerFsAndMkTs = async (register: Register, fs: PFS, logger: CodegenLog) => { const rfs = registerFs(register, fs); return await transformFhirSchema(register, rfs, logger); }; From f0f5c46ad108f9c154aa2034a4c14fa2939a2525 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 9 Mar 2026 14:59:47 +0100 Subject: [PATCH 31/32] Fix logger type comment in writer-generator guide --- docs/guides/writer-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/writer-generator.md b/docs/guides/writer-generator.md index 2e683298b..82e89d5ae 100644 --- a/docs/guides/writer-generator.md +++ b/docs/guides/writer-generator.md @@ -77,7 +77,7 @@ The `FileSystemWriter` class handles file I/O and directory management. It requi export type FileSystemWriterOptions = { outputDir: string; // Where to write files inMemoryOnly?: boolean; // Don't write to disk (for testing) - logger?: CodegenLogManager; // Optional logging + logger?: CodegenLogManager; // Created automatically if not provided resolveAssets?: (fn: string) => string; // Asset resolution function }; ``` From 096d0c9341775b1185f042c2df17670d99075395 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Tue, 10 Mar 2026 15:59:22 +0100 Subject: [PATCH 32/32] Fix typo in duplicate schema warning, pass logger to enrichResolver --- src/typeschema/index.ts | 2 +- src/typeschema/register.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typeschema/index.ts b/src/typeschema/index.ts index a4d9d1f39..3f4d78161 100644 --- a/src/typeschema/index.ts +++ b/src/typeschema/index.ts @@ -59,7 +59,7 @@ const deduplicateSchemas = (schemasWithSources: SchemaWithSource[], logger?: Cod if (sorted.length > 1) { const pkg = best.typeSchema.identifier.package; const url = best.typeSchema.identifier.url; - logger?.dryWarn("#duplicateSchema", `'${url}' from '${pkg}'' has ${sorted.length} versions`); + logger?.dryWarn("#duplicateSchema", `'${url}' from '${pkg}' has ${sorted.length} versions`); collisions[pkg] ??= {}; collisions[pkg][url] = sorted.flatMap((v) => v.sources.map((s) => ({ diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index 39e8f3f19..141dfccac 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -175,7 +175,7 @@ export const registerFromManager = async ( for (const pkg of packages) { await mkPackageAwareResolver(manager, pkg, 0, resolver, logger); } - enrichResolver(resolver); + enrichResolver(resolver, logger); const resolveFs = (pkg: PackageMeta, canonicalUrl: CanonicalUrl) => { const pkgIndex = resolver[packageMetaToFhir(pkg)];