babelfhir-ts 1.0.28 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
2
- import { sanitizeIdentifier } from './utils.js';
2
+ import { sanitizeIdentifier, toPascalCase } from './utils.js';
3
+ import { logger } from "../logger.js";
3
4
  /**
4
5
  * Manages TypeScript import statements for generated FHIR interfaces.
5
6
  * Handles three types of imports:
@@ -15,9 +16,17 @@ export class ImportManager {
15
16
  localInterfaceNames;
16
17
  existingProfiles;
17
18
  externalProfiles = new Map();
18
- constructor(localInterfaceNames, existingProfiles) {
19
+ profileIdToName; // Map from profile ID to actual interface name
20
+ profileUrlToName; // Map from profile URL to actual interface name
21
+ constructor(localInterfaceNames, existingProfiles, profileIdToName, profileUrlToName) {
19
22
  this.localInterfaceNames = localInterfaceNames;
20
23
  this.existingProfiles = existingProfiles;
24
+ this.profileIdToName = profileIdToName;
25
+ this.profileUrlToName = profileUrlToName;
26
+ logger.log(`[ImportManager] Initialized with profileUrlToName entries: ${profileUrlToName?.size || 0}`);
27
+ if (profileUrlToName && profileUrlToName.size > 0) {
28
+ logger.log(`[ImportManager] Sample URLs in map:`, Array.from(profileUrlToName.entries()).slice(0, 3));
29
+ }
21
30
  }
22
31
  /**
23
32
  * Adds a type to the appropriate import category.
@@ -30,9 +39,13 @@ export class ImportManager {
30
39
  addType(typeName, profileUrl, baseType, isFhirBase = false) {
31
40
  if (!typeName)
32
41
  return;
42
+ // If we have a profile URL, check if we know the actual type name for it
43
+ if (profileUrl && this.profileUrlToName?.has(profileUrl)) {
44
+ const actualTypeName = this.profileUrlToName.get(profileUrl);
45
+ typeName = actualTypeName;
46
+ }
33
47
  // Check if it's a nested interface of the current profile (defined locally in this file)
34
48
  if (this.localInterfaceNames.has(typeName)) {
35
- // Don't add to imports - defined in the same file
36
49
  return;
37
50
  }
38
51
  // Check if it's a known FHIR type from @types/fhir
@@ -40,17 +53,33 @@ export class ImportManager {
40
53
  this.fhirImports.add(typeName);
41
54
  return;
42
55
  }
56
+ // Normalize to PascalCase for consistency (all generated interfaces use PascalCase)
57
+ const normalizedTypeName = toPascalCase(sanitizeIdentifier(typeName));
43
58
  // Check if it exists in our generated profiles/types
44
59
  if (this.existingProfiles?.has(typeName)) {
45
- this.customImports.add(typeName);
60
+ this.customImports.add(normalizedTypeName);
61
+ return;
62
+ }
63
+ else if (this.existingProfiles?.has(normalizedTypeName)) {
64
+ this.customImports.add(normalizedTypeName);
46
65
  return;
47
66
  }
48
67
  // Check if this is a primitive type that shouldn't be fetched
49
68
  if (this.isPrimitiveLikeType(typeName)) {
50
69
  return;
51
70
  }
52
- // Add to custom imports (will be generated or fetched)
53
- this.customImports.add(typeName);
71
+ // Check if we already have an external profile registered with this URL
72
+ // If so, use the existing name to avoid duplicates
73
+ if (profileUrl) {
74
+ for (const [existingName, existingMeta] of this.externalProfiles) {
75
+ if (existingMeta.profileUrl === profileUrl) {
76
+ const existingNormalized = toPascalCase(sanitizeIdentifier(existingName));
77
+ this.customImports.add(existingNormalized);
78
+ return;
79
+ }
80
+ }
81
+ }
82
+ this.customImports.add(normalizedTypeName);
54
83
  // Track as external profile if we have profile metadata
55
84
  if (profileUrl || baseType) {
56
85
  this.registerExternalProfile(typeName, profileUrl, baseType, isFhirBase);
@@ -62,14 +91,25 @@ export class ImportManager {
62
91
  registerExternalProfile(rawTypeName, profileUrl, baseType, isFhirBase = false) {
63
92
  if (!rawTypeName)
64
93
  return;
65
- const sanitizedTypeName = sanitizeIdentifier(rawTypeName);
94
+ // Normalize to PascalCase since that's what we use for interface names
95
+ const normalizedTypeName = toPascalCase(sanitizeIdentifier(rawTypeName));
66
96
  const resolvedProfileUrl = profileUrl && profileUrl.length > 0
67
97
  ? profileUrl
68
- : `http://hl7.org/fhir/StructureDefinition/${sanitizedTypeName}`;
69
- let meta = this.externalProfiles.get(sanitizedTypeName);
98
+ : `http://hl7.org/fhir/StructureDefinition/${normalizedTypeName}`;
99
+ // Check if we already have an external profile with the same URL
100
+ // If so, skip to avoid duplicate registrations
101
+ if (resolvedProfileUrl) {
102
+ for (const [existingName, existingMeta] of this.externalProfiles) {
103
+ if (existingMeta.profileUrl === resolvedProfileUrl) {
104
+ logger.log(`[importManager.registerExternalProfile] SKIP duplicate URL: ${normalizedTypeName} -> ${resolvedProfileUrl} (already registered as ${existingName})`);
105
+ return;
106
+ }
107
+ }
108
+ }
109
+ let meta = this.externalProfiles.get(normalizedTypeName);
70
110
  if (!meta) {
71
111
  meta = { profileUrl: resolvedProfileUrl, baseTypes: new Map() };
72
- this.externalProfiles.set(sanitizedTypeName, meta);
112
+ this.externalProfiles.set(normalizedTypeName, meta);
73
113
  }
74
114
  else if (profileUrl && profileUrl.length > 0 &&
75
115
  meta.profileUrl.startsWith('http://hl7.org/fhir/StructureDefinition/')) {
@@ -109,10 +149,22 @@ export class ImportManager {
109
149
  for (const [typeName, relativePath] of sortedPathBasedImports) {
110
150
  statements.push(`import { ${typeName} } from "${relativePath}";`);
111
151
  }
112
- // Custom local imports
152
+ // Custom local imports - deduplicate to avoid duplicate import lines
113
153
  const sortedCustomImports = Array.from(this.customImports).sort();
154
+ logger.log(`[ImportManager.generateImportStatements] customImports size: ${this.customImports.size}`);
155
+ logger.log(`[ImportManager.generateImportStatements] customImports contents:`, sortedCustomImports.slice(0, 20));
156
+ const seenTypes = new Set();
114
157
  for (const type of sortedCustomImports) {
115
- statements.push(`import { ${type} } from "./${type}";`);
158
+ // Check if this type has been remapped (e.g., "Aufnahmegrund" -> "ExtensionAufnahmegrund")
159
+ const actualTypeName = this.profileIdToName?.get(type) || type;
160
+ // Skip if we've already added an import for this actual type name
161
+ if (seenTypes.has(actualTypeName)) {
162
+ logger.log(`[ImportManager.generateImportStatements] SKIPPING duplicate: ${type} -> ${actualTypeName}`);
163
+ continue;
164
+ }
165
+ logger.log(`[ImportManager.generateImportStatements] Adding import: ${type} -> ${actualTypeName}`);
166
+ statements.push(`import { ${actualTypeName} } from "./${actualTypeName}";`);
167
+ seenTypes.add(actualTypeName);
116
168
  }
117
169
  return statements.join('\n');
118
170
  }
@@ -12,6 +12,30 @@ import { generateValueSetTypeScript, generateValueSetRegistry } from './valueSet
12
12
  import { logger } from '../logger.js';
13
13
  import fhirInterfaceNames from './fhirInterfaces.json' with { type: 'json' };
14
14
  import { spawn } from 'child_process';
15
+ // Track failed fetches to provide helpful warnings
16
+ let failedFetchCount = 0;
17
+ const failedFetchProfiles = [];
18
+ export function resetFetchFailureTracking() {
19
+ failedFetchCount = 0;
20
+ failedFetchProfiles.length = 0;
21
+ }
22
+ export function getFetchFailureCount() {
23
+ return failedFetchCount;
24
+ }
25
+ export function getFetchFailureWarning() {
26
+ if (failedFetchCount === 0)
27
+ return null;
28
+ const profileList = failedFetchProfiles.slice(0, 5).map(p => ` - ${p}`).join('\n');
29
+ const moreMessage = failedFetchProfiles.length > 5 ? `\n ... and ${failedFetchProfiles.length - 5} more` : '';
30
+ return `\n⚠️ Warning: ${failedFetchCount} external profiles could not be fetched (timeout or network error):\n${profileList}${moreMessage}\n\n` +
31
+ `These profiles were generated as Element type aliases, which may cause TypeScript errors.\n` +
32
+ `This usually happens when external FHIR servers are slow or unavailable.\n\n` +
33
+ `Recommended actions:\n` +
34
+ ` 1. Run the generation command again (cached profiles will be reused, only missing ones will be retried)\n` +
35
+ ` 2. Check your internet connection\n` +
36
+ ` 3. If the issue persists, the external server may be temporarily down\n\n` +
37
+ `Note: The .cache folder has been preserved to speed up the next generation attempt.\n`;
38
+ }
15
39
  /** Ensure RandomSupport.ts exists in a generation output directory */
