babelfhir-ts 1.6.0 → 1.6.2

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 (39) hide show
  1. package/out/src/generator/core/constants.js +2 -1
  2. package/out/src/generator/core/extensionSdReader.js +2 -0
  3. package/out/src/generator/core/utils.js +22 -9
  4. package/out/src/generator/emitters/class/classGenerator.js +14 -8
  5. package/out/src/generator/emitters/class/classGeneratorHelpers.js +12 -7
  6. package/out/src/generator/emitters/class/classHeuristics.js +4 -2
  7. package/out/src/generator/emitters/class/fieldPatternExtractor.js +17 -13
  8. package/out/src/generator/emitters/class/sliceElementDefaults.js +11 -5
  9. package/out/src/generator/emitters/class/sliceElementGenerator.js +3 -3
  10. package/out/src/generator/emitters/interface/backboneSliceTyping.js +21 -13
  11. package/out/src/generator/emitters/interface/importManager.js +1 -1
  12. package/out/src/generator/emitters/interface/interfaceGenerator.js +14 -9
  13. package/out/src/generator/emitters/interface/postProcessExtensions.js +24 -14
  14. package/out/src/generator/emitters/interface/processFlatField.js +9 -7
  15. package/out/src/generator/emitters/interface/processNestedField.js +5 -4
  16. package/out/src/generator/emitters/prefab/prefabTypeMapper.js +2 -0
  17. package/out/src/generator/emitters/validator/sliceDelegation.js +8 -6
  18. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +3 -3
  19. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +26 -14
  20. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +3 -2
  21. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +4 -3
  22. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +5 -5
  23. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +8 -6
  24. package/out/src/generator/emitters/validator/validatorGenerator.js +12 -7
  25. package/out/src/generator/emitters/validator/validatorTemplates.js +4 -1
  26. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +9 -5
  27. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +15 -16
  28. package/out/src/generator/fhir/corePackageResolver.js +2 -3
  29. package/out/src/generator/fhir/corePackageResolver.ts +2 -3
  30. package/out/src/generator/index.js +4 -2
  31. package/out/src/generator/parser/packageManager.js +4 -4
  32. package/out/src/generator/parser/packageParser.js +40 -64
  33. package/out/src/generator/parser/sdFetcher.js +5 -3
  34. package/out/src/generator/parser/sdParser.js +1 -1
  35. package/out/src/generator/parser/sliceAggregator.js +13 -10
  36. package/out/src/generator/parser/sliceAggregatorComposition.js +11 -2
  37. package/out/src/generator/parser/vsParser.js +3 -2
  38. package/out/src/generator/sdProcessorRequiredFields.js +3 -2
  39. package/package.json +2 -2
@@ -6,6 +6,7 @@
6
6
  * maintenance trivial.
7
7
  */
8
8
  import { createRequire } from 'module';
9
+ import { stripVersionFromCanonicalUrl } from './utils.js';
9
10
  import { fileURLToPath } from 'url';
10
11
  import path from 'path';
11
12
  const require = createRequire(import.meta.url);
