babelfhir-ts 1.5.21 → 1.6.1

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.
Files changed (57) hide show
  1. package/README.md +6 -3
  2. package/out/src/generator/core/constants.js +2 -1
  3. package/out/src/generator/core/extensionSdReader.js +4 -1
  4. package/out/src/generator/core/utils.js +91 -9
  5. package/out/src/generator/emitters/class/classGenerator.js +14 -8
  6. package/out/src/generator/emitters/class/classGeneratorHelpers.js +22 -9
  7. package/out/src/generator/emitters/class/classHeuristics.js +4 -2
  8. package/out/src/generator/emitters/class/fieldPatternExtractor.js +17 -13
  9. package/out/src/generator/emitters/class/sliceElementDefaults.js +21 -6
  10. package/out/src/generator/emitters/class/sliceElementGenerator.js +3 -3
  11. package/out/src/generator/emitters/interface/backboneSliceTyping.js +21 -13
  12. package/out/src/generator/emitters/interface/importManager.js +1 -1
  13. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +15 -12
  14. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +6 -3
  15. package/out/src/generator/emitters/interface/interfaceGenerator.js +14 -9
  16. package/out/src/generator/emitters/interface/postProcessExtensions.js +32 -16
  17. package/out/src/generator/emitters/interface/processFlatField.js +9 -7
  18. package/out/src/generator/emitters/interface/processNestedField.js +9 -10
  19. package/out/src/generator/emitters/namingsystem/namingSystemGenerator.js +136 -0
  20. package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -15
  21. package/out/src/generator/emitters/prefab/prefabRenderer.js +2 -19
  22. package/out/src/generator/emitters/prefab/prefabTypeMapper.js +2 -0
  23. package/out/src/generator/emitters/validator/closedSlicingValidation.js +5 -2
  24. package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +7 -1
  25. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +27 -12
  26. package/out/src/generator/emitters/validator/sliceDelegation.js +8 -6
  27. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +3 -3
  28. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +41 -41
  29. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +8 -3
  30. package/out/src/generator/emitters/validator/subExtensionScope.js +10 -2
  31. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +4 -3
  32. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +5 -5
  33. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +8 -6
  34. package/out/src/generator/emitters/validator/validatorExpressions.js +44 -0
  35. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +34 -17
  36. package/out/src/generator/emitters/validator/validatorGenerator.js +43 -47
  37. package/out/src/generator/emitters/validator/validatorRuntime.js +13 -2
  38. package/out/src/generator/emitters/validator/validatorTemplates.js +4 -1
  39. package/out/src/generator/emitters/valueset/valueSetGenerator.js +6 -10
  40. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +48 -8
  41. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +51 -24
  42. package/out/src/generator/fhir/corePackageResolver.js +2 -3
  43. package/out/src/generator/fhir/corePackageResolver.ts +2 -3
  44. package/out/src/generator/generationHelpers.js +59 -7
  45. package/out/src/generator/index.js +94 -49
  46. package/out/src/generator/parser/packageManager.js +4 -4
  47. package/out/src/generator/parser/packageParser.js +116 -64
  48. package/out/src/generator/parser/sdFetcher.js +5 -3
  49. package/out/src/generator/parser/sdParser.js +1 -1
  50. package/out/src/generator/parser/sliceAggregator.js +13 -10
  51. package/out/src/generator/parser/sliceAggregatorComposition.js +11 -2
  52. package/out/src/generator/parser/vsParser.js +6 -3
  53. package/out/src/generator/sdProcessor.js +30 -21
  54. package/out/src/generator/sdProcessorRequiredFields.js +3 -2
  55. package/out/src/main.js +5 -0
  56. package/package.json +13 -10
  57. package/parity-matrix.json +6 -0
@@ -8,7 +8,7 @@
8
8
  import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
9
9
  import { getRules, versionSlug } from '../../fhir/versionContext.js';
10
10
  import { getZodPackageTypes } from './zodTypes.js';
11
- import { mergeFields } from '../../core/utils.js';
11
+ import { mergeFields, capitalize, stripSliceSuffix, firstSegment, stripVersionFromCanonicalUrl } from '../../core/utils.js';
12
12
  import { buildRefinements } from './zodRefinementBuilder.js';
13
13
  /** Map FHIR primitive types to Zod schema calls */
14
14
  /** Runtime helpers a refinement may reference, imported from ValidatorOptions. */