16
40
  function ensureRandomSupportFile(dir) {
17
41
  const filePath = path.join(dir, 'RandomSupport.ts');
@@ -50,7 +74,7 @@ export function skeletonQuantity(): Quantity { return { value: randomInt(1,100),
50
74
  }
51
75
  catch { /* ignore read errors, will rewrite */ }
52
76
  fs.writeFileSync(filePath, content, 'utf-8');
53
- console.log(`Updated RandomSupport.ts in ${dir}`);
77
+ logger.log(`Updated RandomSupport.ts in ${dir}`);
54
78
  }
55
79
  else {
56
80
  fs.writeFileSync(filePath, content, 'utf-8');
@@ -178,9 +202,14 @@ async function generateIndexFile(dir) {
178
202
  }
179
203
  // Reusable processor for a single StructureDefinition
180
204
  async function processStructureDefinition(sd, ctx) {
181
- const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, flags, fhirChildTypeMap } = ctx;
205
+ const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap } = ctx;
182
206
  const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
183
- const interfaceName = toPascalCase(rawInterfaceName);
207
+ let interfaceName = toPascalCase(rawInterfaceName);
208
+ // Check if the interface name conflicts with a FHIR base type
209
+ if (fhirInterfaceNames.includes(interfaceName)) {
210
+ logger.log(`Interface name '${interfaceName}' conflicts with FHIR base type, adding 'Extension' suffix`);
211
+ interfaceName = `${interfaceName}Extension`;
212
+ }
184
213
  let baseResource = sanitizeIdentifier(getBaseResource(sd.baseDefinition));
185
214
  // Check if baseResource is a profile ID that maps to a generated interface name
186
215
  if (profileIdToName?.has(baseResource)) {
@@ -194,16 +223,22 @@ async function processStructureDefinition(sd, ctx) {
194
223
  const parsed = await parseStructureDefinition(sd, fhirSourceHint, valueSetCodesMap);
195
224
  if (!parsed.newFields.length) {
196
225
  logger.log(`No new SD fields for ${interfaceName}, skipping.`);
197
- return;
226
+ return interfaceName;
198
227
  }
199
228
  const aligned = alignFields(parsed.newFields, parsed.oldFields);
200
- const interfaceResult = generateInterfaces(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets, sd.type, existingStructureDefinitions, fhirChildTypeMap);
229
+ const interfaceResult = generateInterfaces(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets, sd.type, existingStructureDefinitions, fhirChildTypeMap, profileIdToName, profileUrlToName);
201
230
  const interfaceContent = interfaceResult.content;
231
+ // Debug: Log external profiles
232
+ if (interfaceResult.referencedExternalProfiles.size > 0) {
233
+ logger.log(`[index] External profiles for ${interfaceName}:`, Array.from(interfaceResult.referencedExternalProfiles.keys()));
234
+ }
202
235
  // Generate type alias files for external profiled identifiers that don't exist as StructureDefinitions
203
236
  for (const [typeName, profileInfo] of interfaceResult.referencedExternalProfiles) {
237
+ // Normalize to PascalCase since that's what we use for interface names
238
+ const normalizedTypeName = toPascalCase(sanitizeIdentifier(typeName));
204
239
  // Only emit an external alias if this interface actually imports/uses it as a local type
205
- const isUsedHere = interfaceContent.includes(`import { ${typeName} } from "./${typeName}"`) ||
206
- (interfaceContent.includes(`from "./${typeName}"`) && interfaceContent.includes(`{ ${typeName} }`));
240
+ const isUsedHere = interfaceContent.includes(`import { ${normalizedTypeName} } from "./${normalizedTypeName}"`) ||
241
+ (interfaceContent.includes(`from "./${normalizedTypeName}"`) && interfaceContent.includes(`{ ${normalizedTypeName} }`));
207
242
  if (!isUsedHere) {
208
243
  continue; // Skip unused external references for this interface
209
244
  }
@@ -214,7 +249,7 @@ async function processStructureDefinition(sd, ctx) {
214
249
  // Try to fetch the external StructureDefinition
215
250
  let externalSD = null;
216
251
  try {
217
- console.log(`Attempting to fetch external profile: ${profileInfo.profileUrl}`);
252
+ logger.log(`Attempting to fetch external profile: ${profileInfo.profileUrl}`);
218
253
  externalSD = await fetchStructureDefinition(profileInfo.profileUrl);
219
254
  }
220
255
  catch (err) {
@@ -223,26 +258,68 @@ async function processStructureDefinition(sd, ctx) {
223
258
  // If we successfully fetched the external SD, process it as a full profile
224
259
  if (externalSD) {
225
260
  try {
226
- console.log(`Processing external profile as StructureDefinition: ${typeName}`);
227
- await processStructureDefinition(externalSD, { outputDir, fhirSourceHint: profileInfo.profileUrl, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, flags });
261
+ logger.log(`Processing external profile as StructureDefinition: ${normalizedTypeName}`);
262
+ const actualInterfaceName = await processStructureDefinition(externalSD, { outputDir, fhirSourceHint: profileInfo.profileUrl, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap });
263
+ // If the actual generated interface name differs from what we expected,
264
+ // add a mapping so other profiles can find it
265
+ if (actualInterfaceName !== normalizedTypeName) {
266
+ logger.log(`External profile ${normalizedTypeName} was generated as ${actualInterfaceName}, adding to profileIdToName mapping`);
267
+ profileIdToName?.set(normalizedTypeName, actualInterfaceName);
268
+ // Also add to URL mapping if we have the SD with a URL
269
+ if (externalSD.url) {
270
+ profileUrlToName?.set(externalSD.url, actualInterfaceName);
271
+ }
272
+ // Also add to existingStructureDefinitions so it won't be regenerated
273
+ existingStructureDefinitions?.add(normalizedTypeName);
274
+ }
275
+ else if (externalSD.url) {
276
+ // Even if names match, add URL mapping for future lookups
277
+ profileUrlToName?.set(externalSD.url, actualInterfaceName);
278
+ }
228
279
  continue; // Skip type alias generation since we created a full interface
229
280
  }
230
281
  catch (err) {
231
- console.warn(`Failed to process external profile ${typeName}: ${err.message}, falling back to type alias`);
282
+ console.warn(`Failed to process external profile ${normalizedTypeName}: ${err.message}, falling back to type alias`);
232
283
  }
233
284
  }
234
285
  // Fallback: generate minimal type alias
286
+ failedFetchCount++;
287
+ failedFetchProfiles.push(profileInfo.profileUrl || normalizedTypeName);
235
288
  const baseTypeCandidates = Array.from(profileInfo.baseTypes.entries());
236
289
  const preferred = baseTypeCandidates.find(([, meta]) => meta.isFhir);
237
- const baseType = preferred?.[0] || 'Element';
290
+ // Smart detection: if the URL or name contains "extension", or baseTypes includes Extension, use Extension as base type
291
+ let baseType = preferred?.[0] || 'Element';
292
+ const urlLower = (profileInfo.profileUrl || '').toLowerCase();
293
+ const nameLower = normalizedTypeName.toLowerCase();
294
+ const hasExtensionInBaseTypes = baseTypeCandidates.some(([type]) => type === 'Extension');
295
+ if (urlLower.includes('extension') || nameLower.includes('extension') || hasExtensionInBaseTypes) {
296
+ baseType = 'Extension';
297
+ }
238
298
  const importLine = `import type { ${baseType} } from "fhir/r4";\n\n`;
239
- const typeAliasContent = `${importLine}// Type alias for external profile\n// Profile: ${profileInfo.profileUrl}\n// Note: Full StructureDefinition could not be fetched; using type alias based on ${baseType}\nexport type ${typeName} = ${baseType};\n`;
240
- writeToFile(path.join(outputDir, `${typeName}.ts`), typeAliasContent);
241
- console.log(`Generated type alias for external profile: ${typeName} (fetch failed, using ${baseType} alias)`);
299
+ const typeAliasContent = `${importLine}// Type alias for external profile\n// Profile: ${profileInfo.profileUrl}\n// Note: Full StructureDefinition could not be fetched; using type alias based on ${baseType}\nexport type ${normalizedTypeName} = ${baseType};\n`;
300
+ writeToFile(path.join(outputDir, `${normalizedTypeName}.ts`), typeAliasContent);
301
+ logger.log(`Generated type alias for external profile: ${normalizedTypeName} (fetch failed, using ${baseType} alias)`);
302
+ }
303
+ // Post-process: Fix up interface content to use correct import names for external profiles
304
+ // that were remapped during processing
305
+ let finalInterfaceContent = interfaceContent;
306
+ for (const [originalName, actualName] of profileIdToName?.entries() || []) {
307
+ if (originalName !== actualName) {
308
+ // Replace imports
309
+ const oldImport = `import { ${originalName} } from "./${originalName}"`;
310
+ const newImport = `import { ${actualName} } from "./${actualName}"`;
311
+ finalInterfaceContent = finalInterfaceContent.replace(oldImport, newImport);
312
+ // Also replace type usage in the interface
313
+ // We need to be careful to only replace type annotations, not field names
314
+ // This regex matches: type references after `: ` or `Array<` or `| ` or `& `
315
+ const typeUsageRegex = new RegExp(`(:\\s+|Array<|\\|\\s+|&\\s+)${originalName}(\\s*[;,>\\|&\\)\\n])`, 'g');
316
+ finalInterfaceContent = finalInterfaceContent.replace(typeUsageRegex, `$1${actualName}$2`);
317
+ }
242
318
  }
243
- const merged = mergeFields(parsed.newFields, parsed.oldFields);
319
+ // Use aligned fields for validation to ensure consistency with interface generation
320
+ const merged = mergeFields(aligned.alignedNewFields, parsed.oldFields);
244
321
  const validateFn = generateValidateProfileFunction(interfaceName, merged);
245
- const candidateRequiredFields = [...parsed.newFields, ...parsed.oldFields];
322
+ const candidateRequiredFields = [...aligned.alignedNewFields, ...parsed.oldFields];
246
323
  let requiredFields = candidateRequiredFields
247
324
  // only take direct children of the root path: `${sd.id}.${child}` or `${baseResource}.${child}`
248
325
  .filter(f => {
@@ -278,7 +355,7 @@ async function processStructureDefinition(sd, ctx) {
278
355
  }
279
356
  const classContent = generateClass(`${interfaceName}Class`, interfaceName, baseResource, requiredFields, sd.url);
280
357
  ensureRandomSupportFile(outputDir);
281
- writeInterfaceAndValidatorToFile(outputDir, interfaceName, interfaceContent, validateFn);
358
+ writeInterfaceAndValidatorToFile(outputDir, interfaceName, finalInterfaceContent, validateFn);
282
359
  // Only generate class files if --no-classes flag is not set
283
360
  if (!flags?.noClasses) {
284
361
  writeToFile(path.join(outputDir, `${interfaceName}Class.ts`), classContent);
@@ -288,9 +365,10 @@ async function processStructureDefinition(sd, ctx) {
288
365
  }
289
366
  }
290
367
  console.log(`Generated artifacts for ${interfaceName}`);
368
+ return interfaceName;
291
369
  }
292
370
  export async function generate(fhirSource, outputDir, flags) {
293
- console.log(`Fetching StructureDefinitions from: ${fhirSource}`);
371
+ logger.log(`Fetching StructureDefinitions from: ${fhirSource}`);
294
372
  let structureDefinitions = [];
295
373
  let localPath = fhirSource;
296
374
  try {
@@ -311,7 +389,7 @@ export async function generate(fhirSource, outputDir, flags) {
311
389
  structureDefinitions = readStructureDefinitionsFromDir(extracted);
312
390
  valueSetCodesMap = readValueSetCodesFromDir(extracted);
313
391
  valueSets = readValueSetsFromDir(extracted);
314
- console.log(`Loaded ${valueSets.size} ValueSets (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
392
+ logger.log(`Loaded ${valueSets.size} ValueSets (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
315
393
  }
316
394
  finally {
317
395
  try {
@@ -323,7 +401,7 @@ export async function generate(fhirSource, outputDir, flags) {
323
401
  else {
324
402
  structureDefinitions = await fetchStructureDefinitions(fhirSource);
325
403
  }
326
- console.log(`Fetched ${structureDefinitions.length} StructureDefinitions.`);
404
+ logger.log(`Fetched ${structureDefinitions.length} StructureDefinitions.`);
327
405
  ensureDirectoryExists(outputDir);
328
406
  // Precompute child type map for common core datatypes (best effort; silent on failures)
329
407
  let fhirChildTypeMap = new Map();
@@ -335,8 +413,17 @@ export async function generate(fhirSource, outputDir, flags) {
335
413
  // Build set of existing StructureDefinition names to avoid generating type aliases for them
336
414
  const existingStructureDefinitions = new Set();
337
415
  const profileIdToName = new Map(); // Map from profile ID to interface name
416
+ const profileUrlToName = new Map(); // Map from profile URL to interface name
417
+ // First pass: Pre-populate ALL mappings before any interface generation
418
+ // This ensures the maps are complete when generating interfaces (even on first run without cache)
419
+ logger.log(`Pre-populating profile registries with ${structureDefinitions.length} StructureDefinitions...`);
338
420
  for (const sd of structureDefinitions) {
339
- const name = toPascalCase(sd.name || sd.id || '');
421
+ const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
422
+ let name = toPascalCase(rawInterfaceName);
423
+ // Check if the interface name conflicts with a FHIR base type (same logic as processStructureDefinition)
424
+ if (fhirInterfaceNames.includes(name)) {
425
+ name = `${name}Extension`;
426
+ }
340
427
  if (name) {
341
428
  existingStructureDefinitions.add(name);
342
429
  // Also map the profile ID to the name for base profile lookups
@@ -344,11 +431,17 @@ export async function generate(fhirSource, outputDir, flags) {
344
431
  const profileId = sanitizeIdentifier(sd.id);
345
432
  profileIdToName.set(profileId, name);
346
433
  }
434
+ // Also map the profile URL to the name for extension resolution
435
+ if (sd.url) {
436
+ profileUrlToName.set(sd.url, name);
437
+ logger.log(`[Pre-Registry] ${sd.url} -> ${name}`);
438
+ }
347
439
  }
348
440
  }
441
+ logger.log(`Profile registries pre-populated: ${profileUrlToName.size} URLs, ${profileIdToName.size} IDs`);
349
442
  const examples = loadExamplesFromPackage(fhirSource);
350
443
  for (const sd of structureDefinitions) {
351
- await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, flags, fhirChildTypeMap });
444
+ await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap });
352
445
  }
353
446
  }
354
447
  /**
@@ -365,7 +458,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
365
458
  const structureDefinitions = readStructureDefinitionsFromDir(extractedRoot);
366
459
  const valueSetCodesMap = readValueSetCodesFromDir(extractedRoot);
367
460
  const valueSets = readValueSetsFromDir(extractedRoot);
368
- console.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
461
+ logger.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
369
462
  const outputDir = path.join(extractedRoot, 'generated');
370
463
  ensureDirectoryExists(outputDir);
371
464
  // Generate TypeScript files for ValueSets inside the embedded package
@@ -395,12 +488,12 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
395
488
  if (generatedValueSetCount > 0) {
396
489
  const registryContent = generateValueSetRegistry(valueSets);
397
490
  fs.writeFileSync(path.join(valueSetOutputDir, 'index.ts'), registryContent, 'utf-8');
398
- console.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
491
+ logger.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
399
492
  didGenerateValueSets = true;
400
493
  }
401
494
  else {
402
495
  if (skippedEmptyCount > 0) {
403
- console.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
496
+ logger.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
404
497
  }
405
498
  }
406
499
  }
@@ -424,21 +517,34 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
424
517
  // Build set of existing StructureDefinition names
425
518
  const existingStructureDefinitions = new Set();
426
519
  const profileIdToName = new Map();
520
+ const profileUrlToName = new Map();
521
+ // First pass: Pre-populate mappings from all StructureDefinitions
522
+ logger.log(`Pre-populating profile registries with ${structureDefinitions.length} StructureDefinitions...`);
427
523
  for (const sd of structureDefinitions) {
428
- const name = toPascalCase(sd.name || sd.id || '');
524
+ const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
525
+ let name = toPascalCase(rawInterfaceName);
526
+ // Check if the interface name conflicts with a FHIR base type (same logic as processStructureDefinition)
527
+ if (fhirInterfaceNames.includes(name)) {
528
+ name = `${name}Extension`;
529
+ }
429
530
  if (name) {
430
531
  existingStructureDefinitions.add(name);
431
532
  if (sd.id) {
432
533
  const profileId = sanitizeIdentifier(sd.id);
433
534
  profileIdToName.set(profileId, name);
434
535
  }
536
+ if (sd.url) {
537
+ profileUrlToName.set(sd.url, name);
538
+ logger.log(`[Pre-Registry] ${sd.url} -> ${name}`);
539
+ }
435
540
  }
436
541
  }
542
+ logger.log(`Profile registries pre-populated: ${profileUrlToName.size} URLs, ${profileIdToName.size} IDs`);
437
543
  // Register all local StructureDefinitions for resolution before HTTP fetches
438
544
  registerLocalStructureDefinitions(structureDefinitions);
439
545
  const examples = loadExamplesFromPackage(extractedRoot);
440
546
  for (const sd of structureDefinitions) {
441
- await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, flags });
547
+ await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
442
548
  }
443
549
  // Create package.json in generated folder
444
550
  const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
@@ -470,23 +576,23 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
470
576
  };
471
577
  const generatedPackageJsonPath = path.join(outputDir, 'package.json');
472
578
  fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
473
- console.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
579
+ logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
474
580
  // Copy fhir-r4.d.ts ambient module declaration to support fhir/r4 imports
475
581
  // The file is at out/fhir-r4.d.ts (root of compiled output)
476
582
  const fhirR4DtsSource = path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), 'fhir-r4.d.ts');
477
583
  const fhirR4DtsDest = path.join(outputDir, 'fhir-r4.d.ts');
478
584
  if (fs.existsSync(fhirR4DtsSource)) {
479
585
  fs.copyFileSync(fhirR4DtsSource, fhirR4DtsDest);
480
- console.log('Copied fhir-r4.d.ts ambient module declaration');
586
+ logger.log('Copied fhir-r4.d.ts ambient module declaration');
481
587
  }
482
588
  else {
483
589
  console.warn('Warning: fhir-r4.d.ts not found. Run: npm run generate:fhir-module');
484
590
  }
485
591
  // Generate index.ts that exports all interfaces
486
- console.log('Generating index.ts exports...');
592
+ logger.log('Generating index.ts exports...');
487
593
  await generateIndexFile(outputDir);
488
594
  // Compile TypeScript to JavaScript
489
- console.log('Compiling TypeScript to JavaScript...');
595
+ logger.log('Compiling TypeScript to JavaScript...');
490
596
  await compileTypeScriptToJS(outputDir);
491
597
  const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
492
598
  await createPackageFromDir(extractedRoot, finalArchive);
@@ -519,7 +625,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
519
625
  // Load ValueSets from the directory
520
626
  const valueSets = readValueSetsFromDir(inputDir);
521
627
  const valueSetCodesMap = readValueSetCodesFromDir(inputDir);
522
- console.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
628
+ logger.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
523
629
  // Generate TypeScript files for ValueSets
524
630
  const valueSetOutputDir = path.join(outputDir, 'valuesets');
525
631
  let generatedValueSetCount = 0;
@@ -549,11 +655,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
549
655
  const registryContent = generateValueSetRegistry(valueSets);
550
656
  const registryPath = path.join(valueSetOutputDir, 'index.ts');
551
657
  fs.writeFileSync(registryPath, registryContent, 'utf-8');
552
- console.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
658
+ logger.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
553
659
  }
554
660
  else {
555
661
  if (skippedEmptyCount > 0) {
556
- console.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
662
+ logger.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
557
663
  }
558
664
  // Remove any stale valuesets directory from previous runs
559
665
  try {
@@ -580,35 +686,49 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
580
686
  // Build set of existing StructureDefinition names from JSON files
581
687
  const existingStructureDefinitions = new Set();
582
688
  const profileIdToName = new Map();
689
+ const profileUrlToName = new Map(); // Map from profile URL to interface name
583
690
  const localStructureDefinitions = [];
691
+ // First pass: Pre-populate mappings from all JSON StructureDefinitions
692
+ logger.log(`Pre-populating profile registries from ${jsonFiles.length} JSON files...`);
584
693
  for (const jf of jsonFiles) {
585
694
  try {
586
695
  const raw = JSON.parse(fs.readFileSync(path.join(inputDir, jf), 'utf-8'));
587
696
  if (typeof raw === 'object' && raw !== null && raw.resourceType === 'StructureDefinition') {
588
697
  const sd = raw;
589
698
  localStructureDefinitions.push(sd); // Collect for local resolution
590
- const name = toPascalCase(sd.name || sd.id || '');
699
+ const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
700
+ let name = toPascalCase(rawInterfaceName);
701
+ // Check if the interface name conflicts with a FHIR base type (same logic as processStructureDefinition)
702
+ if (fhirInterfaceNames.includes(name)) {
703
+ name = `${name}Extension`;
704
+ }
591
705
  if (name) {
592
706
  existingStructureDefinitions.add(name);
593
707
  if (sd.id) {
594
708
  const profileId = sanitizeIdentifier(sd.id);
595
709
  profileIdToName.set(profileId, name);
596
710
  }
711
+ // Also map profile URL to name for extension resolution
712
+ if (sd.url) {
713
+ profileUrlToName.set(sd.url, name);
714
+ logger.log(`[Pre-Registry] ${sd.url} -> ${name}`);
715
+ }
597
716
  }
598
717
  }
599
718
  }
600
719
  catch { /* ignore parse errors in pre-scan */ }
601
720
  }
721
+ logger.log(`Profile registries pre-populated: ${profileUrlToName.size} URLs, ${profileIdToName.size} IDs`);
602
722
  // Register all local StructureDefinitions for resolution before HTTP fetches
603
723
  registerLocalStructureDefinitions(localStructureDefinitions);
604
724
  for (const file of archives) {
605
725
  const fullPath = path.join(inputDir, file);
606
- console.log(`Processing package: ${file}`);
726
+ logger.log(`Processing package: ${file}`);
607
727
  try {
608
728
  const outName = file.replace(/(\.tgz|\.zip)$/i, '.with-generated$1');
609
729
  const outArchive = path.join(outputDir, outName);
610
730
  await generateIntoPackage(fullPath, outArchive);
611
- console.log(`→ Wrote ${outArchive}`);
731
+ logger.log(`→ Wrote ${outArchive}`);
612
732
  }
613
733
  catch (err) {
614
734
  console.error(`Failed processing ${file}:`, err);
@@ -618,7 +738,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
618
738
  const full = path.join(inputDir, jf);
619
739
  try {
620
740
  const raw = JSON.parse(fs.readFileSync(full, 'utf-8'));
621
- await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, flags });
741
+ await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
622
742
  }
623
743
  catch (err) {
624
744
  console.error(`Failed processing JSON ${jf}:`, err);