babelfhir-ts 1.5.21 → 1.6.0

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 (33) hide show
  1. package/README.md +6 -3
  2. package/out/src/generator/core/extensionSdReader.js +2 -1
  3. package/out/src/generator/core/utils.js +69 -0
  4. package/out/src/generator/emitters/class/classGeneratorHelpers.js +10 -2
  5. package/out/src/generator/emitters/class/sliceElementDefaults.js +10 -1
  6. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +15 -12
  7. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +6 -3
  8. package/out/src/generator/emitters/interface/postProcessExtensions.js +8 -2
  9. package/out/src/generator/emitters/interface/processNestedField.js +4 -6
  10. package/out/src/generator/emitters/namingsystem/namingSystemGenerator.js +136 -0
  11. package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -15
  12. package/out/src/generator/emitters/prefab/prefabRenderer.js +2 -19
  13. package/out/src/generator/emitters/validator/closedSlicingValidation.js +5 -2
  14. package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +7 -1
  15. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +27 -12
  16. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +15 -27
  17. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +5 -1
  18. package/out/src/generator/emitters/validator/subExtensionScope.js +10 -2
  19. package/out/src/generator/emitters/validator/validatorExpressions.js +44 -0
  20. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +34 -17
  21. package/out/src/generator/emitters/validator/validatorGenerator.js +31 -40
  22. package/out/src/generator/emitters/validator/validatorRuntime.js +13 -2
  23. package/out/src/generator/emitters/valueset/valueSetGenerator.js +6 -10
  24. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +39 -3
  25. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +36 -8
  26. package/out/src/generator/generationHelpers.js +59 -7
  27. package/out/src/generator/index.js +90 -47
  28. package/out/src/generator/parser/packageParser.js +76 -0
  29. package/out/src/generator/parser/vsParser.js +4 -2
  30. package/out/src/generator/sdProcessor.js +30 -21
  31. package/out/src/main.js +5 -0
  32. package/package.json +12 -9
  33. package/parity-matrix.json +6 -0
@@ -10,7 +10,7 @@ import path from 'path';
10
10
  import { generateClass } from './emitters/class/classGenerator.js';
11
11
  import { generateInterfaces } from './emitters/interface/interfaceGenerator.js';
12
12
  import { fetchStructureDefinition, parseStructureDefinition } from './parser/sdParser.js';
13
- import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, writeToFile, getBaseResource, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from './core/utils.js';
13
+ import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, writeToFile, getBaseResource, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl, stripSliceSuffix } from './core/utils.js';
14
14
  import { extensionElements, collectSubExtensionSlices, readSubExtensionMin, readBinding } from './core/extensionSdReader.js';
15
15
  import { generateValidateProfileFunction } from './emitters/validator/validatorGenerator.js';
16
16
  import { generateZodSchema } from './emitters/zod/zodSchemaGenerator.js';