@@ -53,7 +53,16 @@ function isPrimitive(fhirType) {
53
53
  * @param baseFields Fields inherited from the base resource
54
54
  * @param valueSets Optional parsed ValueSets for enum generation
55
55
  */
56
- export function generateZodSchema(interfaceName, newFields, baseResource, baseFields = [], valueSets) {
56
+ export function generateZodSchema(interfaceName, newFields, baseResource, baseFields = [], valueSets,
57
+ /**
58
+ * Emit the FHIRPath invariants as refinements.
59
+ *
60
+ * Off by default: each invariant is a compiled FHIRPath evaluation on every
61
+ * parse (~5.5us, so +0.38ms across C4BBOrganization's 69), which is the right
62
+ * trade for conformance checking and the wrong one for per-request validation
63
+ * in a hot path. The caller decides.
64
+ */
65
+ emitInvariants = false) {
57
66
  const lines = [];
58
67
  const imports = new Set();
59
68
  const baseTypeImports = new Set();
@@ -101,8 +110,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
101
110
  : [{ code: field.type || 'string' }];
102
111
  for (const opt of options) {
103
112
  const typeCode = opt.code;
104
- const suffix = typeCode === 'code' ? 'Code'
105
- : typeCode[0].toUpperCase() + typeCode.slice(1);
113
+ const suffix = typeCode === 'code' ? 'Code' : capitalize(typeCode);
106
114
  choiceExpandedProps.add(basePropName + suffix);
107
115
  if (isPrimitive(typeCode)) {
108
116
  choiceExpandedProps.add('_' + basePropName + suffix);
@@ -113,14 +121,22 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
113
121
  // Pre-compute minimum cardinality implied by required slices.
114
122
  // If an array field (e.g., extension) has slices with min>=1, the parent array
115
123
  // must contain at least that many elements even if its own min is 0.
124
+ //
125
+ // Keyed by the slice's full path. Keying by the last segment made every
126
+ // `*.extension:slice` anywhere in the resource count towards the *root*
127
+ // `extension`, so PASClaimBase emitted `z.array(ExtensionSchema).min(3)` for a
128
+ // profile whose Claim.extension slices are all min=0.
129
+ //
130
+ // An unstated `min` counts as 0, not 1. A differential states only what it
131
+ // changes, so a slice that says nothing about cardinality inherits the base,
132
+ // and a slice can only become required by stating min>=1 itself.
116
133
  const sliceMinByParent = new Map();
117
134
  for (const f of allFields) {
118
135
  if (!f.sliceName)
119
136
  continue;
120
- const sliceMin = typeof f.min === 'number' ? f.min : (f.isOptional ? 0 : 1);
137
+ const sliceMin = typeof f.min === 'number' ? f.min : 0;
121
138
  if (sliceMin > 0) {
122
- const parentKey = f.name.split('.').pop();
123
- sliceMinByParent.set(parentKey, (sliceMinByParent.get(parentKey) || 0) + sliceMin);
139
+ sliceMinByParent.set(f.name, (sliceMinByParent.get(f.name) || 0) + sliceMin);
124
140
  }
125
141
  }
126
142
  for (const field of schemaFields) {
@@ -134,8 +150,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
134
150
  const expandedProps = [];
135
151
  for (const opt of options) {
136
152
  const typeCode = opt.code;
137
- const suffix = typeCode === 'code' ? 'Code'
138
- : typeCode[0].toUpperCase() + typeCode.slice(1);
153
+ const suffix = typeCode === 'code' ? 'Code' : capitalize(typeCode);
139
154
  const propName = basePropName + suffix;
140
155
  let variantType = buildZodTypeForCode(typeCode, baseTypeImports);
141
156
  variantType = `${variantType}.optional()`;
@@ -181,7 +196,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
181
196
  if (field.isArray) {
182
197
  let minItems = typeof field.min === 'number' && field.min > 0 ? field.min : 0;
183
198
  // If required slices exist, the parent array needs at least that many items
184
- const sliceImpliedMin = sliceMinByParent.get(fieldName) || 0;
199
+ const sliceImpliedMin = sliceMinByParent.get(field.name) || 0;
185
200
  if (sliceImpliedMin > minItems)
186
201
  minItems = sliceImpliedMin;
187
202
  const maxItems = typeof field.max === 'number' && field.max !== Infinity ? field.max : 0;
@@ -220,7 +235,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
220
235
  }
221
236
  // Make optional or required
222
237
  // A field is required if it has min>=1 OR required slices imply a minimum
223
- const effectiveRequired = !field.isOptional || (sliceMinByParent.get(fieldName) || 0) > 0;
238
+ const effectiveRequired = !field.isOptional || (sliceMinByParent.get(field.name) || 0) > 0;
224
239
  if (!effectiveRequired) {
225
240
  zodType = `${zodType}.optional()`;
226
241
  }
@@ -256,8 +271,8 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
256
271
  // because DataType random() methods don't produce pattern-aware data.
257
272
  const isDataTypeProfile = getZodPackageTypes().has(rootPrefix);
258
273
  const refinementResult = isDataTypeProfile
259
- ? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map() }
260
- : buildRefinements(allFields, rootPrefix, valueSets);
274
+ ? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map(), invariantEvaluators: [] }
275
+ : buildRefinements(allFields, rootPrefix, valueSets, emitInvariants);
261
276
  // Merge ValueSet imports from binding refinements
262
277
  for (const [name, importPath] of refinementResult.valueSetImports) {
263
278
  if (!valueSetImports.has(name)) {
@@ -277,7 +292,18 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
277
292
  const headerIndex = lines.indexOf(`import { z } from "zod";`);
278
293
  lines.splice(headerIndex + 1, 0, `import { ${usedHelpers.join(', ')} } from "./ValidatorOptions.js";`);
279
294
  }
295
+ // Invariant refinements evaluate FHIRPath synchronously, so the engine and the
296
+ // version's model have to be imported alongside the schema.
297
+ if (refinementResult.invariantEvaluators.length > 0) {
298
+ const headerIndex = lines.indexOf(`import { z } from "zod";`);
299
+ lines.splice(headerIndex + 1, 0, `import fhirpath from "fhirpath";`, `import fhirpath_model from "fhirpath/fhir-context/${versionSlug()}/index.js";`);
300
+ }
280
301
  lines.push('');
302
+ // Compiled invariant evaluators, hoisted above the schema so each FHIRPath
303
+ // expression is parsed once at import instead of on every parse.
304
+ if (refinementResult.invariantEvaluators.length > 0) {
305
+ lines.push(...refinementResult.invariantEvaluators, '');
306
+ }
281
307
  // Emit schema
282
308
  const schemaName = `${interfaceName}Schema`;
283
309
  lines.push(`export const ${schemaName} =`);
@@ -285,7 +311,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
285
311
  // Deduplicate properties by name (keep first occurrence)
286
312
  const seenTopProps = new Set();
287
313
  for (const prop of properties) {
288
- const propName = prop.trim().split(':')[0].trim();
314
+ const propName = stripSliceSuffix(prop.trim()).trim();
289
315
  if (seenTopProps.has(propName))
290
316
  continue;
291
317
  seenTopProps.add(propName);
@@ -346,9 +372,10 @@ function buildZodType(field, valueSets, valueSetImports, complexTypeImports, bas
346
372
  return `z.enum([${codes.join(', ')}])`;
347
373
  }
348
374
  // Check for ValueSet binding via parsed ValueSets
349
- if (!isComplexBoundType && field.binding?.uri && valueSets) {
350
- const vsUri = field.binding.uri.split('|')[0];
351
- const vs = valueSets.get(vsUri) || valueSets.get(field.binding.uri);
375
+ const bindingUri = field.binding?.uri;
376
+ if (!isComplexBoundType && bindingUri && valueSets) {
377
+ const vsUri = stripVersionFromCanonicalUrl(bindingUri);
378
+ const vs = valueSets.get(vsUri) || valueSets.get(bindingUri);
352
379
  if (vs && vs.concepts.length > 0 && vs.concepts.length <= 50) {
353
380
  const sanitizedName = sanitizeValueSetName(vs.name);
354
381
  const vsCodeType = `${sanitizedName}Code`;
@@ -423,8 +450,7 @@ function buildInlineBackboneSchema(allFields, parentPath, valueSets, valueSetImp
423
450
  : [{ code: child.type || 'string' }];
424
451
  for (const opt of options) {
425
452
  const typeCode = opt.code;
426
- const suffix = typeCode === 'code' ? 'Code'
427
- : typeCode[0].toUpperCase() + typeCode.slice(1);
453
+ const suffix = typeCode === 'code' ? 'Code' : capitalize(typeCode);
428
454
  choiceExpandedProps.add(basePropName + suffix);
429
455
  if (isPrimitive(typeCode)) {
430
456
  choiceExpandedProps.add('_' + basePropName + suffix);
@@ -444,8 +470,7 @@ function buildInlineBackboneSchema(allFields, parentPath, valueSets, valueSetImp
444
470
  : [{ code: child.type || 'string' }];
445
471
  for (const opt of options) {
446
472
  const typeCode = opt.code;
447
- const suffix = typeCode === 'code' ? 'Code'
448
- : typeCode[0].toUpperCase() + typeCode.slice(1);
473
+ const suffix = typeCode === 'code' ? 'Code' : capitalize(typeCode);
449
474
  const propName = basePropName + suffix;
450
475
  let variantType = buildZodTypeForCode(typeCode, baseTypeImports);
451
476
  variantType = `${variantType}.optional()`;
@@ -493,7 +518,7 @@ function buildInlineBackboneSchema(allFields, parentPath, valueSets, valueSetImp
493
518
  // Deduplicate properties by name (keep first occurrence)
494
519
  const seenProps = new Set();
495
520
  const uniqueProps = props.filter(p => {
496
- const name = p.split(':')[0].trim();
521
+ const name = stripSliceSuffix(p).trim();
497
522
  if (seenProps.has(name))
498
523
  return false;
499
524
  seenProps.add(name);
@@ -507,7 +532,9 @@ function getRootPrefix(fields, baseResource, interfaceName) {
507
532
  // Find the most common first segment
508
533
  const counts = new Map();
509
534
  for (const f of fields) {
510
- const root = f.name.split('.')[0];
535
+ const root = firstSegment(f.name);
536
+ if (!root)
537
+ continue;
511
538
  counts.set(root, (counts.get(root) || 0) + 1);
512
539
  }
513
540
  // Sort by count, prefer baseResource if tied
@@ -524,7 +551,7 @@ function getRootPrefix(fields, baseResource, interfaceName) {
524
551
  /** Sanitize a field name for use as an object property */
525
552
  function sanitizePropertyName(name) {
526
553
  // Strip slice suffix (e.g. "category:VSCat" -> "category")
527
- const clean = name.split(':')[0];
554
+ const clean = stripSliceSuffix(name);
528
555
  // If the name is a JS reserved word or starts with a digit, quote it
529
556
  if (/^\d/.test(clean) || JS_RESERVED.has(clean)) {
530
557
  return `"${clean}"`;
@@ -223,7 +223,7 @@ function collectDataTypeFields(sd, typeName, dataTypes) {
223
223
  if (parts.length !== 2)
224
224
  continue;
225
225
  const fieldName = parts[1];
226
- if (INFRA_FIELDS.has(fieldName))
226
+ if (!fieldName || INFRA_FIELDS.has(fieldName))
227
227
  continue;
228
228
  const typeCode = el.type?.[0]?.code;
229
229
  if (!typeCode)
@@ -260,8 +260,7 @@ function collectBackboneInterfaces(sd, rootType, interfaceNames) {
260
260
  const parts = id.split('.');
261
261
  // Skip the root type segment (parts[0])
262
262
  let name = rootType;
263
- for (let i = 1; i < parts.length; i++) {
264
- const seg = parts[i];
263
+ for (const seg of parts.slice(1)) {
265
264
  name += seg.charAt(0).toUpperCase() + seg.slice(1);
266
265
  }
267
266
  interfaceNames.push(name);
@@ -272,7 +272,7 @@ function collectDataTypeFields(
272
272
  if (parts.length !== 2) continue;
273
273
 
274
274
  const fieldName = parts[1];
275
- if (INFRA_FIELDS.has(fieldName)) continue;
275
+ if (!fieldName || INFRA_FIELDS.has(fieldName)) continue;
276
276
 
277
277
  const typeCode = el.type?.[0]?.code;
278
278
  if (!typeCode) continue;
@@ -315,8 +315,7 @@ function collectBackboneInterfaces(
315
315
  const parts = id.split('.');
316
316
  // Skip the root type segment (parts[0])
317
317
  let name = rootType;
318
- for (let i = 1; i < parts.length; i++) {
319
- const seg = parts[i];
318
+ for (const seg of parts.slice(1)) {
320
319
  name += seg.charAt(0).toUpperCase() + seg.slice(1);
321
320
  }
322
321
 
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import fs from 'fs';
7
7
  import { expandValueSetsFromTx, addPlaceholdersForExternalBindings } from './parser/vsParser.js';
8
8
  import { generateValueSetTypeScript, generateValueSetRegistry, assignValueSetNames } from './emitters/valueset/valueSetGenerator.js';
9
+ import { generateNamingSystemTypeScript, generateNamingSystemRegistry, assignNamingSystemNames } from './emitters/namingsystem/namingSystemGenerator.js';
9
10
  import { collectValueSetBindingUrls, clearLocalStructureDefinitions } from './parser/sdParser.js';
10
11
  import { ensureDirectoryExists, sanitizeIdentifier, toPascalCase } from './core/utils.js';
11
12
  import { getCacheConfig, getFhirPackagesCacheDir } from './core/cacheConfig.js';
@@ -183,6 +184,12 @@ export async function expandValueSetsWithTx(valueSets, valueSetCodesMap, structu
183
184
  * Returns whether any files were generated. Handles stale directory cleanup.
184
185
  */
185
186
  export function emitValueSetFiles(valueSets, outputDir) {
187
+ const generated = writeValueSetFiles(valueSets, outputDir);
188
+ if (!generated)
189
+ cleanupStaleSubdir(outputDir, 'valuesets');
190
+ return generated;
191
+ }
192
+ function writeValueSetFiles(valueSets, outputDir) {
186
193
  if (valueSets.size === 0)
187
194
  return false;
188
195
  const valueSetOutputDir = path.join(outputDir, 'valuesets');
@@ -282,17 +289,62 @@ export function enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingU
282
289
  logger.log(`ValueSet enrichment: ${enrichedCount} enriched, ${createdCount} created from CodeSystem resolution`);
283
290
  }
284
291
  }
285
- /** Remove stale valuesets directory if it exists and no ValueSets were generated. */
286
- export function cleanupStaleValueSetDir(outputDir) {
287
- const vdir = path.join(outputDir, 'valuesets');
292
+ /**
293
+ * Generate TypeScript files for NamingSystems and write them to disk.
294
+ *
295
+ * Mirrors {@link emitValueSetFiles}: an IG's declared identifier systems become
296
+ * typed constants rather than literals retyped by every consumer.
297
+ * Returns whether any files were generated. Handles stale directory cleanup.
298
+ */
299
+ export function emitNamingSystemFiles(namingSystems, outputDir) {
300
+ const generated = writeNamingSystemFiles(namingSystems, outputDir);
301
+ if (!generated)
302
+ cleanupStaleSubdir(outputDir, 'namingsystems');
303
+ return generated;
304
+ }
305
+ function writeNamingSystemFiles(namingSystems, outputDir) {
306
+ if (namingSystems.length === 0)
307
+ return false;
308
+ const nsOutputDir = path.join(outputDir, 'namingsystems');
309
+ let generatedCount = 0;
310
+ const assigned = assignNamingSystemNames(namingSystems);
311
+ for (const { namingSystem, sanitized } of assigned) {
312
+ try {
313
+ if (generatedCount === 0) {
314
+ ensureDirectoryExists(nsOutputDir);
315
+ }
316
+ const { filename, content } = generateNamingSystemTypeScript(namingSystem, { nameOverride: sanitized });
317
+ fs.writeFileSync(path.join(nsOutputDir, filename), content, 'utf-8');
318
+ generatedCount++;
319
+ }
320
+ catch (err) {
321
+ log.error(`Failed to generate NamingSystem ${namingSystem.name}:`, err);
322
+ }
323
+ }
324
+ if (generatedCount === 0)
325
+ return false;
326
+ const registryContent = generateNamingSystemRegistry(namingSystems);
327
+ fs.writeFileSync(path.join(nsOutputDir, 'index.ts'), registryContent, 'utf-8');
328
+ logger.log(`Generated ${generatedCount} NamingSystem files in ${nsOutputDir}`);
329
+ return true;
330
+ }
331
+ /**
332
+ * Remove a generated subdirectory left over from an earlier run.
333
+ *
334
+ * A cached extraction can keep a `valuesets/`/`namingsystems/` folder whose
335
+ * source resources are gone; leaving it behind makes the barrel export a module
336
+ * nothing regenerates.
337
+ */
338
+ function cleanupStaleSubdir(outputDir, subdir) {
339
+ const dir = path.join(outputDir, subdir);
288
340
  try {
289
- if (fs.existsSync(vdir)) {
290
- fs.rmSync(vdir, { recursive: true, force: true });
291
- log.debug(`Removed stale ValueSets directory: ${vdir}`);
341
+ if (fs.existsSync(dir)) {
342
+ fs.rmSync(dir, { recursive: true, force: true });
343
+ log.debug(`Removed stale ${subdir} directory: ${dir}`);
292
344
  }
293
345
  }
294
346
  catch (e) {
295
- log.debug(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
347
+ log.debug(`Failed to remove stale ${subdir} directory ${dir}: ${e.message}`);
296
348
  }
297
349
  }
298
350
  /** Initialize logger and clear local StructureDefinitions for a generation run. */
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
- import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded, readSearchParametersFromDir } from './parser/packageParser.js';
4
+ import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded, readSearchParametersFromDir, readNamingSystemsFromDir } from './parser/packageParser.js';
5
5
  import { fetchStructureDefinitions, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './parser/sdParser.js';
6
6
  import { ensureDirectoryExists, downloadFile } from './core/utils.js';
7
7
  import { getFhirPackagesCacheDir } from './core/cacheConfig.js';
@@ -13,7 +13,7 @@ import { DEFAULT_FHIR_VERSION } from './fhir/types.js';
13
13
  import { FHIR_VERSIONS } from './fhir/versionRegistry.js';
14
14
  import { ensureCorePackage } from './fhir/corePackageResolver.js';
15
15
  import { spawn } from 'child_process';
16
- import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, cleanupStaleValueSetDir, initGenerationContext, enrichValueSetsFromCodeMap, collectReferencedDependencyProfiles, } from './generationHelpers.js';
16
+ import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, emitNamingSystemFiles, initGenerationContext, enrichValueSetsFromCodeMap, collectReferencedDependencyProfiles, } from './generationHelpers.js';
17
17
  /** Resolve the babelfhir-ts CLI version from its own package.json (used to stamp generated packages). */
18
18
  const __generatorDir = path.dirname(fileURLToPath(import.meta.url));
19
19
  const _generatorPkgPath = [
@@ -218,7 +218,62 @@ async function compileTypeScriptToJS(dir) {
218
218
  });
219
219
  });
220
220
  }
221
- /** Generate index.ts that exports all generated interfaces and classes */
221
+ /**
222
+ * Output directories that only some generation runs produce.
223
+ *
224
+ * Single source of truth for both the generated `package.json` exports map and
225
+ * the root index barrel. These were three separate lists — two identical
226
+ * `exports` blocks plus a hardcoded check in the barrel writer — so registering
227
+ * an emitter meant remembering all three, and reaching a second entry module
228
+ * meant naming it literally (`./dicomweb/cornerstone`). `modules` covers any
229
+ * module a folder emits, so nothing here names one.
230
+ */
231
+ const OPTIONAL_OUTPUT_DIRS = [
232
+ { dir: 'fhir-client', barrel: true },
233
+ // dicomweb writes a second entry module (cornerstone) beside its barrel.
234
+ { dir: 'dicomweb', barrel: true, modules: true },
235
+ { dir: 'prefab', barrel: true },
236
+ { dir: 'valuesets', modules: true, rootReExport: true },
237
+ { dir: 'namingsystems', modules: true, rootReExport: true },
238
+ ];
239
+ /**
240
+ * Add subpath exports for the optional output directories that were created.
241
+ *
242
+ * Shared by generateIntoPackage() and generateIntoPackageDirect(), which
243
+ * previously carried identical copies of this block.
244
+ */
245
+ function addOptionalSubpathExports(outputDir, generatedExports) {
246
+ for (const { dir, barrel, modules } of OPTIONAL_OUTPUT_DIRS) {
247
+ if (!fs.existsSync(path.join(outputDir, dir)))
248
+ continue;
249
+ if (barrel) {
250
+ generatedExports[`./${dir}`] = { types: `./${dir}/index.d.ts`, import: `./${dir}/index.js` };
251
+ }
252
+ if (modules) {
253
+ generatedExports[`./${dir}/*`] = { types: `./${dir}/*.d.ts`, import: `./${dir}/*.js` };
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Put the extraction root's `package.json` back the way it was found.
259
+ *
260
+ * The generated manifest is staged there only so `createPackageFromDir` puts it
261
+ * at the tarball root; it must not outlive the repack when the extraction root
262
+ * is a shared cache entry.
263
+ */
264
+ export function restoreRootManifest(rootPackageJsonPath, prior) {
265
+ try {
266
+ if (prior) {
267
+ fs.writeFileSync(rootPackageJsonPath, prior);
268
+ }
269
+ else {
270
+ fs.rmSync(rootPackageJsonPath, { force: true });
271
+ }
272
+ }
273
+ catch (err) {
274
+ log.warn(`Could not restore ${rootPackageJsonPath}: ${err.message}`);
275
+ }
276
+ }
222
277
  async function generateIndexFile(dir) {
223
278
  const files = fs.readdirSync(dir, { withFileTypes: true });
224
279
  const exports = [];
@@ -233,8 +288,10 @@ async function generateIndexFile(dir) {
233
288
  const content = fs.readFileSync(filePath, 'utf-8');
234
289
  const names = new Map();
235
290
  const exportMatches = content.matchAll(/export\s+(interface|class|type|const|function)\s+(\w+)/g);
236
- for (const match of exportMatches) {
237
- names.set(match[2], match[1] === 'interface' || match[1] === 'type' ? 'type' : 'value');
291
+ for (const [, kind, name] of exportMatches) {
292
+ if (!name)
293
+ continue;
294
+ names.set(name, kind === 'interface' || kind === 'type' ? 'type' : 'value');
238
295
  }
239
296
  fileExports.set(file.name, names);
240
297
  }
@@ -279,10 +336,11 @@ async function generateIndexFile(dir) {
279
336
  }
280
337
  }
281
338
  }
282
- // Also export from valuesets subfolder if it exists
283
- const valueSetsDir = path.join(dir, 'valuesets');
284
- if (fs.existsSync(valueSetsDir)) {
285
- exports.push(`export * from './valuesets/index.js';`);
339
+ // Re-export the optional subfolders that opt into it (see OPTIONAL_OUTPUT_DIRS)
340
+ for (const { dir: subdir } of OPTIONAL_OUTPUT_DIRS.filter(d => d.rootReExport)) {
341
+ if (fs.existsSync(path.join(dir, subdir))) {
342
+ exports.push(`export * from './${subdir}/index.js';`);
343
+ }
286
344
  }
287
345
  // Export shared ValidatorOptions type
288
346
  if (fs.existsSync(path.join(dir, 'ValidatorOptions.ts'))) {
@@ -450,10 +508,8 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
450
508
  ensureDirectoryExists(outputDir);
451
509
  // Generate TypeScript files for ValueSets inside the embedded package
452
510
  endPhase = startPhase('generate');
453
- const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir);
454
- if (!didGenerateValueSets) {
455
- cleanupStaleValueSetDir(outputDir);
456
- }
511
+ emitValueSetFiles(valueSets, outputDir);
512
+ emitNamingSystemFiles(readNamingSystemsFromDir(extractedRoot), outputDir);
457
513
  // Build profile registries from BOTH main and dependency SDs so delegation can resolve names
458
514
  const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allSDs, fhirInterfaceNames);
459
515
  // Register all local StructureDefinitions for resolution before HTTP fetches
@@ -543,20 +599,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
543
599
  const { finalizePrefabOutput } = await import('./emitters/prefab/prefabEmitter.js');
544
600
  finalizePrefabOutput(outputDir, { prefabStylesPath: flags.prefabStyles });
545
601
  }
546
- // Only add subpath exports if their directories were actually created
547
- if (fs.existsSync(path.join(outputDir, 'fhir-client'))) {
548
- generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
549
- }
550
- if (fs.existsSync(path.join(outputDir, 'dicomweb'))) {
551
- generatedExports['./dicomweb'] = { types: './dicomweb/index.d.ts', import: './dicomweb/index.js' };
552
- generatedExports['./dicomweb/cornerstone'] = { types: './dicomweb/cornerstone.d.ts', import: './dicomweb/cornerstone.js' };
553
- }
554
- if (fs.existsSync(path.join(outputDir, 'prefab'))) {
555
- generatedExports['./prefab'] = { types: './prefab/index.d.ts', import: './prefab/index.js' };
556
- }
557
- if (fs.existsSync(path.join(outputDir, 'valuesets'))) {
558
- generatedExports['./valuesets/*'] = { types: './valuesets/*.d.ts', import: './valuesets/*.js' };
559
- }
602
+ addOptionalSubpathExports(outputDir, generatedExports);
560
603
  const generatedPackageJsonPath = path.join(outputDir, 'package.json');
561
604
  fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
562
605
  logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
@@ -611,14 +654,30 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
611
654
  fs.rmdirSync(nodeModulesDir);
612
655
  }
613
656
  catch { /* not empty or doesn't exist — fine */ }
614
- // Copy package.json to root of extractedRoot so it's at the tarball root
657
+ // Copy package.json to root of extractedRoot so it's at the tarball root.
658
+ //
659
+ // extractedRoot is the shared FHIR cache entry whenever the package was
660
+ // already cached (see shouldCleanup), so this file is not ours to keep: a
661
+ // package installed flat — no `package/` subdir, as `babelfhir-ts install`
662
+ // produces for a locally built IG — keeps its own manifest right here, and
663
+ // leaving the generated one behind makes the next run read `@types/fhir`
664
+ // and friends as FHIR package specs. Restore the previous state once the
665
+ // archive is written.
615
666
  const rootPackageJsonPath = path.join(extractedRoot, 'package.json');
667
+ const priorRootManifest = fs.existsSync(rootPackageJsonPath)
668
+ ? fs.readFileSync(rootPackageJsonPath)
669
+ : undefined;
616
670
  fs.copyFileSync(generatedPackageJsonPath, rootPackageJsonPath);
617
671
  logger.log('Copied package.json to tarball root');
618
672
  const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
619
673
  endPhase = startPhase('repack');
620
- await createPackageFromDir(extractedRoot, finalArchive);
621
- endPhase(); // repack
674
+ try {
675
+ await createPackageFromDir(extractedRoot, finalArchive);
676
+ }
677
+ finally {
678
+ endPhase(); // repack
679
+ restoreRootManifest(rootPackageJsonPath, priorRootManifest);
680
+ }
622
681
  // Write timing summary to output directory
623
682
  const timing = getTimingSummary();
624
683
  fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
@@ -692,9 +751,8 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
692
751
  }
693
752
  ensureDirectoryExists(outputDir);
694
753
  endPhase = startPhase('generate');
695
- const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir);
696
- if (!didGenerateValueSets)
697
- cleanupStaleValueSetDir(outputDir);
754
+ emitValueSetFiles(valueSets, outputDir);
755
+ emitNamingSystemFiles(readNamingSystemsFromDir(extractedRoot), outputDir);
698
756
  const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allSDs, fhirInterfaceNames);
699
757
  registerLocalStructureDefinitions(allSDs);
700
758
  // PackageManager already loaded and registered dependency SDs above —
@@ -775,19 +833,7 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
775
833
  finalizePrefabOutput(outputDir, { prefabStylesPath: flags.prefabStyles });
776
834
  }
777
835
  // Only add subpath exports if their directories were actually created
778
- if (fs.existsSync(path.join(outputDir, 'fhir-client'))) {
779
- generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
780
- }
781
- if (fs.existsSync(path.join(outputDir, 'dicomweb'))) {
782
- generatedExports['./dicomweb'] = { types: './dicomweb/index.d.ts', import: './dicomweb/index.js' };
783
- generatedExports['./dicomweb/cornerstone'] = { types: './dicomweb/cornerstone.d.ts', import: './dicomweb/cornerstone.js' };
784
- }
785
- if (fs.existsSync(path.join(outputDir, 'prefab'))) {
786
- generatedExports['./prefab'] = { types: './prefab/index.d.ts', import: './prefab/index.js' };
787
- }
788
- if (fs.existsSync(path.join(outputDir, 'valuesets'))) {
789
- generatedExports['./valuesets/*'] = { types: './valuesets/*.d.ts', import: './valuesets/*.js' };
790
- }
836
+ addOptionalSubpathExports(outputDir, generatedExports);
791
837
  const generatedPackageJsonPath = path.join(outputDir, 'package.json');
792
838
  fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
793
839
  logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
@@ -918,9 +964,8 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
918
964
  }
919
965
  // Generate TypeScript files for ValueSets
920
966
  endPhase = startPhase('generate');
921
- if (!emitValueSetFiles(valueSets, outputDir)) {
922
- cleanupStaleValueSetDir(outputDir);
923
- }
967
+ emitValueSetFiles(valueSets, outputDir);
968
+ emitNamingSystemFiles(readNamingSystemsFromDir(inputDir), outputDir);
924
969
  const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allLocalSDs, fhirInterfaceNames);
925
970
  // Register all local StructureDefinitions for resolution before HTTP fetches
926
971
  registerLocalStructureDefinitions(allLocalSDs);
@@ -30,8 +30,8 @@ export class PackageManager {
30
30
  parsePackageReference(ref) {
31
31
  // Handle # separator (FHIR package format)
32
32
  if (ref.includes('#')) {
33
- const [name, version] = ref.split('#');
34
- return { name, version, original: ref };
33
+ const hashIndex = ref.indexOf('#');
34
+ return { name: ref.substring(0, hashIndex), version: ref.substring(hashIndex + 1), original: ref };
35
35
  }
36
36
  // Handle @ separator (npm format)
37
37
  if (ref.includes('@') && !ref.startsWith('@')) {
@@ -111,11 +111,11 @@ export class PackageManager {
111
111
  return closestHigher.pkg;
112
112
  }
113
113
  // No higher version, return highest available
114
- return sortedByVersion[sortedByVersion.length - 1]?.pkg || matchingPackages[0];
114
+ return sortedByVersion[sortedByVersion.length - 1]?.pkg ?? matchingPackages[0] ?? null;
115
115
  }
116
116
  // Return the first (or only) matching package
117
117
  // Could be enhanced to prefer stable versions, latest, etc.
118
- return matchingPackages[0];
118
+ return matchingPackages[0] ?? null;
119
119
  }
120
120
  /**
121
121
  * Find all packages that match a pattern (supports wildcards)