@@ -244,7 +245,7 @@ export const VALUESET_DEFAULTS = {
244
245
  */
245
246
  export function resolveValueSetDefault(bindingUri) {
246
247
  // Strip version suffix: "http://...ValueSet/foo|4.0.1" → "http://...ValueSet/foo"
247
- const bare = bindingUri.split('|')[0];
248
+ const bare = stripVersionFromCanonicalUrl(bindingUri);
248
249
  return VALUESET_DEFAULTS[bare];
249
250
  }
250
251
  export const FALLBACK_CODES = {
@@ -70,6 +70,8 @@ export function collectSubExtensionSlices(els) {
70
70
  if (!match)
71
71
  continue;
72
72
  const sliceName = el.sliceName ?? match[1];
73
+ if (!sliceName)
74
+ continue;
73
75
  const urlEl = byId.get(`Extension.extension:${sliceName}.url`);
74
76
  const url = typeof urlEl?.fixedUri === 'string' ? urlEl.fixedUri : undefined;
75
77
  if (!url)
@@ -28,6 +28,16 @@ export function stripSliceSuffix(path) {
28
28
  const colonIndex = path.indexOf(':');
29
29
  return colonIndex >= 0 ? path.substring(0, colonIndex) : path;
30
30
  }
31
+ /**
32
+ * The slice name after the colon: `extension:Stadtteil` -> `Stadtteil`.
33
+ *
34
+ * Undefined when the segment carries no slice, which is the distinction callers
35
+ * act on — the counterpart to {@link stripSliceSuffix}, which returns the base.
36
+ */
37
+ export function sliceSuffix(segment) {
38
+ const colonIndex = segment.indexOf(':');
39
+ return colonIndex >= 0 ? segment.substring(colonIndex + 1) : undefined;
40
+ }
31
41
  /**
32
42
  * The first dot-separated segment of a path: `code.coding.system` -> `code`.
33
43
  *
@@ -123,7 +133,9 @@ export function getResourceTypes(options) {
123
133
  const exportRegex = /export\s+(?:\*|{[^}]+})\s+from\s+['"]\.\/([\w-]+)\.js['"]/g;
124
134
  let match;
125
135
  while ((match = exportRegex.exec(indexContent)) !== null) {
126
- exportedTypes.add(match[1]);
136
+ const moduleName = match[1];
137
+ if (moduleName)
138
+ exportedTypes.add(moduleName);
127
139
  }
128
140
  }
129
141
  for (const file of files) {
@@ -136,9 +148,9 @@ export function getResourceTypes(options) {
136
148
  if (actualFile) {
137
149
  const content = fs.readFileSync(actualFile, "utf-8");
138
150
  // Check if it has resourceType property (base resource)
139
- const resourceTypeMatch = content.match(/resourceType:\s*["'](\w+)["']/);
140
- if (resourceTypeMatch) {
141
- resourceTypes.set(profileName, resourceTypeMatch[1]);
151
+ const declaredResourceType = content.match(/resourceType:\s*["'](\w+)["']/)?.[1];
152
+ if (declaredResourceType) {
153
+ resourceTypes.set(profileName, declaredResourceType);
142
154
  continue;
143
155
  }
144
156
  // Check if it extends a FHIR resource (handles both `extends Foo` and `extends Omit<Foo, ...>`)
@@ -163,15 +175,16 @@ export function getBaseResourceType(profileName, outputDir) {
163
175
  }
164
176
  const content = fs.readFileSync(actualFile, "utf-8");
165
177
  // Check for explicit resourceType property
166
- const resourceTypeMatch = content.match(/resourceType:\s*["'](\w+)["']/);
167
- if (resourceTypeMatch) {
168
- return resourceTypeMatch[1];
178
+ const declaredResourceType = content.match(/resourceType:\s*["'](\w+)["']/)?.[1];
179
+ if (declaredResourceType) {
180
+ return declaredResourceType;
169
181
  }
170
182
  // Check for extends clause (handles both `extends Foo` and `extends Omit<Foo, ...>`)
171
183
  const baseTypes = extractBaseTypes(content);
172
- if (baseTypes.length > 0) {
184
+ const [firstBase] = baseTypes;
185
+ if (firstBase) {
173
186
  // Prefer a known resource type, fall back to the first candidate
174
- return baseTypes.find(bt => ctx().resourceNames.includes(bt)) ?? baseTypes[0];
187
+ return baseTypes.find(bt => ctx().resourceNames.includes(bt)) ?? firstBase;
175
188
  }
176
189
  return profileName; // fallback
177
190
  }
@@ -1,4 +1,5 @@
1
1
  import { ctx } from '../../fhir/versionContext.js';
2
+ import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
2
3
  import { TS, } from '../../core/constants.js';
3
4
  import { primitivePlaceholder, generateCodeDefault, makeCodeableConcept, chooseChoiceVariant, applyRequiredFieldHeuristic, deriveIdentifierConfig, } from './classHeuristics.js';
4
5
  import { generateSliceElement, buildNestedRequirementsObject } from './sliceElementGenerator.js';
@@ -187,7 +188,11 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
187
188
  .filter((s) => typeof s === 'string' && s.length > 0);
188
189
  const uniqueSystems = Array.from(new Set(systems));
189
190
  const singleSystem = uniqueSystems.length === 1 ? JSON.stringify(uniqueSystems[0]) : undefined;
191
+ // bindingCodes is non-empty per the guard above, but indexing does not say
192
+ // so; without a code there is no skeleton value to emit.
190
193
  const primary = rf.bindingCodes[0];
194
+ if (!primary)
195
+ continue;
191
196
  if (rf.type === 'CodeableConcept') {
192
197
  const expr = singleSystem
193
198
  ? `skeletonCodeableConcept(${singleSystem}, ${codesArray})`
@@ -349,8 +354,8 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
349
354
  }
350
355
  }
351
356
  // Check profile URLs for known coding profiles
352
- if (slice.profileUrls && slice.profileUrls.length > 0) {
353
- const profileUrl = slice.profileUrls[0];
357
+ const profileUrl = slice.profileUrls?.[0];
358
+ if (profileUrl) {
354
359
  const profileUrlLower = profileUrl.toLowerCase();
355
360
  // ISiK SNOMED-CT coding profile
356
361
  if (profileUrlLower.includes('snomedctcoding') || profileUrlLower.includes('snomed')) {
@@ -376,7 +381,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
376
381
  // code from a different context and should be deferred to runtime resolution.
377
382
  const sliceHasOwnBinding = !!(slice.binding?.uri && slice.binding.uri.length > 0);
378
383
  const sliceBindingMatchesParent = !rf.bindingUri ||
379
- (sliceHasOwnBinding && slice.binding.uri.split('|')[0] === rf.bindingUri.split('|')[0]);
384
+ (sliceHasOwnBinding && stripVersionFromCanonicalUrl(slice.binding.uri) === stripVersionFromCanonicalUrl(rf.bindingUri));
380
385
  if (sliceBindingMatchesParent && slice.binding?.sampleCode) {
381
386
  if (slice.binding.sampleCode.system) {
382
387
  system = slice.binding.sampleCode.system;
@@ -384,7 +389,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
384
389
  code = JSON.stringify(slice.binding.sampleCode.code);
385
390
  codeResolved = true;
386
391
  }
387
- else if (sliceBindingMatchesParent && slice.binding?.codes && slice.binding.codes.length > 0) {
392
+ else if (sliceBindingMatchesParent && slice.binding?.codes?.[0]) {
388
393
  // Fallback to first code from the binding's codes array
389
394
  const firstCode = slice.binding.codes[0];
390
395
  if (firstCode.system) {
@@ -458,8 +463,8 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
458
463
  // Handle CodeableConcept with pattern constraint - use the pattern instead of skeleton
459
464
  if (rf.type === 'CodeableConcept' && rf.patternConstraint && typeof rf.patternConstraint === 'object') {
460
465
  const pc = rf.patternConstraint;
461
- if (pc.coding && Array.isArray(pc.coding) && pc.coding.length > 0) {
462
- const firstCoding = pc.coding[0];
466
+ const firstCoding = Array.isArray(pc.coding) ? pc.coding[0] : undefined;
467
+ if (firstCoding) {
463
468
  const systemStr = JSON.stringify(firstCoding.system || TS.NULL_FLAVOR);
464
469
  const codeStr = JSON.stringify(firstCoding.code || 'UNK');
465
470
  const displayStr = firstCoding.display ? `, ${JSON.stringify(firstCoding.display)}` : '';
@@ -522,8 +527,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
522
527
  }
523
528
  // Complex type skeletons (also check baseType for profiled types like CodeableConcept-uv-ips)
524
529
  const skeletonType = COMPLEX_SKELETON_MAP[rf.type] ? rf.type : (rf.baseType && COMPLEX_SKELETON_MAP[rf.baseType] ? rf.baseType : undefined);
525
- if (skeletonType) {
526
- const expr = COMPLEX_SKELETON_MAP[skeletonType];
530
+ const skeletonExpr = skeletonType ? COMPLEX_SKELETON_MAP[skeletonType] : undefined;
531
+ if (skeletonType && skeletonExpr) {
532
+ const expr = skeletonExpr;
527
533
  const initExpr = buildValueExpression(rf, expr);
528
534
  requiredInits.push(`${propKey(rf.name)}: ${initExpr}`);
529
535
  continue;
@@ -61,15 +61,14 @@ export function buildValueExpression(rf, expr, opts) {
61
61
  export function buildCrossReferencedEntryArray(sliceElements) {
62
62
  const compositionIdx = [];
63
63
  const resourceEntries = [];
64
- for (let i = 0; i < sliceElements.length; i++) {
65
- const expr = sliceElements[i];
64
+ for (const [i, expr] of sliceElements.entries()) {
66
65
  if (/resourceType:\s*'Composition'/.test(expr)) {
67
66
  compositionIdx.push(i);
68
67
  }
69
68
  else {
70
- const m = expr.match(/resourceType:\s*'(\w+)'/);
71
- if (m)
72
- resourceEntries.push({ idx: i, type: m[1] });
69
+ const type = expr.match(/resourceType:\s*'(\w+)'/)?.[1];
70
+ if (type)
71
+ resourceEntries.push({ idx: i, type });
73
72
  }
74
73
  }
75
74
  if (compositionIdx.length === 0 || resourceEntries.length === 0)
@@ -79,10 +78,16 @@ export function buildCrossReferencedEntryArray(sliceElements) {
79
78
  for (const { idx, type } of resourceEntries) {
80
79
  const varName = `_${type.charAt(0).toLowerCase() + type.slice(1)}Url`;
81
80
  decls.push(`const ${varName} = 'urn:uuid:' + crypto.randomUUID();`);
82
- rewritten[idx] = rewritten[idx].replace(/fullUrl:\s*'urn:uuid:'\s*\+\s*crypto\.randomUUID\(\)/, `fullUrl: ${varName}`);
81
+ const entry = rewritten[idx];
82
+ if (entry !== undefined) {
83
+ rewritten[idx] = entry.replace(/fullUrl:\s*'urn:uuid:'\s*\+\s*crypto\.randomUUID\(\)/, `fullUrl: ${varName}`);
84
+ }
83
85
  for (const ci of compositionIdx) {
84
86
  const refPattern = new RegExp(`reference:\\s*'${type}/\\s*'\\s*\\+\\s*randomId\\(\\)`);
85
- rewritten[ci] = rewritten[ci].replace(refPattern, `reference: ${varName}`);
87
+ const composition = rewritten[ci];
88
+ if (composition !== undefined) {
89
+ rewritten[ci] = composition.replace(refPattern, `reference: ${varName}`);
90
+ }
86
91
  }
87
92
  }
88
93
  return `(() => { ${decls.join(' ')} return [${rewritten.join(', ')}]; })()`;
@@ -91,6 +91,8 @@ export function chooseChoiceVariant(rf) {
91
91
  return null;
92
92
  }
93
93
  let chosen = options[0];
94
+ if (chosen === undefined)
95
+ return null;
94
96
  if (lowerBase === 'value' && preferCodeableConcept) {
95
97
  chosen = 'CodeableConcept';
96
98
  }
@@ -210,8 +212,8 @@ export function applyRequiredFieldHeuristic(rf, ctx) {
210
212
  return { expr: makeCoding(system, picked.code), isObjectLiteral: true };
211
213
  }
212
214
  }
213
- if (isCodeableConcept) {
214
- const first = rf.bindingCodes[0];
215
+ const [first] = rf.bindingCodes;
216
+ if (isCodeableConcept && first) {
215
217
  const system = first.system || TS.SNOMED;
216
218
  return { expr: `skeletonCodeableConcept('${system}', ['${first.code}'])` };
217
219
  }
@@ -1,4 +1,5 @@
1
1
  import { fetchStructureDefinition } from '../../parser/sdParser.js';
2
+ import { stripSliceSuffix, sliceSuffix } from '../../core/utils.js';
2
3
  export async function extractFieldPatternsAndMetadata(patternSources, baseResource, sd, requiredFields, candidateRequiredFields, nestedRequirementsMap, resolvedProfilePatterns) {
3
4
  const patternMap = new Map();
4
5
  for (const src of patternSources) {
@@ -7,7 +8,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
7
8
  continue;
8
9
  // Extract the field path parts
9
10
  const parts = f.name.split('.');
10
- const lastPart = (parts.pop() || f.name).split(':')[0]; // base field name without slice
11
+ const lastPart = stripSliceSuffix(parts.pop() ?? f.name); // base field name without slice
11
12
  // For patterns on '.coding' elements, associate with the parent CodeableConcept field
12
13
  // e.g., for "Coverage.type.coding:VersicherungsArtDeBasis", use "type" as key
13
14
  let key = lastPart;
@@ -15,7 +16,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
15
16
  // This is a coding inside a CodeableConcept - use the parent field name
16
17
  const parentPart = parts.pop();
17
18
  if (parentPart) {
18
- key = parentPart.split(':')[0];
19
+ key = stripSliceSuffix(parentPart);
19
20
  }
20
21
  }
21
22
  // For nested fields (children of children, e.g., identifier.type vs top-level type),
@@ -28,8 +29,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
28
29
  idParts.shift(); // remove resource type prefix
29
30
  const nestedParts = [];
30
31
  for (const part of idParts) {
31
- if (part.includes(':')) {
32
- nestedParts.push(part.split(':')[1]); // use slice name
32
+ const partSliceName = sliceSuffix(part);
33
+ if (partSliceName !== undefined) {
34
+ nestedParts.push(partSliceName); // use slice name
33
35
  }
34
36
  else if (!part.includes('[x]')) {
35
37
  nestedParts.push(part);
@@ -204,9 +206,10 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
204
206
  // e.g., "value[x]:valueQuantity.unit" -> "valueQuantity.unit"
205
207
  const nestedParts = [];
206
208
  for (const part of idParts) {
207
- if (part.includes(':')) {
208
- // Extract slice name (e.g., "value[x]:valueQuantity" -> "valueQuantity")
209
- nestedParts.push(part.split(':')[1]);
209
+ // Slice name, e.g. "value[x]:valueQuantity" -> "valueQuantity"
210
+ const partSliceName = sliceSuffix(part);
211
+ if (partSliceName !== undefined) {
212
+ nestedParts.push(partSliceName);
210
213
  }
211
214
  else if (!part.includes('[x]')) {
212
215
  nestedParts.push(part);
@@ -235,8 +238,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
235
238
  // Build nested key, extracting slice names where present
236
239
  const nestedParts = [];
237
240
  for (const part of idParts) {
238
- if (part.includes(':')) {
239
- nestedParts.push(part.split(':')[1]);
241
+ const partSliceName = sliceSuffix(part);
242
+ if (partSliceName !== undefined) {
243
+ nestedParts.push(partSliceName);
240
244
  }
241
245
  else if (!part.includes('[x]')) {
242
246
  nestedParts.push(part);
@@ -252,7 +256,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
252
256
  }
253
257
  }
254
258
  // Otherwise, if this is a slice with binding codes, synthesize a pattern
255
- else if (!patternMap.has(key) && f.sliceName && f.binding && f.binding.codes && f.binding.codes.length > 0) {
259
+ else if (!patternMap.has(key) && f.sliceName && f.binding?.codes?.[0]) {
256
260
  // Create a CodeableConcept pattern from the first binding code
257
261
  const firstCode = f.binding.codes[0];
258
262
  const synthesizedPattern = {
@@ -363,9 +367,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
363
367
  const [basePart, sliceName] = p.split(':', 2);
364
368
  // For choice types (e.g., value[x]:valueQuantity), use the slice name
365
369
  // as the actual JS property name
366
- if (basePart.includes('[x]') && sliceName)
370
+ if (basePart?.includes('[x]') && sliceName)
367
371
  return sliceName;
368
- return basePart; // strip slice qualifier for non-choice types
372
+ return basePart ?? p; // strip slice qualifier for non-choice types
369
373
  }
370
374
  return p;
371
375
  });
@@ -413,7 +417,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
413
417
  const parts = elemId.split('.');
414
418
  if (parts.length !== 2)
415
419
  continue;
416
- const fieldName = (parts[1] || '').split(':')[0]; // Strip slice names
420
+ const fieldName = stripSliceSuffix(parts[1] || '');
417
421
  if (fieldName && !seenFieldNames.has(fieldName)) {
418
422
  seenFieldNames.add(fieldName);
419
423
  fieldOrder.push(fieldName);
@@ -399,11 +399,17 @@ export function getTypePlaceholder(fhirType) {
399
399
  return '"https://babelfhir.dev"';
400
400
  case 'code':
401
401
  return '"code"';
402
+ // Dates are emitted as a call, not a value. This function returns code that
403
+ // lands in a generated class, so interpolating new Date() here froze the
404
+ // generation-time wall clock into the source: every regeneration produced a
405
+ // different file, and every consumer got the same instant forever. The
406
+ // sibling getDefaultValueForType already defers to randomDate() through its
407
+ // __DATE_EXPR__ placeholder; this now agrees with it.
402
408
  case 'dateTime':
403
409
  case 'instant':
404
- return '"' + new Date().toISOString() + '"';
410
+ return 'randomDate()';
405
411
  case 'date':
406
- return '"' + new Date().toISOString().split('T')[0] + '"';
412
+ return 'randomDate().slice(0, 10)';
407
413
  case 'time':
408
414
  return '"12:00:00"';
409
415
  case 'Reference':
@@ -423,7 +429,7 @@ export function getTypePlaceholder(fhirType) {
423
429
  case 'ContactPoint':
424
430
  return '{ system: "phone", value: "555-1234" }';
425
431
  case 'Period':
426
- return '{ start: "' + new Date().toISOString() + '" }';
432
+ return '{ start: randomDate() }';
427
433
  case 'Attachment':
428
434
  return '{ contentType: "text/plain" }';
429
435
  case 'Narrative':
@@ -447,9 +453,9 @@ export function getTypePlaceholder(fhirType) {
447
453
  case 'Annotation':
448
454
  return '{ text: "Note" }';
449
455
  case 'Signature':
450
- return `{ type: [{ system: "urn:iso-astm:E1762-95:2013", code: "1.2.840.10065.1.12.1.1" }], when: "${new Date().toISOString()}", who: { reference: "Practitioner/example" } }`;
456
+ return `{ type: [{ system: "urn:iso-astm:E1762-95:2013", code: "1.2.840.10065.1.12.1.1" }], when: randomDate(), who: { reference: "Practitioner/example" } }`;
451
457
  case 'Meta':
452
- return '{ lastUpdated: "' + new Date().toISOString() + '" }';
458
+ return '{ lastUpdated: randomDate() }';
453
459
  case 'ContactDetail':
454
460
  return '{ name: "Contact" }';
455
461
  case 'UsageContext':
@@ -102,8 +102,8 @@ export function generateSliceElement(slice, elementType) {
102
102
  // Detect already-resolved choice type variants
103
103
  if (!wasResolvedFromChoiceNotation) {
104
104
  const choiceMatch = childPath.match(/^([a-z]+)(Boolean|Integer|Decimal|DateTime|Date|Time|Instant|String|Uri|Url|Canonical|Base64Binary|Code|Oid|Id|Uuid|Markdown|UnsignedInt|PositiveInt|Coding|CodeableConcept|Quantity|Range|Period|Ratio|SampledData|Attachment|Duration|Distance|Count|Money|Age|Annotation|Signature|HumanName|Address|ContactPoint|Timing|Reference|Identifier|Meta|Dosage|Narrative|Expression|TriggerDefinition|DataRequirement|ParameterDefinition|RelatedArtifact|ContactDetail|UsageContext)$/);
105
- if (choiceMatch) {
106
- const baseName = choiceMatch[1];
105
+ const baseName = choiceMatch?.[1];
106
+ if (baseName) {
107
107
  if (resolvedChoiceBases.has(baseName)) {
108
108
  continue;
109
109
  }
@@ -169,7 +169,7 @@ export function generateSliceElement(slice, elementType) {
169
169
  else if (profileUrl) {
170
170
  const profileName = profileUrl.split('/').pop() || '';
171
171
  if (profileName.includes('-')) {
172
- resourceType = capitalise(profileName.split('-')[0]);
172
+ resourceType = capitalise(profileName.split('-')[0] ?? profileName);
173
173
  }
174
174
  else {
175
175
  resourceType = capitalise(profileName);
@@ -18,7 +18,7 @@
18
18
  * }
19
19
  * ```
20
20
  */
21
- import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
21
+ import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, firstSegment } from '../../core/utils.js';
22
22
  import { getRules } from '../../fhir/versionContext.js';
23
23
  import { logger } from '../../../logger.js';
24
24
  const log = logger.withTag('backbone-slices');
@@ -35,7 +35,11 @@ export function generateBackboneSliceTypes(ctx) {
35
35
  return;
36
36
  for (const group of sliceGroups) {
37
37
  const { parentField, sliceFields, childFields, slicingRules } = group;
38
- const fieldName = parentField.name.split('.').pop();
38
+ const fieldName = parentField.name.split('.').at(-1);
39
+ // A grouped parent always has a dotted path, so this is unreachable; skipping
40
+ // is right if it ever is not, and it replaces a non-null assertion.
41
+ if (!fieldName)
42
+ continue;
39
43
  // Determine the base backbone type for this field
40
44
  let backboneType = inferBackboneType(baseResource, fieldName);
41
45
  if (!backboneType) {
@@ -100,8 +104,9 @@ export function generateBackboneSliceTypes(ctx) {
100
104
  if (isOpen) {
101
105
  unionMembers.push(backboneType);
102
106
  }
103
- const unionType = unionMembers.length === 1
104
- ? `${unionMembers[0]}[]`
107
+ const [soleMember] = unionMembers;
108
+ const unionType = unionMembers.length === 1 && soleMember
109
+ ? `${soleMember}[]`
105
110
  : `(${unionMembers.join(' | ')})[]`;
106
111
  // Find and update the field in the root interface
107
112
  updateRootInterfaceField(interfaces, interfaceName, fieldName, parentField.isOptional, unionType, baseResource);
@@ -181,7 +186,7 @@ function isEligibleBackbonePath(baseType, path, baseResource) {
181
186
  const parts = path.split('.');
182
187
  if (parts.length !== 2)
183
188
  return false;
184
- if (baseResource && parts[0] !== baseResource)
189
+ if (baseResource && firstSegment(path) !== baseResource)
185
190
  return false;
186
191
  return true;
187
192
  }
@@ -224,9 +229,10 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
224
229
  const childId = child.elementId || child.name;
225
230
  const relativePath = childId.substring(sliceElementId.length + 1);
226
231
  const match = choiceSlicePattern.exec(relativePath);
227
- if (match) {
232
+ const choiceParent = match?.[1]; // e.g. "value[x]"
233
+ if (choiceParent) {
228
234
  choiceTypeSlices.push(child);
229
- choiceTypeParents.add(match[1]); // e.g. "value[x]"
235
+ choiceTypeParents.add(choiceParent);
230
236
  }
231
237
  }
232
238
  // Collect deeply nested fixed values for object literal synthesis.
@@ -240,7 +246,9 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
240
246
  if (relativePath.includes('.')) {
241
247
  if (child.fixedValue !== undefined) {
242
248
  const segments = relativePath.split('.');
243
- const rawPropName = segments[0];
249
+ // firstSegment is segments[0] without the possibly-undefined read; segments
250
+ // is still needed for the tail below.
251
+ const rawPropName = firstSegment(relativePath);
244
252
  const propName = rawPropName.replace(/\[x\]$/, '');
245
253
  // Skip nested values under:
246
254
  // - choice-type elements ([x]) — handled by direct child or sub-slices
@@ -425,8 +433,8 @@ function updateRootInterfaceField(interfaces, interfaceName, fieldName, isOption
425
433
  const newFieldLine = ` ${fieldName}${optMark}: ${unionType};`;
426
434
  // Try to find an existing interface declaration
427
435
  const rootIdx = interfaces.findIndex(i => i.startsWith(`export interface ${interfaceName} `) || i.startsWith(`export interface ${interfaceName}<`));
428
- if (rootIdx >= 0) {
429
- const iface = interfaces[rootIdx];
436
+ const iface = rootIdx >= 0 ? interfaces[rootIdx] : undefined;
437
+ if (iface !== undefined) {
430
438
  const fieldPattern = new RegExp(`^(\\s+)${fieldName}[?]?:\\s*.+;`, 'm');
431
439
  if (fieldPattern.test(iface)) {
432
440
  // Replace existing field line
@@ -443,10 +451,10 @@ function updateRootInterfaceField(interfaces, interfaceName, fieldName, isOption
443
451
  }
444
452
  // Try to find a type alias (e.g., "export type BloodPressure = Observation;")
445
453
  const aliasIdx = interfaces.findIndex(i => i.startsWith(`export type ${interfaceName} =`));
446
- if (aliasIdx >= 0) {
454
+ const alias = aliasIdx >= 0 ? interfaces[aliasIdx] : undefined;
455
+ if (alias !== undefined) {
447
456
  // Convert type alias to interface with the field
448
- const aliasMatch = interfaces[aliasIdx].match(/export type \S+ = (\S+);/);
449
- const baseType = aliasMatch ? aliasMatch[1] : baseResource || '';
457
+ const baseType = alias.match(/export type \S+ = (\S+);/)?.[1] ?? baseResource ?? '';
450
458
  interfaces[aliasIdx] = `export interface ${interfaceName} extends ${baseType} {\n${newFieldLine}\n}`;
451
459
  }
452
460
  }
@@ -263,7 +263,7 @@ export class ImportManager {
263
263
  const allFhirTypes = [];
264
264
  let match;
265
265
  while ((match = fhirImportRegex.exec(output)) !== null) {
266
- const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
266
+ const types = (match[1] ?? '').split(',').map(t => t.trim()).filter(Boolean);
267
267
  allFhirTypes.push(...types);
268
268
  }
269
269
  if (allFhirTypes.length === 0) {
@@ -61,7 +61,9 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
61
61
  const baseFieldAnyArray = new Map();
62
62
  for (const bf of baseFields) {
63
63
  const parts = bf.name.split('.');
64
- const seg = parts[parts.length - 1];
64
+ const seg = parts.at(-1);
65
+ if (!seg)
66
+ continue;
65
67
  if (!baseFieldByLastSegment.has(seg))
66
68
  baseFieldByLastSegment.set(seg, bf);
67
69
  // Track the widest array flag across direct children only (not nested duplicates)
@@ -83,10 +85,11 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
83
85
  f.sliceName &&
84
86
  f.binding?.uri);
85
87
  // Use the first slice binding if available (slices typically have more specific constraints)
86
- if (slicesForThisField.length > 0 && slicesForThisField[0].binding) {
87
- binding = slicesForThisField[0].binding;
88
+ const firstSlice = slicesForThisField[0];
89
+ if (firstSlice?.binding) {
90
+ binding = firstSlice.binding;
88
91
  isSlicedField = true;
89
- debug(`Using binding from slice ${slicesForThisField[0].sliceName} for ${field.name}`);
92
+ debug(`Using binding from slice ${firstSlice.sliceName} for ${field.name}`);
90
93
  }
91
94
  }
92
95
  if (!binding) {
@@ -196,6 +199,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
196
199
  // This must run after all interfaces + extension unions are assembled.
197
200
  for (let i = 0; i < interfaces.length; i++) {
198
201
  const iface = interfaces[i];
202
+ if (!iface)
203
+ continue;
199
204
  // Match "export interface Foo extends Bar {" or "export interface Foo extends Omit<Bar, ...> {"
200
205
  const extendsMatch = iface.match(/^export interface (\S+) extends (Omit<)?(\S+?)([,>])? \{/);
201
206
  if (!extendsMatch)
@@ -203,16 +208,16 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
203
208
  const alreadyOmit = !!extendsMatch[2];
204
209
  const baseType = extendsMatch[3];
205
210
  // Only wrap FHIR base types (not local profile interfaces)
206
- if (!isFhirType(baseType))
211
+ if (!baseType || !isFhirType(baseType))
207
212
  continue;
208
213
  // Extract all declared property names from the interface body
209
214
  const bodyStart = iface.indexOf('{');
210
215
  const body = iface.slice(bodyStart + 1);
211
216
  const propNames = [];
212
217
  for (const line of body.split('\n')) {
213
- const m = line.match(/^\s+(\w+)[?:]/);
214
- if (m && m[1] !== 'resourceType')
215
- propNames.push(m[1]);
218
+ const propName = line.match(/^\s+(\w+)[?:]/)?.[1];
219
+ if (propName && propName !== 'resourceType')
220
+ propNames.push(propName);
216
221
  }
217
222
  if (propNames.length === 0)
218
223
  continue;
@@ -266,7 +271,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
266
271
  logger.debug('[DEBUG FINAL INTERFACES]', {
267
272
  interfaceName,
268
273
  interfaceCount: interfaces.length,
269
- interfaceNames: interfaces.map(i => i.split('\n')[0]).filter(n => n.includes('ConditionDeBasis02'))
274
+ interfaceNames: interfaces.map(i => i.split('\n')[0] ?? '').filter(n => n.includes('ConditionDeBasis02'))
270
275
  });
271
276
  }
272
277
  return {
@@ -122,18 +122,22 @@ export function postProcessExtensions(params) {
122
122
  }
123
123
  idx = findInterfaceIndex(name);
124
124
  }
125
- if (idx >= 0 && !interfaces[idx].includes(line.trim())) {
125
+ // Hoisted once: the block below reads the same entry five times, and
126
+ // indexing it each time reads as possibly-undefined even after the bound
127
+ // check. Reads go through `target`, writes still go to interfaces[idx].
128
+ const target = idx >= 0 ? interfaces[idx] : undefined;
129
+ if (target !== undefined && !target.includes(line.trim())) {
126
130
  // Also check if a property with the same name already exists to avoid duplicates
127
- const propName = line.trim().split(/[?:]/)[0].trim();
131
+ const propName = (line.trim().split(/[?:]/)[0] ?? '').trim();
128
132
  const propPattern = new RegExp(`^\\s*${propName}[?:]`, 'm');
129
- if (interfaces[idx].match(propPattern)) {
133
+ if (target.match(propPattern)) {
130
134
  // For extension properties, REPLACE the existing declaration with the new union
131
135
  // This handles both union `(Extension | Type)[]` and plain `Type[]` formats
132
136
  if (propName === 'extension' && line.includes('extension?:')) {
133
137
  const existingExtPattern = /\s*extension\?\s*:\s*[^;\n]+;/;
134
- if (existingExtPattern.test(interfaces[idx])) {
138
+ if (existingExtPattern.test(target)) {
135
139
  const newExtLine = line.trim();
136
- interfaces[idx] = interfaces[idx].replace(existingExtPattern, `\n ${newExtLine}`);
140
+ interfaces[idx] = target.replace(existingExtPattern, `\n ${newExtLine}`);
137
141
  logger.debug('[DEBUG replaced extension property]', { propName, line: newExtLine });
138
142
  return;
139
143
  }
@@ -142,9 +146,9 @@ export function postProcessExtensions(params) {
142
146
  return;
143
147
  }
144
148
  if (name === 'USCoreQuestionnaireResponseProfileItem' && line.includes('answer')) {
145
- logger.debug('[DEBUG appendToInterface Item answer]', { name, line, currentInterface: interfaces[idx] });
149
+ logger.debug('[DEBUG appendToInterface Item answer]', { name, line, currentInterface: target });
146
150
  }
147
- interfaces[idx] = interfaces[idx].replace(/}\s*$/, `\n ${line}\n}`);
151
+ interfaces[idx] = target.replace(/}\s*$/, `\n ${line}\n}`);
148
152
  }
149
153
  };
150
154
  // Ensure root interface exists and track its name
@@ -174,7 +178,9 @@ export function postProcessExtensions(params) {
174
178
  continue;
175
179
  }
176
180
  if (segs.length === 1) {
177
- const seg = segs[0].replace(/\[x\]/g, '');
181
+ const seg = (segs[0] ?? '').replace(/\[x\]/g, '');
182
+ if (!seg)
183
+ continue;
178
184
  const nestedName = `${rootName}${capitalize(seg)}`;
179
185
  // Create alias types and union
180
186
  const aliases = [];
@@ -204,8 +210,9 @@ export function postProcessExtensions(params) {
204
210
  const optionalLine = `_${seg}?: ${elementIface};`;
205
211
  // Replace any required with optional first
206
212
  const idx = findInterfaceIndex(rootName);
207
- if (idx >= 0) {
208
- interfaces[idx] = interfaces[idx].replace(requiredLine, optionalLine);
213
+ const existing = idx >= 0 ? interfaces[idx] : undefined;
214
+ if (existing !== undefined) {
215
+ interfaces[idx] = existing.replace(requiredLine, optionalLine);
209
216
  }
210
217
  appendToInterface(rootName, `_${seg}${opt}: ${elementIface};`);
211
218
  }
@@ -265,6 +272,8 @@ export function postProcessExtensions(params) {
265
272
  }
266
273
  else if (segs.length === 2) {
267
274
  const [seg1Raw, seg2Raw] = segs;
275
+ if (!seg1Raw || !seg2Raw)
276
+ continue;
268
277
  const seg1 = seg1Raw.replace(/\[x\]/g, '');
269
278
  const seg2 = seg2Raw.replace(/\[x\]/g, '');
270
279
  // Ensure first-level container exists
@@ -389,14 +398,15 @@ export function safetyNetRootExtensions(params) {
389
398
  importManager.addFhirType('Extension');
390
399
  const union = ['Extension', ...aliases].join(' | ');
391
400
  const idx = findInterfaceIndex(interfaceName);
392
- if (idx >= 0) {
401
+ const target = idx >= 0 ? interfaces[idx] : undefined;
402
+ if (target !== undefined) {
393
403
  // Replace any existing extension property (union or plain type) with the new union
394
404
  const existingExtPattern = /\s*extension\?\s*:\s*[^;\n]+;/;
395
- if (existingExtPattern.test(interfaces[idx])) {
396
- interfaces[idx] = interfaces[idx].replace(existingExtPattern, `\n extension?: (${union})[];`);
405
+ if (existingExtPattern.test(target)) {
406
+ interfaces[idx] = target.replace(existingExtPattern, `\n extension?: (${union})[];`);
397
407
  }
398
408
  else {
399
- interfaces[idx] = interfaces[idx].replace(/}\s*$/, `\n extension?: (${union})[];\n}`);
409
+ interfaces[idx] = target.replace(/}\s*$/, `\n extension?: (${union})[];\n}`);
400
410
  }
401
411
  }
402
412
  }