babelfhir-ts 1.3.2 → 1.3.4

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/README.md CHANGED
@@ -47,7 +47,7 @@
47
47
  - **Random data builders** for testing and development (when class generation is enabled)
48
48
  - **Zero manual mapping**—consume any FHIR package or Implementation Guide directly from registries
49
49
  - **Fast and lightweight**—minimal runtime deps; only `fhirpath` is required for validators
50
- - **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) with profile-specific methods (e.g., `.usCorePatient()`, `.pASClaim()`) on top of base R4 resource accessors
50
+ - **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [`client-r4b`](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [`client-r5`](https://www.npmjs.com/package/@babelfhir-ts/client-r5) with profile-specific methods (e.g., `.usCorePatient()`, `.pASClaim()`) on top of base resource accessors
51
51
  - **Install any FHIR profile as a node module**—use `babelfhir-ts install` to add Implementation Guides directly to your project
52
52
 
53
53
  <!-- PARITY-BADGES:START - Do not remove or modify this section -->
@@ -260,7 +260,7 @@ Full documentation is available at **[max-health-inc.github.io/BabelFHIR-TS/docs
260
260
  - [Generated Code Guide](https://max-health-inc.github.io/BabelFHIR-TS/docs/generated-code) — understanding the output, module resolution, FHIR type imports
261
261
  - [FHIR Client](https://max-health-inc.github.io/BabelFHIR-TS/docs/fhir-client) — type-safe server interactions
262
262
  - [Validation](https://max-health-inc.github.io/BabelFHIR-TS/docs/validation) — what validators check, CI pipelines, known Firely SDK issues
263
- - [Limitations](https://max-health-inc.github.io/BabelFHIR-TS/docs/limitations) — R4 only, edge cases, random() caveats
263
+ - [Limitations](https://max-health-inc.github.io/BabelFHIR-TS/docs/limitations) — edge cases, random() caveats
264
264
  - [Contributing](https://max-health-inc.github.io/BabelFHIR-TS/docs/contributing) — dev setup, scripts, project structure
265
265
 
266
266
  ## License
@@ -283,7 +283,8 @@ For security issues, please see [SECURITY.md](SECURITY.md) for our security poli
283
283
  ## Links
284
284
 
285
285
  - [npm package — babelfhir-ts](https://www.npmjs.com/package/babelfhir-ts)
286
- - [npm package — @babelfhir-ts/client-r4](https://www.npmjs.com/package/@babelfhir-ts/client-r4)
286
+ - [npm package — @babelfhir-ts/client-r4](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [client-r4b](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [client-r5](https://www.npmjs.com/package/@babelfhir-ts/client-r5)
287
+ - [npm package — @babelfhir-ts/zod](https://www.npmjs.com/package/@babelfhir-ts/zod)
287
288
  - [GitHub repository](https://github.com/Max-Health-Inc/BabelFHIR-TS)
288
289
  - [Validation report](https://max-health-inc.github.io/BabelFHIR-TS/)
289
290
  - [Issue tracker](https://github.com/Max-Health-Inc/BabelFHIR-TS/issues)
@@ -1,7 +1,31 @@
1
1
  import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
2
- import { fhirModuleSpecifier } from '../../fhir/versionContext.js';
2
+ import { ctx, fhirModuleSpecifier } from '../../fhir/versionContext.js';
3
+ /**
4
+ * Build the SampledData skeleton object literal from the active version's
5
+ * dataTypes map. R4 has `period`, R5 replaced it with `interval` +
6
+ * `intervalUnit` — but instead of hard-coding the difference we simply
7
+ * inspect which fields exist for the current FHIR version.
8
+ */
9
+ function buildSampledDataFields() {
10
+ const sdFields = ctx().dataTypes['SampledData'] ?? {};
11
+ const parts = [];
12
+ // origin is always a Quantity — use a complete skeleton
13
+ parts.push("origin: { value: 0, unit: 'unit', system: '" + TS.UCUM + "', code: '1' }");
14
+ // Version-varying numeric / string fields
15
+ if ('period' in sdFields)
16
+ parts.push('period: 1');
17
+ if ('interval' in sdFields)
18
+ parts.push('interval: 1');
19
+ if ('intervalUnit' in sdFields)
20
+ parts.push("intervalUnit: 'ms'");
21
+ // Always-present fields
22
+ parts.push('dimensions: 1');
23
+ parts.push("data: 'E'");
24
+ return parts.join(', ');
25
+ }
3
26
  export function generateRandomUtilities() {
4
27
  const fhirModule = fhirModuleSpecifier();
28
+ const sampledDataFields = buildSampledDataFields();
5
29
  return `// Auto-generated helper for random resource generation with optional deterministic seeding
6
30
  // Uses official FHIR TypeScript definitions
7
31
  import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity, Money, Narrative, Dosage, Ratio, Range, Annotation, Attachment, Age, Duration, Expression, RelatedArtifact, DataRequirement, ParameterDefinition, TriggerDefinition, ContactDetail, UsageContext, Signature, SampledData, Timing } from '${fhirModule}';
@@ -96,7 +120,7 @@ export function skeletonTriggerDefinition(): TriggerDefinition { return { type:
96
120
  export function skeletonContactDetail(): ContactDetail { return { name: 'Contact' }; }
97
121
  export function skeletonUsageContext(): UsageContext { return { code: { system: 'http://terminology.hl7.org/CodeSystem/usage-context-type', code: 'focus' }, valueCodeableConcept: skeletonCodeableConcept() }; }
98
122
  export function skeletonSignature(): Signature { return { type: [{ system: 'urn:iso-astm:E1762-95:2013', code: '1.2.840.10065.1.12.1.1' }], when: randomDate(), who: skeletonReference('Practitioner') }; }
99
- export function skeletonSampledData(): SampledData { return { origin: { value: 0, unit: 'unit', system: '${TS.UCUM}', code: '1' }, period: 1, dimensions: 1, data: 'E' }; }
123
+ export function skeletonSampledData(): SampledData { return { ${sampledDataFields} }; }
100
124
  export function skeletonTiming(): Timing { return { repeat: { frequency: 1, period: 1, periodUnit: 'd' } }; }
101
125
 
102
126
  /**
@@ -39,8 +39,8 @@ export function installBaseZodTypes(outputDir) {
39
39
  if (fs.existsSync(srcPkgJson)) {
40
40
  fs.copyFileSync(srcPkgJson, path.join(pkgDir, 'package.json'));
41
41
  }
42
- // Copy all runtime dist files into the version-specific subdirectory
43
- const distSlugDir = path.join(pkgDir, slug);
42
+ // Copy all runtime dist files into dist/<slug>/ to match package.json exports
43
+ const distSlugDir = path.join(pkgDir, 'dist', slug);
44
44
  fs.mkdirSync(distSlugDir, { recursive: true });
45
45
  for (const file of fs.readdirSync(runtimeDist)) {
46
46
  fs.copyFileSync(path.join(runtimeDist, file), path.join(distSlugDir, file));
@@ -121,70 +121,72 @@ export function deriveVersionData(corePackageDir) {
121
121
  const resourceFields = {};
122
122
  const files = fs.readdirSync(sdDir)
123
123
  .filter(f => f.startsWith('StructureDefinition-') && f.endsWith('.json'));
124
- // Two-pass approach: first collect base specialisations, then constraint profiles
125
- const parsedSDs = [];
124
+ // Memory-efficient approach: parse SDs one file at a time.
125
+ // Pass 1 processes base specialisations and records which constraint SDs
126
+ // need full processing in Pass 2 (only complex-type constraints whose
127
+ // base type is a known specialisation — typically <10 out of ~828).
128
+ const complexTypeSpecNames = new Set();
129
+ const constraintSDFiles = []; // filenames needing Pass 2
126
130
  for (const file of files) {
127
131
  try {
128
132
  const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
129
133
  const sd = JSON.parse(raw);
130
134
  if (sd.resourceType !== 'StructureDefinition')
131
135
  continue;
132
- parsedSDs.push({
133
- kind: sd.kind, derivation: sd.derivation,
134
- typeName: sd.type, sdName: sd.name || sd.id, sd,
135
- });
136
+ const kind = sd.kind;
137
+ const derivation = sd.derivation;
138
+ const typeName = sd.type;
139
+ // Constraint SDs are deferred to Pass 2
140
+ if (derivation === 'constraint' && kind === 'complex-type') {
141
+ constraintSDFiles.push(file);
142
+ continue;
143
+ }
144
+ if (!typeName)
145
+ continue;
146
+ if (derivation && derivation !== 'specialization')
147
+ continue;
148
+ if (kind === 'logical')
149
+ continue;
150
+ if (kind === 'resource') {
151
+ resourceNames.push(typeName);
152
+ interfaceNames.push(typeName);
153
+ collectBackboneInterfaces(sd, typeName, interfaceNames);
154
+ collectDataTypeFields(sd, typeName, resourceFields);
155
+ }
156
+ else if (kind === 'primitive-type') {
157
+ primitiveTypeNames.push(typeName);
158
+ }
159
+ else if (kind === 'complex-type') {
160
+ complexTypeSpecNames.add(typeName);
161
+ interfaceNames.push(typeName);
162
+ collectDataTypeFields(sd, typeName, dataTypes);
163
+ collectBackboneInterfaces(sd, typeName, interfaceNames);
164
+ }
165
+ // sd is now GC-eligible — no reference kept
136
166
  }
137
167
  catch { /* skip */ }
138
168
  }
139
- // Track complex-type specialisation names to filter constraint SDs later
140
- const complexTypeSpecNames = new Set();
141
- // Pass 1: base specialisation SDs (derivation = 'specialization' or absent)
142
- for (const { kind, derivation, typeName, sd } of parsedSDs) {
143
- if (!typeName)
144
- continue;
145
- // Abstract bases (Element, Resource) have no derivation field
146
- if (derivation && derivation !== 'specialization')
147
- continue;
148
- // Skip logical types (Definition, Event, FiveWs, Request)
149
- if (kind === 'logical')
150
- continue;
151
- if (kind === 'resource') {
152
- resourceNames.push(typeName);
153
- // Resources are also valid interface names (used by isFhirType)
154
- interfaceNames.push(typeName);
155
- collectBackboneInterfaces(sd, typeName, interfaceNames);
156
- // Collect resource direct-child fields for cardinality tables
157
- collectDataTypeFields(sd, typeName, resourceFields);
158
- }
159
- else if (kind === 'primitive-type') {
160
- primitiveTypeNames.push(typeName);
161
- }
162
- else if (kind === 'complex-type') {
163
- complexTypeSpecNames.add(typeName);
164
- interfaceNames.push(typeName);
165
- collectDataTypeFields(sd, typeName, dataTypes);
166
- collectBackboneInterfaces(sd, typeName, interfaceNames);
167
- }
168
- }
169
- // Pass 2: core profiles (constraint) on data types shipped in the core package
170
- // Examples: MoneyQuantity, SimpleQuantity — both are constraint on Quantity
171
- // Only include SDs whose base type is a known complex-type specialisation,
172
- // excluding Extension (hundreds of extension constraint SDs) and
173
- // infrastructure types that aren't used as field type codes.
169
+ // Pass 2: re-read only the constraint complex-type SDs whose base type is a
170
+ // known specialisation (e.g. MoneyQuantity constraining Quantity).
174
171
  const CONSTRAINT_EXCLUDE_TYPES = new Set(['Extension', 'ElementDefinition']);
175
- for (const { kind, derivation, typeName, sdName, sd } of parsedSDs) {
176
- if (!typeName || !sdName)
177
- continue;
178
- if (derivation !== 'constraint' || kind !== 'complex-type')
179
- continue;
180
- if (sdName === typeName)
181
- continue;
182
- if (!complexTypeSpecNames.has(typeName))
183
- continue;
184
- if (CONSTRAINT_EXCLUDE_TYPES.has(typeName))
185
- continue;
186
- interfaceNames.push(sdName);
187
- collectDataTypeFields(sd, sdName, dataTypes);
172
+ for (const file of constraintSDFiles) {
173
+ try {
174
+ const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
175
+ const sd = JSON.parse(raw);
176
+ const typeName = sd.type;
177
+ const sdName = sd.name || sd.id;
178
+ if (!typeName || !sdName)
179
+ continue;
180
+ if (sdName === typeName)
181
+ continue;
182
+ if (!complexTypeSpecNames.has(typeName))
183
+ continue;
184
+ if (CONSTRAINT_EXCLUDE_TYPES.has(typeName))
185
+ continue;
186
+ interfaceNames.push(sdName);
187
+ collectDataTypeFields(sd, sdName, dataTypes);
188
+ }
189
+ catch { /* skip */ }
188
190
  }
189
191
  // Deterministic ordering
190
192
  resourceNames.sort();
@@ -163,67 +163,67 @@ export function deriveVersionData(corePackageDir: string): DerivedVersionData {
163
163
  const files = fs.readdirSync(sdDir)
164
164
  .filter(f => f.startsWith('StructureDefinition-') && f.endsWith('.json'));
165
165
 
166
- // Two-pass approach: first collect base specialisations, then constraint profiles
167
- const parsedSDs: Array<{
168
- kind?: string; derivation?: string; typeName?: string; sdName?: string;
169
- sd: Record<string, unknown>;
170
- }> = [];
166
+ // Memory-efficient approach: parse SDs one file at a time.
167
+ // Pass 1 processes base specialisations and records which constraint SDs
168
+ // need full processing in Pass 2 (only complex-type constraints whose
169
+ // base type is a known specialisation — typically <10 out of ~828).
170
+ const complexTypeSpecNames = new Set<string>();
171
+ const constraintSDFiles: string[] = []; // filenames needing Pass 2
171
172
 
172
173
  for (const file of files) {
173
174
  try {
174
175
  const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
175
176
  const sd = JSON.parse(raw);
176
177
  if (sd.resourceType !== 'StructureDefinition') continue;
177
- parsedSDs.push({
178
- kind: sd.kind, derivation: sd.derivation,
179
- typeName: sd.type, sdName: sd.name || sd.id, sd,
180
- });
181
- } catch { /* skip */ }
182
- }
183
178
 
184
- // Track complex-type specialisation names to filter constraint SDs later
185
- const complexTypeSpecNames = new Set<string>();
186
-
187
- // Pass 1: base specialisation SDs (derivation = 'specialization' or absent)
188
- for (const { kind, derivation, typeName, sd } of parsedSDs) {
189
- if (!typeName) continue;
190
- // Abstract bases (Element, Resource) have no derivation field
191
- if (derivation && derivation !== 'specialization') continue;
192
- // Skip logical types (Definition, Event, FiveWs, Request)
193
- if (kind === 'logical') continue;
194
-
195
- if (kind === 'resource') {
196
- resourceNames.push(typeName);
197
- // Resources are also valid interface names (used by isFhirType)
198
- interfaceNames.push(typeName);
199
- collectBackboneInterfaces(sd, typeName, interfaceNames);
200
- // Collect resource direct-child fields for cardinality tables
201
- collectDataTypeFields(sd, typeName, resourceFields);
202
- } else if (kind === 'primitive-type') {
203
- primitiveTypeNames.push(typeName);
204
- } else if (kind === 'complex-type') {
205
- complexTypeSpecNames.add(typeName);
206
- interfaceNames.push(typeName);
207
- collectDataTypeFields(sd, typeName, dataTypes);
208
- collectBackboneInterfaces(sd, typeName, interfaceNames);
209
- }
179
+ const kind = sd.kind;
180
+ const derivation = sd.derivation;
181
+ const typeName = sd.type;
182
+
183
+ // Constraint SDs are deferred to Pass 2
184
+ if (derivation === 'constraint' && kind === 'complex-type') {
185
+ constraintSDFiles.push(file);
186
+ continue;
187
+ }
188
+
189
+ if (!typeName) continue;
190
+ if (derivation && derivation !== 'specialization') continue;
191
+ if (kind === 'logical') continue;
192
+
193
+ if (kind === 'resource') {
194
+ resourceNames.push(typeName);
195
+ interfaceNames.push(typeName);
196
+ collectBackboneInterfaces(sd, typeName, interfaceNames);
197
+ collectDataTypeFields(sd, typeName, resourceFields);
198
+ } else if (kind === 'primitive-type') {
199
+ primitiveTypeNames.push(typeName);
200
+ } else if (kind === 'complex-type') {
201
+ complexTypeSpecNames.add(typeName);
202
+ interfaceNames.push(typeName);
203
+ collectDataTypeFields(sd, typeName, dataTypes);
204
+ collectBackboneInterfaces(sd, typeName, interfaceNames);
205
+ }
206
+ // sd is now GC-eligible — no reference kept
207
+ } catch { /* skip */ }
210
208
  }
211
209
 
212
- // Pass 2: core profiles (constraint) on data types shipped in the core package
213
- // Examples: MoneyQuantity, SimpleQuantity both are constraint on Quantity
214
- // Only include SDs whose base type is a known complex-type specialisation,
215
- // excluding Extension (hundreds of extension constraint SDs) and
216
- // infrastructure types that aren't used as field type codes.
210
+ // Pass 2: re-read only the constraint complex-type SDs whose base type is a
211
+ // known specialisation (e.g. MoneyQuantity constraining Quantity).
217
212
  const CONSTRAINT_EXCLUDE_TYPES = new Set(['Extension', 'ElementDefinition']);
218
- for (const { kind, derivation, typeName, sdName, sd } of parsedSDs) {
219
- if (!typeName || !sdName) continue;
220
- if (derivation !== 'constraint' || kind !== 'complex-type') continue;
221
- if (sdName === typeName) continue;
222
- if (!complexTypeSpecNames.has(typeName)) continue;
223
- if (CONSTRAINT_EXCLUDE_TYPES.has(typeName)) continue;
224
-
225
- interfaceNames.push(sdName);
226
- collectDataTypeFields(sd, sdName, dataTypes);
213
+ for (const file of constraintSDFiles) {
214
+ try {
215
+ const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
216
+ const sd = JSON.parse(raw);
217
+ const typeName = sd.type;
218
+ const sdName = sd.name || sd.id;
219
+ if (!typeName || !sdName) continue;
220
+ if (sdName === typeName) continue;
221
+ if (!complexTypeSpecNames.has(typeName)) continue;
222
+ if (CONSTRAINT_EXCLUDE_TYPES.has(typeName)) continue;
223
+
224
+ interfaceNames.push(sdName);
225
+ collectDataTypeFields(sd, sdName, dataTypes);
226
+ } catch { /* skip */ }
227
227
  }
228
228
 
229
229
  // Deterministic ordering
@@ -11,6 +11,8 @@ import { initVersionContext, ctx, versionSlug } from './fhir/versionContext.js';
11
11
  import { DEFAULT_FHIR_VERSION } from './fhir/types.js';
12
12
  import { spawn } from 'child_process';
13
13
  import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, cleanupStaleValueSetDir, initGenerationContext, enrichValueSetsFromCodeMap, } from './generationHelpers.js';
14
+ import { resetTimings, startPhase, getTimingSummary, formatTimingSummary } from './timing.js';
15
+ export { getTimingSummary, formatTimingSummary } from './timing.js';
14
16
  const log = logger.withTag('generator');
15
17
  // Convert version-context dataTypes to the Map format expected by code
16
18
  function buildFhirChildTypeMapFromJson() {
@@ -284,13 +286,17 @@ export async function generate(fhirSource, outputDir, flags) {
284
286
  */
285
287
  export async function generateIntoPackage(packageArchivePath, outArchivePath, flags) {
286
288
  initGenerationContext(flags, path.dirname(outArchivePath || packageArchivePath));
289
+ resetTimings();
287
290
  if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
288
291
  throw new Error('Only .tgz or .zip supported.');
292
+ let endPhase = startPhase('extract');
289
293
  const extractResult = await extractPackage(packageArchivePath);
290
294
  const extractedRoot = extractResult.path;
295
+ endPhase();
291
296
  const shouldCleanup = !extractResult.isCached; // Only cleanup temp directories, not cache
292
297
  try {
293
298
  // Auto-detect FHIR version from package when not explicitly set
299
+ endPhase = startPhase('init');
294
300
  if (!flags?.fhirVersion) {
295
301
  const detected = detectFhirVersion(extractedRoot);
296
302
  await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
@@ -299,8 +305,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
299
305
  await initVersionContext(flags.fhirVersion);
300
306
  }
301
307
  const fhirInterfaceNames = ctx().interfaceNames;
308
+ endPhase(); // init
302
309
  // Load dependencies from package.json BEFORE reading StructureDefinitions
303
310
  // This ensures base profiles (like coverage-de-basis) are available for resolution
311
+ endPhase = startPhase('resolve');
304
312
  const { getPackageManager } = await import('./parser/packageManager.js');
305
313
  const packageManager = getPackageManager();
306
314
  packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
@@ -313,6 +321,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
313
321
  let valueSets = readValueSetsFromDir(extractedRoot);
314
322
  logger.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
315
323
  logger.log(`Loaded ${Object.keys(valueSetCodesMap).length} ValueSet code mappings (including dependencies)`);
324
+ endPhase(); // resolve
316
325
  // Enrich ValueSets with codes resolved from CodeSystems (system-only compose.include)
317
326
  const bindingUrls = collectValueSetBindingUrls(structureDefinitions);
318
327
  enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingUrls);
@@ -328,6 +337,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
328
337
  }
329
338
  ensureDirectoryExists(outputDir);
330
339
  // Generate TypeScript files for ValueSets inside the embedded package
340
+ endPhase = startPhase('generate');
331
341
  const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
332
342
  if (!didGenerateValueSets) {
333
343
  cleanupStaleValueSetDir(outputDir);
@@ -335,12 +345,12 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
335
345
  const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
336
346
  // Register all local StructureDefinitions for resolution before HTTP fetches
337
347
  registerLocalStructureDefinitions(structureDefinitions);
338
- // Register SDs from dependency packages so external profile resolution can find them
339
- const depSDs = readStructureDefinitionsFromDependencies(extractedRoot);
340
- registerLocalStructureDefinitions(depSDs);
348
+ // PackageManager already loaded and registered dependency SDs above
349
+ // no need to re-parse them via readStructureDefinitionsFromDependencies.
341
350
  for (const sd of structureDefinitions) {
342
351
  await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
343
352
  }
353
+ endPhase(); // generate
344
354
  // Create package.json in generated folder
345
355
  const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
346
356
  let packageName = path.basename(packageArchivePath, path.extname(packageArchivePath));
@@ -441,8 +451,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
441
451
  fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
442
452
  fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
443
453
  // Compile TypeScript to JavaScript
454
+ endPhase = startPhase('compile');
444
455
  logger.log('Compiling TypeScript to JavaScript...');
445
456
  await compileTypeScriptToJS(outputDir);
457
+ endPhase(); // compile
446
458
  // Remove fhirpath stub — the real package is a peer dependency
447
459
  removeFhirpathStub(outputDir);
448
460
  // Remove @types/node stub — only needed for compilation
@@ -481,7 +493,13 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
481
493
  fs.copyFileSync(generatedPackageJsonPath, rootPackageJsonPath);
482
494
  logger.log('Copied package.json to tarball root');
483
495
  const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
496
+ endPhase = startPhase('repack');
484
497
  await createPackageFromDir(extractedRoot, finalArchive);
498
+ endPhase(); // repack
499
+ // Write timing summary to output directory
500
+ const timing = getTimingSummary();
501
+ fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
502
+ logger.log(formatTimingSummary(timing));
485
503
  return finalArchive;
486
504
  }
487
505
  finally {
@@ -503,12 +521,16 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
503
521
  */
504
522
  export async function generateIntoPackageDirect(packageArchivePath, flags) {
505
523
  initGenerationContext(flags, path.dirname(packageArchivePath));
524
+ resetTimings();
506
525
  if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
507
526
  throw new Error('Only .tgz or .zip supported.');
527
+ let endPhase = startPhase('extract');
508
528
  const extractResult = await extractPackage(packageArchivePath);
509
529
  const extractedRoot = extractResult.path;
530
+ endPhase();
510
531
  const shouldCleanup = !extractResult.isCached;
511
532
  // Auto-detect FHIR version from package when not explicitly set
533
+ endPhase = startPhase('init');
512
534
  if (!flags?.fhirVersion) {
513
535
  const detected = detectFhirVersion(extractedRoot);
514
536
  await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
@@ -517,6 +539,8 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
517
539
  await initVersionContext(flags.fhirVersion);
518
540
  }
519
541
  const fhirInterfaceNames = ctx().interfaceNames;
542
+ endPhase(); // init
543
+ endPhase = startPhase('resolve');
520
544
  const { getPackageManager } = await import('./parser/packageManager.js');
521
545
  const packageManager = getPackageManager();
522
546
  packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
@@ -527,6 +551,7 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
527
551
  let valueSets = readValueSetsFromDir(extractedRoot);
528
552
  logger.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
529
553
  logger.log(`Loaded ${Object.keys(valueSetCodesMap).length} ValueSet code mappings (including dependencies)`);
554
+ endPhase(); // resolve
530
555
  const bindingUrls = collectValueSetBindingUrls(structureDefinitions);
531
556
  enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingUrls);
532
557
  if (flags?.txServer) {
@@ -539,16 +564,18 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
539
564
  fs.rmSync(outputDir, { recursive: true, force: true });
540
565
  }
541
566
  ensureDirectoryExists(outputDir);
567
+ endPhase = startPhase('generate');
542
568
  const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
543
569
  if (!didGenerateValueSets)
544
570
  cleanupStaleValueSetDir(outputDir);
545
571
  const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
546
572
  registerLocalStructureDefinitions(structureDefinitions);
547
- const depSDs = readStructureDefinitionsFromDependencies(extractedRoot);
548
- registerLocalStructureDefinitions(depSDs);
573
+ // PackageManager already loaded and registered dependency SDs above —
574
+ // no need to re-parse them via readStructureDefinitionsFromDependencies.
549
575
  for (const sd of structureDefinitions) {
550
576
  await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
551
577
  }
578
+ endPhase(); // generate
552
579
  // Create package.json in generated folder
553
580
  const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
554
581
  let packageName = path.basename(packageArchivePath, path.extname(packageArchivePath));
@@ -633,8 +660,10 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
633
660
  fs.mkdirSync(nodeTypesDir, { recursive: true });
634
661
  fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
635
662
  fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
663
+ endPhase = startPhase('compile');
636
664
  logger.log('Compiling TypeScript to JavaScript...');
637
665
  await compileTypeScriptToJS(outputDir);
666
+ endPhase(); // compile
638
667
  removeFhirpathStub(outputDir);
639
668
  try {
640
669
  fs.rmSync(path.join(outputDir, 'node_modules', '@types'), { recursive: true, force: true });
@@ -660,6 +689,10 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
660
689
  fs.rmdirSync(nodeModulesDir);
661
690
  }
662
691
  catch { /* not empty or doesn't exist — fine */ }
692
+ // Write timing summary to output directory
693
+ const timing = getTimingSummary();
694
+ fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
695
+ logger.log(formatTimingSummary(timing));
663
696
  return {
664
697
  generatedDir: outputDir,
665
698
  cleanup: () => {
@@ -681,7 +714,9 @@ function deriveOutputArchiveName(input) {
681
714
  */
682
715
  export async function generateForDirectory(inputDir, outputDir, flags) {
683
716
  initGenerationContext(flags, outputDir);
717
+ resetTimings();
684
718
  // Auto-detect FHIR version from directory contents when not explicitly set
719
+ let endPhase = startPhase('init');
685
720
  if (!flags?.fhirVersion) {
686
721
  const detected = detectFhirVersion(inputDir);
687
722
  await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
@@ -690,11 +725,13 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
690
725
  await initVersionContext(flags.fhirVersion);
691
726
  }
692
727
  const fhirInterfaceNames = ctx().interfaceNames;
728
+ endPhase(); // init
693
729
  if (!fs.existsSync(inputDir))
694
730
  throw new Error(`Input directory not found: ${inputDir}`);
695
731
  if (!fs.existsSync(outputDir))
696
732
  fs.mkdirSync(outputDir, { recursive: true });
697
733
  // Load ValueSets from the directory
734
+ endPhase = startPhase('resolve');
698
735
  const valueSets = readValueSetsFromDir(inputDir);
699
736
  // Ensure dependency packages are downloaded before loading ValueSets
700
737
  await ensureDependenciesDownloaded(inputDir);
@@ -710,6 +747,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
710
747
  log.warn(`No archives or StructureDefinition JSON in ${inputDir}`);
711
748
  return;
712
749
  }
750
+ endPhase(); // resolve
713
751
  // Collect StructureDefinitions from JSON files, then build profile registries
714
752
  const localStructureDefinitions = [];
715
753
  for (const jf of jsonFiles) {
@@ -733,6 +771,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
733
771
  }
734
772
  }
735
773
  // Generate TypeScript files for ValueSets
774
+ endPhase = startPhase('generate');
736
775
  if (!emitValueSetFiles(valueSets, outputDir)) {
737
776
  cleanupStaleValueSetDir(outputDir);
738
777
  }
@@ -786,6 +825,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
786
825
  const { generateDicomweb } = await import('./emitters/dicomweb/dicomwebGenerator.js');
787
826
  generateDicomweb({ outputDir });
788
827
  }
828
+ endPhase(); // generate
829
+ // Write timing summary to output directory
830
+ const timing = getTimingSummary();
831
+ fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
832
+ logger.log(formatTimingSummary(timing));
789
833
  }
790
834
  export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
791
835
  // Initialize FHIR version context
@@ -4,6 +4,7 @@
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { registerLocalStructureDefinitions } from './sdParser.js';
7
+ import { isLargeHl7Package } from './packageParser.js';
7
8
  import { logger } from '../../logger.js';
8
9
  import { getFhirPackagesCacheDir, ensureCacheDir } from '../core/cacheConfig.js';
9
10
  export class PackageManager {
@@ -166,9 +167,14 @@ export class PackageManager {
166
167
  try {
167
168
  const files = fs.readdirSync(sdDir);
168
169
  for (const file of files) {
170
+ // Only read StructureDefinition-*.json files (FHIR naming convention).
171
+ // Fall back to all .json if no prefix-matched files found (e.g. KBV packages).
169
172
  if (!file.endsWith('.json') || file === 'package.json') {
170
173
  continue;
171
174
  }
175
+ if (!file.startsWith('StructureDefinition-')) {
176
+ continue;
177
+ }
172
178
  try {
173
179
  const filePath = path.join(sdDir, file);
174
180
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -182,6 +188,25 @@ export class PackageManager {
182
188
  continue;
183
189
  }
184
190
  }
191
+ // Fallback: if no prefix-matched files, scan all JSON (non-standard naming)
192
+ if (sds.length === 0) {
193
+ for (const file of files) {
194
+ if (!file.endsWith('.json') || file === 'package.json' || file.startsWith('StructureDefinition-')) {
195
+ continue;
196
+ }
197
+ try {
198
+ const filePath = path.join(sdDir, file);
199
+ const content = fs.readFileSync(filePath, 'utf-8');
200
+ const resource = JSON.parse(content);
201
+ if (resource.resourceType === 'StructureDefinition') {
202
+ sds.push(resource);
203
+ }
204
+ }
205
+ catch {
206
+ continue;
207
+ }
208
+ }
209
+ }
185
210
  }
186
211
  catch (e) {
187
212
  logger.log(`Failed to read StructureDefinitions from ${packagePath}: ${e.message}`);
@@ -243,8 +268,10 @@ export class PackageManager {
243
268
  const packageJson = JSON.parse(content);
244
269
  if (packageJson.dependencies && typeof packageJson.dependencies === 'object') {
245
270
  for (const [depName, depVersion] of Object.entries(packageJson.dependencies)) {
246
- // Skip hl7.fhir.r4.core and similar base packages - they're too large and not needed
247
- if (depName.startsWith('hl7.fhir.') && depName.includes('.core')) {
271
+ // Skip large HL7 base packages (core, extensions, terminology)
272
+ // they contain thousands of files and are handled via deriveVersionData
273
+ // or on-demand fetching.
274
+ if (isLargeHl7Package(depName)) {
248
275
  continue;
249
276
  }
250
277
  const depRef = `${depName}@${depVersion}`;
@@ -95,7 +95,7 @@ export function resolveWellKnownValueSetCodes(valueSetCodesMap) {
95
95
  function resolveValueSetCompose(visitedDirs, merged) {
96
96
  let resolved = 0;
97
97
  for (const dir of visitedDirs) {
98
- const jsonFiles = findAllJsonFiles(dir);
98
+ const jsonFiles = findJsonFilesByPrefix(dir, 'ValueSet-');
99
99
  for (const file of jsonFiles) {
100
100
  try {
101
101
  const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
@@ -197,6 +197,37 @@ function findAllJsonFiles(dir) {
197
197
  }
198
198
  return results;
199
199
  }
200
+ /**
201
+ * Find JSON files matching a filename prefix (e.g., "StructureDefinition-").
202
+ * FHIR packages name files by resourceType, so this avoids parsing thousands
203
+ * of irrelevant JSON files and dramatically reduces memory usage for large
204
+ * packages like hl7.fhir.r5.core (~2,970 files but only ~307 are SDs).
205
+ *
206
+ * Falls back to returning ALL JSON files when no prefix-matched files are found,
207
+ * because some packages (e.g. KBV) use non-standard naming conventions.
208
+ */
209
+ function findJsonFilesByPrefix(dir, prefix) {
210
+ const prefixed = findJsonFilesMatching(dir, prefix);
211
+ if (prefixed.length > 0)
212
+ return prefixed;
213
+ // Non-standard naming — fall back to all JSON files
214
+ log.debug(`No files matching prefix "${prefix}" in ${dir}, falling back to all JSON files`);
215
+ return findAllJsonFiles(dir);
216
+ }
217
+ function findJsonFilesMatching(dir, prefix) {
218
+ let results = [];
219
+ const list = fs.readdirSync(dir, { withFileTypes: true });
220
+ for (const entry of list) {
221
+ const fullPath = path.join(dir, entry.name);
222
+ if (entry.isDirectory()) {
223
+ results = results.concat(findJsonFilesMatching(fullPath, prefix));
224
+ }
225
+ else if (entry.isFile() && entry.name.endsWith(".json") && entry.name.startsWith(prefix)) {
226
+ results.push(fullPath);
227
+ }
228
+ }
229
+ return results;
230
+ }
200
231
  /** Map FHIR version strings to slugs. */
201
232
  const FHIR_VERSION_TO_SLUG = {
202
233
  '4.0.0': 'r4', '4.0.1': 'r4',
@@ -209,8 +240,9 @@ const FHIR_VERSION_TO_SLUG = {
209
240
  * Returns the detected slug, or undefined if detection fails.
210
241
  */
211
242
  export function detectFhirVersion(extractedRoot) {
212
- const jsonFiles = findAllJsonFiles(extractedRoot);
213
- for (const file of jsonFiles) {
243
+ // Try ImplementationGuide files first (usually just 1)
244
+ const igFiles = findJsonFilesByPrefix(extractedRoot, 'ImplementationGuide-');
245
+ for (const file of igFiles) {
214
246
  try {
215
247
  const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
216
248
  if (content.resourceType === 'ImplementationGuide') {
@@ -229,7 +261,8 @@ export function detectFhirVersion(extractedRoot) {
229
261
  }
230
262
  }
231
263
  // Fallback: check fhirVersion on first StructureDefinition found
232
- for (const file of jsonFiles) {
264
+ const sdFiles = findJsonFilesByPrefix(extractedRoot, 'StructureDefinition-');
265
+ for (const file of sdFiles) {
233
266
  try {
234
267
  const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
235
268
  if (content.resourceType === 'StructureDefinition' && content.fhirVersion) {
@@ -293,7 +326,7 @@ export async function extractPackage(packagePath) {
293
326
  * Reads all StructureDefinition resources from an already extracted package directory.
294
327
  */
295
328
  export function readStructureDefinitionsFromDir(extractedRoot) {
296
- const jsonFiles = findAllJsonFiles(extractedRoot);
329
+ const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'StructureDefinition-');
297
330
  const structureDefinitions = [];
298
331
  for (const file of jsonFiles) {
299
332
  try {
@@ -324,7 +357,7 @@ export async function createPackageFromDir(sourceDir, outFile) {
324
357
  * compose.include entries that reference a whole system without listing concepts.
325
358
  */
326
359
  export function readCodeSystemConceptsFromDir(extractedRoot) {
327
- const jsonFiles = findAllJsonFiles(extractedRoot);
360
+ const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'CodeSystem-');
328
361
  const map = {};
329
362
  for (const file of jsonFiles) {
330
363
  try {
@@ -367,7 +400,7 @@ function flattenCodeSystemConcepts(concepts) {
367
400
  * Each code entry has shape { code, system? } drawn from compose.include or expansion contains.
368
401
  */
369
402
  export function readValueSetCodesFromDir(extractedRoot, codeSystemMap) {
370
- const jsonFiles = findAllJsonFiles(extractedRoot);
403
+ const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'ValueSet-');
371
404
  const map = {};
372
405
  for (const file of jsonFiles) {
373
406
  try {
@@ -529,6 +562,28 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
529
562
  }
530
563
  await walkDeps(extractedRoot);
531
564
  }
565
+ /**
566
+ * Returns true for large HL7 base packages that should be skipped when walking
567
+ * dependency trees eagerly. These packages contain thousands of JSON files and
568
+ * loading them all into memory causes OOM for R5 workloads.
569
+ *
570
+ * Core SDs are derived via `deriveVersionData()` / bundled base definitions.
571
+ * Extensions and terminology resources are fetched on-demand as needed.
572
+ */
573
+ export function isLargeHl7Package(depName) {
574
+ // hl7.terminology.r5, hl7.terminology.r4, etc. — large CodeSystem/ValueSet packages
575
+ if (depName.startsWith('hl7.terminology'))
576
+ return true;
577
+ if (!depName.startsWith('hl7.fhir.'))
578
+ return false;
579
+ // hl7.fhir.r5.core, hl7.fhir.r4.core, etc.
580
+ if (depName.includes('.core'))
581
+ return true;
582
+ // hl7.fhir.uv.extensions.r5, hl7.fhir.uv.extensions.r4, etc.
583
+ if (depName.includes('extensions'))
584
+ return true;
585
+ return false;
586
+ }
532
587
  /**
533
588
  * Reads StructureDefinitions from all dependency packages (recursively).
534
589
  * Returns SDs from dependencies only — the main package SDs are loaded separately.
@@ -560,6 +615,10 @@ export function readStructureDefinitionsFromDependencies(extractedRoot) {
560
615
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
561
616
  const deps = pkgJson.dependencies || {};
562
617
  for (const [depName, depVersion] of Object.entries(deps)) {
618
+ // Skip large HL7 base packages — core SDs are available via deriveVersionData /
619
+ // bundled base SDs, and extensions & terminology are fetched on-demand.
620
+ if (isLargeHl7Package(depName))
621
+ continue;
563
622
  for (const sep of ['@', '#']) {
564
623
  const depDir = path.join(cacheDir, `${depName}${sep}${depVersion}`);
565
624
  if (fs.existsSync(depDir)) {
@@ -608,6 +667,8 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
608
667
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
609
668
  const deps = pkgJson.dependencies || {};
610
669
  for (const [depName, depVersion] of Object.entries(deps)) {
670
+ if (isLargeHl7Package(depName))
671
+ continue;
611
672
  const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
612
673
  if (fs.existsSync(depDir))
613
674
  collectCodeSystems(depDir);
@@ -654,6 +715,8 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
654
715
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
655
716
  const deps = pkgJson.dependencies || {};
656
717
  for (const [depName, depVersion] of Object.entries(deps)) {
718
+ if (isLargeHl7Package(depName))
719
+ continue;
657
720
  // Look for dependency in cache: name@version format
658
721
  const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
659
722
  if (fs.existsSync(depDir)) {
@@ -683,7 +746,7 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
683
746
  * This provides richer metadata than readValueSetCodesFromDir, including type generation capabilities.
684
747
  */
685
748
  export function readValueSetsFromDir(extractedRoot) {
686
- const jsonFiles = findAllJsonFiles(extractedRoot);
749
+ const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'ValueSet-');
687
750
  const map = new Map();
688
751
  for (const file of jsonFiles) {
689
752
  try {
@@ -101,13 +101,12 @@ export async function fetchStructureDefinition(url, fallbackUrl) {
101
101
  url = url.replace(/^https?:\/\/hl7\.org\/fhir\/StructureDefinition\//, `http://hl7.org/fhir/${targetVersion}/StructureDefinition/`);
102
102
  logger.debug(`[fetchSD] Adjusted versionless FHIR URL to ${targetVersion}: ${originalUrl} -> ${url}`);
103
103
  }
104
- // Special case: Base resource should always use resource.profile.json
105
- if (url.includes('/StructureDefinition/Base') && url.includes('hl7.org/fhir')) {
106
- const version = targetVersion;
107
- const baseUrl = url.includes('https://') ? 'https://hl7.org/fhir' : 'http://hl7.org/fhir';
108
- const resourceProfileUrl = `${baseUrl}/${version}/resource.profile.json`;
109
- logger.debug(`[fetchSD] Base resource detected, redirecting to: ${resourceProfileUrl}`);
110
- return fetchStructureDefinition(resourceProfileUrl, resourceProfileUrl);
104
+ // Special case: FHIR "Base" is the absolute root type with no parent.
105
+ // It has no meaningful fields to inherit. Return null to stop recursion.
106
+ // Note: Must match exactly /Base (not /BaseResource, /BaseSomething etc.)
107
+ if (/\/StructureDefinition\/Base$/.test(url) && url.includes('hl7.org/fhir')) {
108
+ logger.debug(`[fetchSD] Base type detected (FHIR root) — returning null to stop recursion`);
109
+ return null;
111
110
  }
112
111
  // First, check if we have this SD registered locally (from input directory)
113
112
  if (localStructureDefinitions.has(url)) {
@@ -9,6 +9,8 @@ const log = logger.withTag('sdParser');
9
9
  // Re-export fetcher functions for backward compatibility
10
10
  export { fetchStructureDefinition, fetchStructureDefinitions, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './sdFetcher.js';
11
11
  const processedBaseDefinitions = new Map(); // Track processed base resources
12
+ // Guard against circular base-definition chains (e.g., Base → Base in R5)
13
+ const _inFlightBaseUrls = new Set();
12
14
  /** Deep-clone an array of Field objects to prevent shared mutable state.
13
15
  * Clones binding, patternConstraint, nestedCodingSlices (with their sub-objects),
14
16
  * and requiredSlices so that mutations on one copy don't affect the other. */
@@ -84,8 +86,14 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
84
86
  // modifying nestedCodingSlices) from bleeding between sibling profiles.
85
87
  baseFields = deepCloneFields(processedBaseDefinitions.get(baseDefinitionUrl) || []);
86
88
  }
89
+ else if (_inFlightBaseUrls.has(baseDefinitionUrl)) {
90
+ // Circular base-definition chain detected (e.g., Base → Base in R5).
91
+ // Treat as root type — no inherited fields.
92
+ log.log(`Circular baseDefinition detected for ${baseDefinitionUrl}, treating as root type.`);
93
+ }
87
94
  else {
88
95
  log.log(`Fetching base resource for StructureDefinition ${structureDefinition.id || "unknown"}: ${baseDefinitionUrl}`);
96
+ _inFlightBaseUrls.add(baseDefinitionUrl);
89
97
  // Only use fhirServerUrl if it's a valid HTTP URL AND it's not a canonical StructureDefinition URL
90
98
  // (fhirServerUrl should be a server base like "https://server.com", not a full StructureDefinition URL)
91
99
  const isValidHttpUrl = fhirServerUrl && (fhirServerUrl.startsWith('http://') || fhirServerUrl.startsWith('https://'));
@@ -138,6 +146,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
138
146
  impact: 'Generated code may have wrong types; validators cannot fully evaluate this profile.',
139
147
  });
140
148
  }
149
+ _inFlightBaseUrls.delete(baseDefinitionUrl);
141
150
  }
142
151
  }
143
152
  const elements = structureDefinition.snapshot?.element || structureDefinition.differential?.element || [];
@@ -6,6 +6,7 @@ import fs from 'fs';
6
6
  import { logger } from '../logger.js';
7
7
  import { generateRandomSupportContent } from './emitters/class/randomSupportGenerator.js';
8
8
  import { generateValidatorOptionsContent } from './emitters/validator/validatorGenerator.js';
9
+ import { ctx } from './fhir/versionContext.js';
9
10
  // ── Fetch failure tracking ─────────────────────────────────────────
10
11
  let failedFetchCount = 0;
11
12
  const failedFetchProfiles = [];
@@ -35,8 +36,13 @@ export function getFetchFailureWarning() {
35
36
  `Note: The .cache folder has been preserved to speed up the next generation attempt.\n`;
36
37
  }
37
38
  // ── FHIR complex type child map ────────────────────────────────────
38
- /** Map of FHIR complex type child field names to their primitive types */
39
- const COMPLEX_TYPE_CHILDREN = {
39
+ /**
40
+ * Static fallback for complex type child fields.
41
+ * Used when ctx().dataTypes is not yet populated (early init) or for types
42
+ * not present in the derived data. The canonical source of truth is
43
+ * ctx().dataTypes which is derived from the real FHIR core SDs.
44
+ */
45
+ const COMPLEX_TYPE_CHILDREN_FALLBACK = {
40
46
  Quantity: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
41
47
  Money: { value: 'decimal', currency: 'code' },
42
48
  Identifier: { use: 'code', system: 'uri', value: 'string' },
@@ -53,11 +59,23 @@ const COMPLEX_TYPE_CHILDREN = {
53
59
  Count: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
54
60
  Distance: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
55
61
  Timing: { event: 'dateTime' },
56
- SampledData: { period: 'decimal', dimensions: 'positiveInt', data: 'string' },
57
62
  };
58
- /** Resolve the type of a child field within a known FHIR complex type */
63
+ /**
64
+ * Resolve the type of a child field within a known FHIR complex type.
65
+ * Prefers the version-derived ctx().dataTypes (which handles cross-version
66
+ * differences like SampledData automatically), falling back to the static map.
67
+ */
59
68
  export function resolveComplexTypeChildType(parentType, childName) {
60
- return COMPLEX_TYPE_CHILDREN[parentType]?.[childName];
69
+ // Primary: version-derived data (works for any FHIR version)
70
+ try {
71
+ const derived = ctx().dataTypes[parentType];
72
+ if (derived?.[childName])
73
+ return derived[childName].type;
74
+ }
75
+ catch {
76
+ // ctx() not yet initialised — fall through to static map
77
+ }
78
+ return COMPLEX_TYPE_CHILDREN_FALLBACK[parentType]?.[childName];
61
79
  }
62
80
  // ── Support file generators ────────────────────────────────────────
63
81
  /** Ensure RandomSupport.ts exists in a generation output directory */
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Lightweight generation-phase timer.
3
+ *
4
+ * Usage:
5
+ * resetTimings();
6
+ * const end = startPhase('fetch');
7
+ * await doWork();
8
+ * end();
9
+ * // … more phases …
10
+ * const summary = getTimingSummary(); // { phases: { fetch: 1234, … }, totalMs: 5678 }
11
+ */
12
+ let _phases = {};
13
+ let _wallStart = 0;
14
+ /** Clear all recorded timings and start the wall clock. */
15
+ export function resetTimings() {
16
+ _phases = {};
17
+ _wallStart = performance.now();
18
+ }
19
+ /**
20
+ * Begin timing a named phase. Returns a callback to call when the phase ends.
21
+ * If the same phase name is used multiple times the durations are summed.
22
+ */
23
+ export function startPhase(name) {
24
+ const t0 = performance.now();
25
+ return () => {
26
+ const elapsed = performance.now() - t0;
27
+ _phases[name] = (_phases[name] ?? 0) + elapsed;
28
+ };
29
+ }
30
+ /** Get the timing summary (all phases + wall-clock total). */
31
+ export function getTimingSummary() {
32
+ return {
33
+ phases: { ...(_phases) },
34
+ totalMs: Math.round(performance.now() - _wallStart),
35
+ };
36
+ }
37
+ /** Format the summary as a human-readable string for console output. */
38
+ export function formatTimingSummary(summary) {
39
+ const fmt = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
40
+ const lines = Object.entries(summary.phases).map(([name, ms]) => ` ${name}: ${fmt(ms)}`);
41
+ lines.push(` total: ${fmt(summary.totalMs)}`);
42
+ return `⏱ Generation timing\n${lines.join('\n')}`;
43
+ }
package/out/src/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { generateIntoPackage, generateForDirectory, generateFromJsonFile, generateFromProfileUrl, resetFetchFailureTracking, getFetchFailureWarning } from "./generator/index.js";
2
+ import { generateIntoPackage, generateForDirectory, generateFromJsonFile, generateFromProfileUrl, resetFetchFailureTracking, getFetchFailureWarning, getTimingSummary, formatTimingSummary } from "./generator/index.js";
3
3
  import { generateClient } from "./generator/emitters/client/clientGenerator.js";
4
4
  import { extractPackage } from "./generator/parser/packageParser.js";
5
5
  import { getCacheConfig, getFhirPackagesCacheDir, clearAllCaches, ensureCacheDir, setCacheConfig } from "./generator/core/cacheConfig.js";
@@ -113,6 +113,10 @@ function completeGeneration(flags, outputDir) {
113
113
  const summary = formatReportSummary(report);
114
114
  if (summary.trim())
115
115
  console.log(summary);
116
+ // Print generation timing summary
117
+ const timing = getTimingSummary();
118
+ if (timing.totalMs > 0)
119
+ console.log(formatTimingSummary(timing));
116
120
  const hasWarnings = showGenerationWarnings();
117
121
  if (!hasWarnings && flags.noCache) {
118
122
  cleanupCache();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",