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.
- package/out/generator/importManager.js +64 -12
- package/out/generator/index.js +159 -39
- package/out/generator/interfaceGenerator.js +288 -125
- package/out/generator/packageParser.js +4 -3
- package/out/generator/sdParser.js +134 -69
- package/out/generator/testGenerator.js +2 -1
- package/out/generator/utils.js +2 -1
- package/out/generator/validatorGenerator.js +247 -4
- package/out/main.js +47 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
//
|
|
53
|
-
|
|
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
|
-
|
|
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/${
|
|
69
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/out/generator/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 { ${
|
|
206
|
-
(interfaceContent.includes(`from "./${
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
240
|
-
writeToFile(path.join(outputDir, `${
|
|
241
|
-
|
|
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
|
-
|
|
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 = [...
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
592
|
+
logger.log('Generating index.ts exports...');
|
|
487
593
|
await generateIndexFile(outputDir);
|
|
488
594
|
// Compile TypeScript to JavaScript
|
|
489
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
+
logger.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
|
|
553
659
|
}
|
|
554
660
|
else {
|
|
555
661
|
if (skippedEmptyCount > 0) {
|
|
556
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|