@@ -113,7 +113,10 @@ async function fillUnstatedFromBaseResource(fields, resourceType) {
113
113
  if (!current)
114
114
  continue;
115
115
  for (let i = next; i < segments.length && current; i++) {
116
- current = resolveComplexTypeChildType(current, segments[i]);
116
+ const segment = segments[i];
117
+ if (segment === undefined)
118
+ break;
119
+ current = resolveComplexTypeChildType(current, segment);
117
120
  }
118
121
  if (current) {
119
122
  field.type = current;
@@ -193,11 +196,12 @@ async function fillSliceProfileUrlsFromBaseProfiles(fields, baseDefinition) {
193
196
  async function resolveExtensionMetadata(fields) {
194
197
  const extUrls = new Set();
195
198
  for (const f of fields) {
196
- if (!f.sliceName || !f.profileUrls?.length)
199
+ const firstProfile = f.sliceName ? f.profileUrls?.[0] : undefined;
200
+ if (!firstProfile)
197
201
  continue;
198
- const basePath = (f.name || '').replace(/^[^.]+\./, '').split(':')[0];
202
+ const basePath = stripSliceSuffix((f.name || '').replace(/^[^.]+\./, ''));
199
203
  if (basePath.endsWith('extension') || basePath.endsWith('modifierExtension')) {
200
- extUrls.add(f.profileUrls[0]);
204
+ extUrls.add(firstProfile);
201
205
  }
202
206
  }
203
207
  if (extUrls.size === 0)
@@ -242,8 +246,9 @@ async function resolveExtensionMetadata(fields) {
242
246
  // Check binding on value[x] first, then fall back to typed slices
243
247
  // (e.g., Extension.valueCode may carry the binding instead of Extension.value[x])
244
248
  let binding = readBinding(valueEl);
245
- if (!binding && types.length === 1) {
246
- const suffix = types[0].charAt(0).toUpperCase() + types[0].slice(1);
249
+ const onlyType = types.length === 1 ? types[0] : undefined;
250
+ if (!binding && onlyType) {
251
+ const suffix = onlyType.charAt(0).toUpperCase() + onlyType.slice(1);
247
252
  const sliceEl = els.find(el => el.id === `Extension.value[x]:value${suffix}` ||
248
253
  el.path === `Extension.value${suffix}` ||
249
254
  el.id === `Extension.value${suffix}`);
@@ -274,12 +279,13 @@ async function resolveExtensionMetadata(fields) {
274
279
  async function resolveDatatypeProfilePins(fields) {
275
280
  const urls = new Set();
276
281
  for (const f of fields) {
277
- if (!f.sliceName || !f.profileUrls?.length)
282
+ const firstProfile = f.sliceName ? f.profileUrls?.[0] : undefined;
283
+ if (!firstProfile)
278
284
  continue;
279
- const basePath = (f.name || '').replace(/^[^.]+\./, '').split(':')[0];
285
+ const basePath = stripSliceSuffix((f.name || '').replace(/^[^.]+\./, ''));
280
286
  if (basePath.endsWith('extension') || basePath.endsWith('modifierExtension'))
281
287
  continue;
282
- urls.add(f.profileUrls[0]);
288
+ urls.add(firstProfile);
283
289
  }
284
290
  if (urls.size === 0)
285
291
  return undefined;
@@ -514,8 +520,11 @@ export async function processStructureDefinition(sd, ctx) {
514
520
  continue;
515
521
  if (f.isOptional || f.isForbidden)
516
522
  continue;
517
- const parentFieldName = parts[1].split(':')[0];
518
- let relativePath = parts.slice(2).map((p) => p.split(':')[0]).join('.');
523
+ const parentSegment = parts[1];
524
+ if (!parentSegment)
525
+ continue;
526
+ const parentFieldName = stripSliceSuffix(parentSegment);
527
+ let relativePath = parts.slice(2).map((p) => stripSliceSuffix(p)).join('.');
519
528
  if (relativePath.includes('[x]')) {
520
529
  const resolvedPath = resolveChoiceSegments(f, parts, candidateRequiredFields);
521
530
  // An intermediate choice we cannot narrow to one type has no property name.
@@ -566,9 +575,9 @@ export async function processStructureDefinition(sd, ctx) {
566
575
  const parentPath = parts.slice(0, -1).join('.');
567
576
  const parentField = candidateRequiredFields.find(pf => pf?.name === parentPath && pf.type && pf.type !== 'any' && pf.type !== 'unknown')
568
577
  || parsed.oldFields.find(pf => pf?.name === parentPath && pf.type && pf.type !== 'any' && pf.type !== 'unknown');
569
- if (parentField?.type) {
570
- const childName = parts[parts.length - 1].split(':')[0];
571
- fieldType = resolveComplexTypeChildType(parentField.type, childName);
578
+ const leafSegment = parts.at(-1);
579
+ if (parentField?.type && leafSegment) {
580
+ fieldType = resolveComplexTypeChildType(parentField.type, stripSliceSuffix(leafSegment));
572
581
  }
573
582
  }
574
583
  }
@@ -640,8 +649,8 @@ export async function processStructureDefinition(sd, ctx) {
640
649
  if (originalField?.typeOptions?.[0]?.targetProfileUrls?.[0]) {
641
650
  const targetUrl = stripVersionFromCanonicalUrl(originalField.typeOptions[0].targetProfileUrls[0]);
642
651
  const match = targetUrl.match(/\/([^/]+)$/);
643
- if (match) {
644
- const candidate = match[1];
652
+ const candidate = match?.[1];
653
+ if (candidate) {
645
654
  const knownResourceNames = versionCtx().resourceNames;
646
655
  if (knownResourceNames.includes(candidate)) {
647
656
  rf.targetResourceType = candidate;
@@ -651,9 +660,9 @@ export async function processStructureDefinition(sd, ctx) {
651
660
  let resolved = false;
652
661
  for (const bf of baseFields) {
653
662
  for (const tp of bf.typeOptions?.[0]?.targetProfileUrls ?? []) {
654
- const bm = tp.match(/\/([^/]+)$/);
655
- if (bm && knownResourceNames.includes(bm[1])) {
656
- rf.targetResourceType = bm[1];
663
+ const bmCandidate = tp.match(/\/([^/]+)$/)?.[1];
664
+ if (bmCandidate && knownResourceNames.includes(bmCandidate)) {
665
+ rf.targetResourceType = bmCandidate;
657
666
  resolved = true;
658
667
  break;
659
668
  }
@@ -732,7 +741,7 @@ export async function processStructureDefinition(sd, ctx) {
732
741
  writeToFile(path.join(outputDir, `${interfaceName}Class.ts`), classContent);
733
742
  }
734
743
  if (flags?.schema === 'zod') {
735
- const zodResult = generateZodSchema(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets);
744
+ const zodResult = generateZodSchema(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets, !!flags?.zodInvariants);
736
745
  writeToFile(path.join(outputDir, `${interfaceName}.zod.ts`), zodResult.code);
737
746
  }
738
747
  if (flags?.prefab) {
package/out/src/main.js CHANGED
@@ -57,6 +57,7 @@ function printUsage() {
57
57
  console.log(" --no-classes Only generate interfaces and types (skip class generation)");
58
58
  console.log(" --no-client Skip FHIR client generation (client generated by default)");
59
59
  console.log(" --schema <format> Generate schema files alongside outputs (supported: zod)");
60
+ console.log(" --zod-invariants Emit FHIRPath invariants into Zod schemas (fuller checking, slower parse)");
60
61
  console.log(" --dicomweb Generate DICOMweb helpers typed to ImagingStudy profiles in the IG");
61
62
  console.log(" --prefab Generate Prefab UI render functions per profile (@maxhealth.tech/prefab)");
62
63
  console.log(" --prefab-styles <path> Path to styles module (.ts/.js) copied into the generated prefab/ folder");
@@ -366,6 +367,10 @@ export function parseCliArgs(argv) {
366
367
  i++;
367
368
  continue;
368
369
  }
370
+ if (arg === '--zod-invariants') {
371
+ out.flags.zodInvariants = true;
372
+ continue;
373
+ }
369
374
  if (arg === '--fhir-version') {
370
375
  const val = argv[i + 1];
371
376
  if (!val || val.startsWith('-'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.5.21",
3
+ "version": "1.6.0",
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",
@@ -87,23 +87,23 @@
87
87
  "devDependencies": {
88
88
  "@eslint/js": "^10.0.1",
89
89
  "@max-health/config": "github:max-health-inc/config",
90
- "@maxhealth.tech/prefab": "^0.3.5",
90
+ "@maxhealth.tech/prefab": "^0.3.7",
91
91
  "@types/node": "^25.9.5",
92
- "@types/semver": "^7.7.1",
92
+ "@types/semver": "^7.8.0",
93
93
  "@types/unzipper": "^0.10.11",
94
94
  "@vitest/coverage-v8": "^4.1.10",
95
- "@vitest/eslint-plugin": "^1.6.24",
95
+ "@vitest/eslint-plugin": "^1.6.26",
96
96
  "@vitest/ui": "^4.1.10",
97
97
  "cross-env": "^10.1.0",
98
98
  "eslint": "^10.8.0",
99
- "fhirpath": "^5.0.0",
100
- "globals": "^17.7.0",
99
+ "fhirpath": "^5.1.0",
100
+ "globals": "^17.9.0",
101
101
  "rimraf": "^6.1.3",
102
102
  "semver": "^7.8.5",
103
103
  "typescript": "^6.0.3",
104
- "typescript-eslint": "^8.65.0",
104
+ "typescript-eslint": "^8.66.0",
105
105
  "vitepress": "^1.6.4",
106
- "vitepress-plugin-llms": "^1.13.3",
106
+ "vitepress-plugin-llms": "^1.13.4",
107
107
  "vitest": "^4.1.10",
108
108
  "zod": "4.3.6"
109
109
  },
@@ -111,7 +111,10 @@
111
111
  "@types/fhir": "^0.0.44",
112
112
  "consola": "^3.4.2",
113
113
  "tar": "^7.5.22",
114
- "tsx": "^4.23.1",
114
+ "tsx": "^4.23.7",
115
115
  "unzipper": "^0.12.5"
116
+ },
117
+ "overrides": {
118
+ "brace-expansion": "^5.0.9"
116
119
  }
117
120
  }
@@ -116,6 +116,12 @@
116
116
  "category": "us",
117
117
  "fhirVersion": "r4"
118
118
  },
119
+ {
120
+ "name": "vitals",
121
+ "spec": "hl7.fhir.us.vitals@1.0.0",
122
+ "category": "us",
123
+ "fhirVersion": "r4"
124
+ },
119
125
  {
120
126
  "name": "sdc",
121
127
  "spec": "hl7.fhir.uv.sdc@4.0.0",