babelfhir-ts 1.0.26 → 1.0.28

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 Maximilian Nussbaumer
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # BabelFHIR-TS
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/babelfhir-ts.svg)](https://www.npmjs.com/package/babelfhir-ts)
4
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue)](https://www.typescriptlang.org/)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org/)
7
+
3
8
  **BabelFHIR-TS** transforms FHIR® StructureDefinitions into production-ready TypeScript code with full type safety and built-in validation. Unlike generic FHIR type definitions, BabelFHIR-TS generates **profile-aware** interfaces that understand your Implementation Guide's constraints, extensions, and slicing rules.
4
9
 
5
10
  ### What you get
@@ -49,7 +54,7 @@ Download and process a package directly from a registry (defaults to `https://pa
49
54
  babelfhir-ts --package hl7.fhir.us.core@8.0.0
50
55
  ```
51
56
 
52
- Install a processed package into your current project:
57
+ Download, pocess and install a processed package into your current project:
53
58
 
54
59
  ```bash
55
60
  babelfhir-ts install hl7.fhir.us.core@8.0.0
@@ -188,3 +193,18 @@ We continuously improve the generator based on real-world IG usage, and your fee
188
193
  ## License
189
194
 
190
195
  ISC © Maximilian Nussbaumer
196
+
197
+ ## Contributing
198
+
199
+ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project.
200
+
201
+ ## Security
202
+
203
+ For security issues, please see [SECURITY.md](SECURITY.md) for our security policy and how to report vulnerabilities.
204
+
205
+ ## Links
206
+
207
+ - [npm package](https://www.npmjs.com/package/babelfhir-ts)
208
+ - [GitHub repository](https://github.com/quotentiroler/BabelFHIR-ts)
209
+ - [Issue tracker](https://github.com/quotentiroler/BabelFHIR-ts/issues)
210
+ - [Changelog](CHANGELOG.md)
@@ -0,0 +1,252 @@
1
+ import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
2
+ import { sanitizeIdentifier } from './utils.js';
3
+ /**
4
+ * Manages TypeScript import statements for generated FHIR interfaces.
5
+ * Handles three types of imports:
6
+ * 1. FHIR base types from '@types/fhir' (via 'fhir/r4')
7
+ * 2. Custom local types (other generated profiles in the same package)
8
+ * 3. External profiled types (to be fetched/generated separately)
9
+ * 4. Path-based imports (custom relative paths, e.g., ValueSets in subdirectories)
10
+ */
11
+ export class ImportManager {
12
+ fhirImports = new Set();
13
+ customImports = new Set();
14
+ pathBasedImports = new Map(); // typeName -> relativePath
15
+ localInterfaceNames;
16
+ existingProfiles;
17
+ externalProfiles = new Map();
18
+ constructor(localInterfaceNames, existingProfiles) {
19
+ this.localInterfaceNames = localInterfaceNames;
20
+ this.existingProfiles = existingProfiles;
21
+ }
22
+ /**
23
+ * Adds a type to the appropriate import category.
24
+ *
25
+ * @param typeName - The TypeScript type name to import
26
+ * @param profileUrl - Optional profile URL for external profiles
27
+ * @param baseType - Optional base type for external profiles
28
+ * @param isFhirBase - Whether the base type is a FHIR type
29
+ */
30
+ addType(typeName, profileUrl, baseType, isFhirBase = false) {
31
+ if (!typeName)
32
+ return;
33
+ // Check if it's a nested interface of the current profile (defined locally in this file)
34
+ if (this.localInterfaceNames.has(typeName)) {
35
+ // Don't add to imports - defined in the same file
36
+ return;
37
+ }
38
+ // Check if it's a known FHIR type from @types/fhir
39
+ if (this.isFhirType(typeName)) {
40
+ this.fhirImports.add(typeName);
41
+ return;
42
+ }
43
+ // Check if it exists in our generated profiles/types
44
+ if (this.existingProfiles?.has(typeName)) {
45
+ this.customImports.add(typeName);
46
+ return;
47
+ }
48
+ // Check if this is a primitive type that shouldn't be fetched
49
+ if (this.isPrimitiveLikeType(typeName)) {
50
+ return;
51
+ }
52
+ // Add to custom imports (will be generated or fetched)
53
+ this.customImports.add(typeName);
54
+ // Track as external profile if we have profile metadata
55
+ if (profileUrl || baseType) {
56
+ this.registerExternalProfile(typeName, profileUrl, baseType, isFhirBase);
57
+ }
58
+ }
59
+ /**
60
+ * Registers an external profile that needs to be fetched or generated.
61
+ */
62
+ registerExternalProfile(rawTypeName, profileUrl, baseType, isFhirBase = false) {
63
+ if (!rawTypeName)
64
+ return;
65
+ const sanitizedTypeName = sanitizeIdentifier(rawTypeName);
66
+ const resolvedProfileUrl = profileUrl && profileUrl.length > 0
67
+ ? profileUrl
68
+ : `http://hl7.org/fhir/StructureDefinition/${sanitizedTypeName}`;
69
+ let meta = this.externalProfiles.get(sanitizedTypeName);
70
+ if (!meta) {
71
+ meta = { profileUrl: resolvedProfileUrl, baseTypes: new Map() };
72
+ this.externalProfiles.set(sanitizedTypeName, meta);
73
+ }
74
+ else if (profileUrl && profileUrl.length > 0 &&
75
+ meta.profileUrl.startsWith('http://hl7.org/fhir/StructureDefinition/')) {
76
+ // Update with more specific profile URL
77
+ meta.profileUrl = profileUrl;
78
+ }
79
+ // Track base type relationship
80
+ if (baseType && baseType !== 'any') {
81
+ const sanitizedBase = sanitizeIdentifier(baseType);
82
+ const existing = meta.baseTypes.get(sanitizedBase);
83
+ if (!existing) {
84
+ meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
85
+ }
86
+ else if (isFhirBase && !existing.isFhir) {
87
+ existing.isFhir = true;
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Gets all registered external profile references.
93
+ */
94
+ getExternalProfiles() {
95
+ return this.externalProfiles;
96
+ }
97
+ /**
98
+ * Generates import statement lines for FHIR and custom types.
99
+ */
100
+ generateImportStatements() {
101
+ const statements = [];
102
+ // FHIR imports from 'fhir/r4'
103
+ if (this.fhirImports.size > 0) {
104
+ const sortedFhirImports = Array.from(this.fhirImports).sort();
105
+ statements.push(`import { ${sortedFhirImports.join(', ')} } from "fhir/r4";`);
106
+ }
107
+ // Path-based imports (e.g., ValueSets from subdirectories)
108
+ const sortedPathBasedImports = Array.from(this.pathBasedImports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
109
+ for (const [typeName, relativePath] of sortedPathBasedImports) {
110
+ statements.push(`import { ${typeName} } from "${relativePath}";`);
111
+ }
112
+ // Custom local imports
113
+ const sortedCustomImports = Array.from(this.customImports).sort();
114
+ for (const type of sortedCustomImports) {
115
+ statements.push(`import { ${type} } from "./${type}";`);
116
+ }
117
+ return statements.join('\n');
118
+ }
119
+ /**
120
+ * Prunes unused imports from generated code.
121
+ * Removes import statements for types that aren't actually used.
122
+ */
123
+ pruneUnusedImports(output) {
124
+ // Prune unused FHIR imports
125
+ output = output.replace(/import \{([^}]+)\} from "fhir\/r4";?/g, (full, inside) => {
126
+ const used = [];
127
+ const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
128
+ for (const name of specifiers) {
129
+ const re = new RegExp(`\\b${name}\\b`);
130
+ if (re.test(output.replace(full, ''))) {
131
+ used.push(name);
132
+ }
133
+ }
134
+ return used.length ? `import { ${used.join(', ')} } from "fhir/r4";` : '';
135
+ });
136
+ // Prune unused custom imports (both path-based and standard custom imports)
137
+ output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
138
+ const used = [];
139
+ const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
140
+ for (const name of specifiers) {
141
+ const re = new RegExp(`\\b${name}\\b`);
142
+ if (re.test(output.replace(full, ''))) {
143
+ used.push(name);
144
+ }
145
+ }
146
+ return used.length ? `import { ${used.join(', ')} } from "./${path}";` : '';
147
+ });
148
+ // Remove now-empty import lines and collapse multiple blank lines
149
+ output = output.replace(/^\s*import \{\s*\}.*$/mg, '').replace(/\n{3,}/g, '\n\n');
150
+ return output;
151
+ }
152
+ /**
153
+ * Reorganizes imports to prefer local types over FHIR types where appropriate.
154
+ * Moves locally-generated types from fhir/r4 imports to local imports.
155
+ */
156
+ reorganizeImports(output) {
157
+ if (!this.existingProfiles || this.existingProfiles.size === 0) {
158
+ return output;
159
+ }
160
+ // Extract ALL types from fhir/r4 imports (there may be multiple import lines)
161
+ const fhirImportRegex = /import \{([^}]+)\} from "fhir\/r4";/g;
162
+ const allFhirTypes = [];
163
+ let match;
164
+ while ((match = fhirImportRegex.exec(output)) !== null) {
165
+ const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
166
+ allFhirTypes.push(...types);
167
+ }
168
+ if (allFhirTypes.length === 0) {
169
+ return output;
170
+ }
171
+ const localTypes = [];
172
+ const remainingFhirTypes = [];
173
+ for (const typeName of allFhirTypes) {
174
+ if (this.existingProfiles.has(typeName)) {
175
+ // Never move FhirResource to local imports; it must stay from fhir/r4
176
+ if (typeName !== 'FhirResource') {
177
+ localTypes.push(typeName);
178
+ }
179
+ else {
180
+ remainingFhirTypes.push(typeName);
181
+ }
182
+ }
183
+ else {
184
+ remainingFhirTypes.push(typeName);
185
+ }
186
+ }
187
+ // Remove ALL old fhir/r4 import lines
188
+ output = output.replace(/import \{[^}]+\} from "fhir\/r4";\n?/g, '');
189
+ // Add back the imports at the top
190
+ let newImports = '';
191
+ if (remainingFhirTypes.length > 0) {
192
+ newImports += `import { ${remainingFhirTypes.join(', ')} } from "fhir/r4";\n`;
193
+ }
194
+ for (const localType of localTypes) {
195
+ newImports += `import { ${localType} } from "./${localType}";\n`;
196
+ }
197
+ // Insert new imports after any existing custom imports or at the top
198
+ if (newImports) {
199
+ const customImportMatch = output.match(/^(import \{[^}]+\} from "\.\/[^"]+";?\n)+/m);
200
+ if (customImportMatch) {
201
+ // Insert after existing custom imports
202
+ output = output.replace(customImportMatch[0], customImportMatch[0] + newImports);
203
+ }
204
+ else {
205
+ // Insert at the very top
206
+ output = newImports + output;
207
+ }
208
+ }
209
+ return output;
210
+ }
211
+ /**
212
+ * Checks if a type is a known FHIR type from @types/fhir.
213
+ */
214
+ isFhirType(type) {
215
+ return type === 'FhirResource' || fhirInterfaceNames.includes(type);
216
+ }
217
+ /**
218
+ * Checks if a type name looks like a primitive that shouldn't be imported.
219
+ */
220
+ isPrimitiveLikeType(typeName) {
221
+ const primitiveTypeLowerCase = typeName.toLowerCase();
222
+ return [
223
+ 'integer', 'unsignedint', 'positiveint', 'decimal', 'boolean', 'string',
224
+ 'base64binary', 'instant', 'date', 'datetime', 'time', 'code', 'oid',
225
+ 'id', 'markdown', 'uri', 'url', 'canonical', 'uuid'
226
+ ].includes(primitiveTypeLowerCase);
227
+ }
228
+ /**
229
+ * Adds a specific FHIR type to imports.
230
+ */
231
+ addFhirType(typeName) {
232
+ if (this.isFhirType(typeName)) {
233
+ this.fhirImports.add(typeName);
234
+ }
235
+ }
236
+ /**
237
+ * Adds a specific custom local type to imports.
238
+ */
239
+ addCustomType(typeName) {
240
+ if (!this.localInterfaceNames.has(typeName)) {
241
+ this.customImports.add(typeName);
242
+ }
243
+ }
244
+ /**
245
+ * Adds an import with an explicit relative path (e.g., for ValueSets in subdirectories).
246
+ * @param typeName - The type name to import
247
+ * @param relativePath - The relative path without extension (e.g., "./valuesets/ValueSet-Example")
248
+ */
249
+ addPathBasedImport(typeName, relativePath) {
250
+ this.pathBasedImports.set(typeName, relativePath);
251
+ }
252
+ }
@@ -518,6 +518,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
518
518
  fs.mkdirSync(outputDir, { recursive: true });
519
519
  // Load ValueSets from the directory
520
520
  const valueSets = readValueSetsFromDir(inputDir);
521
+ const valueSetCodesMap = readValueSetCodesFromDir(inputDir);
521
522
  console.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
522
523
  // Generate TypeScript files for ValueSets
523
524
  const valueSetOutputDir = path.join(outputDir, 'valuesets');
@@ -617,7 +618,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
617
618
  const full = path.join(inputDir, jf);
618
619
  try {
619
620
  const raw = JSON.parse(fs.readFileSync(full, 'utf-8'));
620
- await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSets, existingStructureDefinitions, profileIdToName, flags });
621
+ await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, flags });
621
622
  }
622
623
  catch (err) {
623
624
  console.error(`Failed processing JSON ${jf}:`, err);
@@ -1,72 +1,27 @@
1
1
  import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
2
2
  import { capitalize, sanitizeIdentifier, toPascalCase } from './utils.js';
3
- import { generateCodeUnionType, getUniformSystem } from './vsParser.js';
3
+ import { getUniformSystem } from './vsParser.js';
4
+ import { sanitizeValueSetName } from './valueSetGenerator.js';
4
5
  import { logger } from '../logger.js';
6
+ import { ImportManager } from './importManager.js';
5
7
  export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap) {
6
8
  const debug = (...args) => { if (process.env.DEBUG_FHIR_GEN === 'true')
7
9
  console.log('[gen:interfaces]', ...args); };
8
- const imports = new Set();
9
- const customImports = new Set();
10
10
  const interfaces = [];
11
11
  const localInterfaceNames = new Set();
12
12
  const writtenLines = new Set();
13
13
  // Track minimal alias extension interfaces generated in this file to avoid duplicates
14
14
  const generatedAliasTypes = new Set();
15
- // Track external profiled types that need type alias files
16
- const referencedExternalProfiles = new Map();
17
- const registerExternalProfile = (rawTypeName, profileUrl, baseType, isFhirBase = false) => {
18
- if (!rawTypeName)
19
- return;
20
- const sanitizedTypeName = sanitizeIdentifier(rawTypeName);
21
- const resolvedProfileUrl = profileUrl && profileUrl.length > 0
22
- ? profileUrl
23
- : `http://hl7.org/fhir/StructureDefinition/${sanitizedTypeName}`;
24
- let meta = referencedExternalProfiles.get(sanitizedTypeName);
25
- if (!meta) {
26
- meta = { profileUrl: resolvedProfileUrl, baseTypes: new Map() };
27
- referencedExternalProfiles.set(sanitizedTypeName, meta);
28
- }
29
- else if (profileUrl && profileUrl.length > 0 &&
30
- meta.profileUrl.startsWith('http://hl7.org/fhir/StructureDefinition/')) {
31
- meta.profileUrl = profileUrl;
32
- }
33
- if (!baseType || baseType === 'any')
34
- return;
35
- const sanitizedBase = sanitizeIdentifier(baseType);
36
- const existing = meta.baseTypes.get(sanitizedBase);
37
- if (!existing) {
38
- meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
39
- }
40
- else if (isFhirBase && !existing.isFhir) {
41
- existing.isFhir = true;
42
- }
43
- };
44
- // Helper to add a type import, checking if it's locally generated first
45
- const addTypeImport = (typeName) => {
15
+ // Initialize ImportManager to handle all import statements
16
+ const importManager = new ImportManager(localInterfaceNames, existingProfiles);
17
+ // Helper to add a type import using ImportManager
18
+ const addTypeImport = (typeName, profileUrl, baseType, isFhirBase) => {
46
19
  // If the type is a nested interface of the current profile, don't import it (defined in same file)
47
20
  const isNestedOfThisProfile = typeName.startsWith(interfaceName);
48
21
  if (isNestedOfThisProfile && localInterfaceNames.has(typeName)) {
49
22
  return; // don't add to imports - defined locally in this file
50
23
  }
51
- // If the type exists in our generated profiles/types, import locally
52
- if (existingProfiles?.has(typeName) || localInterfaceNames.has(typeName)) {
53
- customImports.add(typeName);
54
- }
55
- else if (isFhirType(typeName)) {
56
- imports.add(typeName);
57
- }
58
- else {
59
- // Check if this is a primitive type that shouldn't be fetched
60
- const primitiveTypeLowerCase = typeName.toLowerCase();
61
- const isPrimitiveLikeType = ['integer', 'unsignedint', 'positiveint', 'decimal', 'boolean', 'string',
62
- 'base64binary', 'instant', 'date', 'datetime', 'time', 'code', 'oid', 'id', 'markdown',
63
- 'uri', 'url', 'canonical', 'uuid'].includes(primitiveTypeLowerCase);
64
- customImports.add(typeName);
65
- // Track as external profile to be fetched if not already known and not a primitive type
66
- if (!isPrimitiveLikeType) {
67
- registerExternalProfile(typeName);
68
- }
69
- }
24
+ importManager.addType(typeName, profileUrl, baseType, isFhirBase);
70
25
  };
71
26
  // Helper to find exact interface block index (avoid prefix collisions like Foo and FooBar)
72
27
  const findInterfaceIndex = (name) => {
@@ -87,28 +42,57 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
87
42
  baseFieldByLastSegment.set(seg, bf);
88
43
  }
89
44
  // Helper to apply ValueSet binding constraints to CodeableConcept types
90
- const applyBindingConstraint = (fieldType, field) => {
91
- // Only apply to CodeableConcept types with bindings
92
- if (fieldType !== 'CodeableConcept' || !field.binding || !valueSets) {
45
+ const applyBindingConstraint = (fieldType, field, allFields) => {
46
+ // Only apply to CodeableConcept types
47
+ if (fieldType !== 'CodeableConcept' || !valueSets) {
93
48
  return fieldType;
94
49
  }
95
- const valueSet = valueSets.get(field.binding.uri);
50
+ // Check if this field or any of its slices have bindings
51
+ let binding = field.binding;
52
+ let isSlicedField = false;
53
+ // If this is a base field (no sliceName), check if any slices have more specific bindings
54
+ if (!field.sliceName) {
55
+ const slicesForThisField = allFields.filter(f => f.name === field.name &&
56
+ f.sliceName &&
57
+ f.binding?.uri);
58
+ // Use the first slice binding if available (slices typically have more specific constraints)
59
+ if (slicesForThisField.length > 0 && slicesForThisField[0].binding) {
60
+ binding = slicesForThisField[0].binding;
61
+ isSlicedField = true;
62
+ debug(`Using binding from slice ${slicesForThisField[0].sliceName} for ${field.name}`);
63
+ }
64
+ }
65
+ if (!binding) {
66
+ return fieldType;
67
+ }
68
+ const valueSet = valueSets.get(binding.uri);
96
69
  if (!valueSet || !valueSet.isSmall) {
97
70
  // ValueSet not found or too large for union type - just use CodeableConcept
98
- debug(`Skipping binding constraint for ${field.name}: ValueSet ${field.binding.uri} ${!valueSet ? 'not found' : 'too large'}`);
71
+ debug(`Skipping binding constraint for ${field.name}: ValueSet ${binding.uri} ${!valueSet ? 'not found' : 'too large'}`);
99
72
  return fieldType;
100
73
  }
101
- // Generate constrained type with specific codes
102
- const codeUnion = generateCodeUnionType(valueSet);
74
+ // Import the ValueSet module
75
+ const sanitizedVSName = sanitizeValueSetName(valueSet.name);
76
+ const vsTypeName = `${sanitizedVSName}Code`;
77
+ // Add import for the ValueSet type (from valuesets subdirectory)
78
+ importManager.addPathBasedImport(vsTypeName, './valuesets/ValueSet-' + sanitizedVSName);
79
+ // Generate constrained type using the imported ValueSet Code type
103
80
  const system = getUniformSystem(valueSet);
81
+ let constrainedType;
104
82
  if (system && valueSet.concepts.length > 0) {
105
- // All codes from same system - generate strict constraint
106
- return `CodeableConcept & { coding: Array<{ code: ${codeUnion}; system: "${system}" }> }`;
83
+ // All codes from same system - generate strict constraint with system
84
+ constrainedType = `CodeableConcept & { coding: Array<{ code: ${vsTypeName}; system: "${system}" }> }`;
107
85
  }
108
86
  else {
109
87
  // Mixed systems or no uniform system - just constrain the code
110
- return `CodeableConcept & { coding: Array<{ code: ${codeUnion} }> }`;
88
+ constrainedType = `CodeableConcept & { coding: Array<{ code: ${vsTypeName} }> }`;
111
89
  }
90
+ // If this is a sliced field (binding comes from a slice), create a union type
91
+ // to allow both constrained and unconstrained elements (open slicing)
92
+ if (isSlicedField) {
93
+ return `(${constrainedType} | CodeableConcept)`;
94
+ }
95
+ return constrainedType;
112
96
  };
113
97
  function processFields(fields, parentInterfaceName, parentFieldType, isProcessingSlice = false) {
114
98
  const interfaceLines = [];
@@ -551,8 +535,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
551
535
  aliasTypes.push(alias);
552
536
  }
553
537
  }
554
- if (!imports.has('Extension'))
555
- imports.add('Extension');
538
+ importManager.addFhirType('Extension');
556
539
  const union = ['Extension', ...aliasTypes].join(' | ');
557
540
  // extension on elements is always an optional array
558
541
  const extLine = `extension?: (${union})[];`;
@@ -666,7 +649,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
666
649
  // Special-case: Bundle.entry.resource should be FhirResource
667
650
  if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
668
651
  resolvedType = 'FhirResource';
669
- imports.add('FhirResource');
652
+ importManager.addFhirType('FhirResource');
670
653
  }
671
654
  // Final primitive normalization for nested branch as well
672
655
  resolvedType = mapTypeToTS(resolvedType);
@@ -737,7 +720,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
737
720
  const profileUrl = field.profileUrls?.[0] || (field.type && field.type.includes('http') ? field.type : undefined);
738
721
  const baseTypeName = baseFld?.type;
739
722
  const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
740
- registerExternalProfile(resolvedType, profileUrl, baseTypeName, baseIsFhir);
723
+ addTypeImport(resolvedType, profileUrl, baseTypeName, baseIsFhir);
741
724
  }
742
725
  if (!isPrimitiveType(resolvedType)) {
743
726
  if (isFhirType(resolvedType)) {
@@ -771,7 +754,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
771
754
  }
772
755
  }
773
756
  else {
774
- customImports.add(sanitizeIdentifier(resolvedType));
757
+ importManager.addCustomType(sanitizeIdentifier(resolvedType));
775
758
  }
776
759
  }
777
760
  }
@@ -1023,7 +1006,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1023
1006
  // Special-case: Bundle.entry.resource should be FhirResource, not Resource
1024
1007
  if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
1025
1008
  fieldType = 'FhirResource';
1026
- imports.add('FhirResource');
1009
+ importManager.addFhirType('FhirResource');
1027
1010
  }
1028
1011
  // Normalize canonical URLs to local type names
1029
1012
  if (!isPrimitiveType(fieldType) && !isFhirType(fieldType)) {
@@ -1044,8 +1027,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1044
1027
  aliasTypes.push(alias);
1045
1028
  }
1046
1029
  }
1047
- if (!imports.has('Extension'))
1048
- imports.add('Extension');
1030
+ importManager.addFhirType('Extension');
1049
1031
  const union = ['Extension', ...aliasTypes].join(' | ');
1050
1032
  fieldType = `(${union})[]`;
1051
1033
  // We will set isArray=false to avoid adding [] twice in the final line
@@ -1070,17 +1052,16 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1070
1052
  // Track this as an external profile that needs a type alias file
1071
1053
  const profileUrl = sl.profileUrls?.[0] || sl.type || '';
1072
1054
  if (profileUrl && !isFhirType(normalizedType)) {
1073
- registerExternalProfile(normalizedType, profileUrl, 'Identifier', true);
1055
+ addTypeImport(normalizedType, profileUrl, 'Identifier', true);
1074
1056
  }
1075
1057
  // Add to custom imports if not a FHIR type
1076
1058
  if (!isFhirType(normalizedType)) {
1077
- customImports.add(normalizedType);
1059
+ importManager.addCustomType(normalizedType);
1078
1060
  }
1079
1061
  }
1080
1062
  }
1081
1063
  if (profileTypes.length > 0) {
1082
- if (!imports.has('Identifier'))
1083
- imports.add('Identifier');
1064
+ importManager.addFhirType('Identifier');
1084
1065
  const union = ['Identifier', ...profileTypes].join(' | ');
1085
1066
  fieldType = `(${union})[]`;
1086
1067
  // We will set isArray=false to avoid adding [] twice in the final line
@@ -1115,8 +1096,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1115
1096
  aliasTypes.push(alias);
1116
1097
  }
1117
1098
  }
1118
- if (!imports.has('Extension'))
1119
- imports.add('Extension');
1099
+ importManager.addFhirType('Extension');
1120
1100
  const union = ['Extension', ...aliasTypes].join(' | ');
1121
1101
  const extLine = `extension?: (${union})[];`;
1122
1102
  // Determine parent type to extend
@@ -1135,7 +1115,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1135
1115
  if (!parentT || isPrimitiveLike(parentT)) {
1136
1116
  const elementIface = `${parentInterfaceName}${capitalize(fieldName)}Element`;
1137
1117
  // Ensure Element import
1138
- imports.add('Element');
1118
+ importManager.addFhirType('Element');
1139
1119
  if (!interfaces.some(i => i.startsWith(`export interface ${elementIface}`))) {
1140
1120
  interfaces.push(`export interface ${elementIface} extends Element {\n ${extLine}\n}`);
1141
1121
  }
@@ -1243,7 +1223,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1243
1223
  }
1244
1224
  }
1245
1225
  // Apply ValueSet binding constraints for CodeableConcept types
1246
- fieldType = applyBindingConstraint(fieldType, field);
1226
+ fieldType = applyBindingConstraint(fieldType, field, fields);
1247
1227
  // Preserve arrayness from base when known; if base metadata is missing and field name is a known repeating element (e.g., 'coverage' on Account),
1248
1228
  // default to array to avoid incorrect narrowing.
1249
1229
  // Skip adding [] if fieldType already contains array syntax (e.g., from identifier/extension union types)
@@ -1493,20 +1473,20 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1493
1473
  if (!isFhirType(sanitizedBase)) {
1494
1474
  // Check if this profile exists in our generated files
1495
1475
  if (existingProfiles?.has(sanitizedBase)) {
1496
- customImports.add(sanitizedBase);
1476
+ importManager.addCustomType(sanitizedBase);
1497
1477
  }
1498
1478
  else if (resourceType && isFhirType(resourceType)) {
1499
1479
  // Fallback to the actual FHIR resource type if base profile doesn't exist
1500
- imports.add(resourceType);
1480
+ importManager.addFhirType(resourceType);
1501
1481
  // Update baseResource to use the fallback for extends clause
1502
1482
  baseResource = resourceType;
1503
1483
  }
1504
1484
  else {
1505
- customImports.add(sanitizedBase);
1485
+ importManager.addCustomType(sanitizedBase);
1506
1486
  }
1507
1487
  }
1508
1488
  else {
1509
- imports.add(sanitizedBase);
1489
+ importManager.addFhirType(sanitizedBase);
1510
1490
  }
1511
1491
  }
1512
1492
  processFields(newFields, interfaceName);
@@ -1540,8 +1520,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1540
1520
  interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
1541
1521
  generatedAliasTypes.add(alias);
1542
1522
  }
1543
- if (!imports.has('Extension'))
1544
- imports.add('Extension');
1523
+ importManager.addFhirType('Extension');
1545
1524
  };
1546
1525
  // Helper to append a line to an existing interface body; if target doesn't exist and it's the root, synthesize it
1547
1526
  const appendToInterface = (name, line) => {
@@ -1551,10 +1530,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1551
1530
  const sanitizedBase = sanitizeIdentifier(baseResource);
1552
1531
  // Ensure import of the base resource type
1553
1532
  if (!isFhirType(sanitizedBase)) {
1554
- customImports.add(sanitizedBase);
1533
+ importManager.addCustomType(sanitizedBase);
1555
1534
  }
1556
1535
  else {
1557
- imports.add(sanitizedBase);
1536
+ importManager.addFhirType(sanitizedBase);
1558
1537
  }
1559
1538
  interfaces.push(`export interface ${rootName} extends ${sanitizedBase} {}`);
1560
1539
  }
@@ -1623,7 +1602,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1623
1602
  if (isPrim) {
1624
1603
  // Create Element sidecar with extension union
1625
1604
  const elementIface = `${rootName}${capitalize(seg)}Element`;
1626
- imports.add('Element');
1605
+ importManager.addFhirType('Element');
1627
1606
  if (!interfaces.some(i => i.startsWith(`export interface ${elementIface}`))) {
1628
1607
  interfaces.push(`export interface ${elementIface} extends Element {\n extension?: (${union})[];\n}`);
1629
1608
  }
@@ -1674,7 +1653,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1674
1653
  });
1675
1654
  }
1676
1655
  if (extendT)
1677
- imports.add(extendT);
1656
+ addTypeImport(extendT);
1678
1657
  const extendsClause = extendT ? ` extends ${extendT}` : '';
1679
1658
  if (!interfaces.some(i => i.startsWith(`export interface ${nestedName}`))) {
1680
1659
  interfaces.push(`export interface ${nestedName}${extendsClause} {\n extension?: (${union})[];\n}`);
@@ -1688,7 +1667,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1688
1667
  appendToInterface(rootName, `${seg}${opt}: ${nestedName}${isArr};`);
1689
1668
  // Also record import for known FHIR backbone
1690
1669
  if (extendT && isFhirType(extendT))
1691
- imports.add(extendT);
1670
+ addTypeImport(extendT);
1692
1671
  }
1693
1672
  }
1694
1673
  else if (segs.length === 2) {
@@ -1706,7 +1685,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1706
1685
  if (!interfaces.some(i => i.startsWith(`export interface ${lvl1Name}`))) {
1707
1686
  const extendsClause1 = extendT1 ? ` extends ${extendT1}` : '';
1708
1687
  if (extendT1 && isFhirType(extendT1))
1709
- imports.add(extendT1);
1688
+ addTypeImport(extendT1);
1710
1689
  interfaces.push(`export interface ${lvl1Name}${extendsClause1} {}`);
1711
1690
  appendToInterface(rootName, `${seg1}: ${lvl1Name};`);
1712
1691
  }
@@ -1728,7 +1707,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1728
1707
  }
1729
1708
  }
1730
1709
  if (extendT2 && isFhirType(extendT2))
1731
- imports.add(extendT2);
1710
+ addTypeImport(extendT2);
1732
1711
  const extendsClause2 = extendT2 ? ` extends ${extendT2}` : '';
1733
1712
  if (!interfaces.some(i => i.startsWith(`export interface ${lvl2Name}`))) {
1734
1713
  interfaces.push(`export interface ${lvl2Name}${extendsClause2} {\n extension?: (${union})[];\n}`);
@@ -1758,10 +1737,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1758
1737
  if (baseResource && !isBaseAbs) {
1759
1738
  const sanitizedBase = sanitizedBaseRes;
1760
1739
  if (!isFhirType(sanitizedBase)) {
1761
- customImports.add(sanitizedBase);
1740
+ importManager.addCustomType(sanitizedBase);
1762
1741
  }
1763
1742
  else {
1764
- imports.add(sanitizedBase);
1743
+ importManager.addFhirType(sanitizedBase);
1765
1744
  }
1766
1745
  interfaces.push(`export type ${interfaceName} = ${sanitizedBase};`);
1767
1746
  }
@@ -1791,8 +1770,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1791
1770
  aliases.push(alias);
1792
1771
  }
1793
1772
  }
1794
- if (!imports.has('Extension'))
1795
- imports.add('Extension');
1773
+ importManager.addFhirType('Extension');
1796
1774
  const union = ['Extension', ...aliases].join(' | ');
1797
1775
  const idx = findInterfaceIndex(interfaceName);
1798
1776
  if (idx >= 0 && !/\bextension\?\s*:\s*\(/.test(interfaces[idx])) {
@@ -1800,97 +1778,12 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1800
1778
  }
1801
1779
  }
1802
1780
  }
1803
- const customImportStatements = Array.from(customImports)
1804
- .map((type) => `import { ${type} } from "./${type}"`); // Adjust the path as needed
1805
- const importStatements = Array.from(imports)
1806
- .map((type) => `import { ${type} } from "fhir/r4";`)
1807
- .join("\n");
1808
- let output = `${importStatements}\n${customImportStatements.join("\n")}\n${interfaces.join("\n\n")}`;
1809
- // Ensure HumanName import if referenced but not imported (flattened case)
1810
- if (/\bHumanName\b/.test(output) && !/import \{[^}]*HumanName/.test(output)) {
1811
- output = `import { HumanName } from "fhir/r4";\n` + output;
1812
- }
1813
- // Minimal post-processing retained only for import pruning.
1814
- // Prune unused imported identifiers (both FHIR and custom) to satisfy lint rules.
1815
- output = output.replace(/import \{([^}]+)\} from "fhir\/r4";?/g, (full, inside) => {
1816
- const used = [];
1817
- const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
1818
- for (const name of specifiers) {
1819
- const re = new RegExp(`\\b${name}\\b`);
1820
- if (re.test(output.replace(full, '')))
1821
- used.push(name);
1822
- }
1823
- return used.length ? `import { ${used.join(', ')} } from "fhir/r4";` : '';
1824
- });
1825
- output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
1826
- const used = [];
1827
- const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
1828
- for (const name of specifiers) {
1829
- const re = new RegExp(`\\b${name}\\b`);
1830
- if (re.test(output.replace(full, '')))
1831
- used.push(name);
1832
- }
1833
- return used.length ? `import { ${used.join(', ')} } from "./${path}";` : '';
1834
- });
1835
- // Remove now-empty import lines and collapse multiple blank lines.
1836
- output = output.replace(/^\s*import \{\s*\}.*$/mg, '').replace(/\n{3,}/g, '\n\n');
1837
- // Post-processing: Move locally-generated types from fhir/r4 imports to local imports
1838
- if (existingProfiles && existingProfiles.size > 0) {
1839
- // Extract ALL types from fhir/r4 imports (there may be multiple import lines)
1840
- const fhirImportRegex = /import \{([^}]+)\} from "fhir\/r4";/g;
1841
- const allFhirTypes = [];
1842
- let match;
1843
- logger.log('[DEBUG post-processing]', { interfaceName, hasSimpleQuantity: existingProfiles.has('SimpleQuantity'), existingProfilesSize: existingProfiles.size });
1844
- while ((match = fhirImportRegex.exec(output)) !== null) {
1845
- const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
1846
- logger.log('[DEBUG found fhir/r4 import]', { types });
1847
- allFhirTypes.push(...types);
1848
- }
1849
- if (allFhirTypes.length > 0) {
1850
- const localTypes = [];
1851
- const remainingFhirTypes = [];
1852
- logger.log('[DEBUG separating types]', { allFhirTypes });
1853
- for (const typeName of allFhirTypes) {
1854
- if (existingProfiles.has(typeName)) {
1855
- logger.log('[DEBUG local type]', { typeName });
1856
- // Never move FhirResource to local imports; it must stay from fhir/r4
1857
- if (typeName !== 'FhirResource') {
1858
- localTypes.push(typeName);
1859
- }
1860
- else {
1861
- remainingFhirTypes.push(typeName);
1862
- }
1863
- }
1864
- else {
1865
- logger.log('[DEBUG fhir type]', { typeName });
1866
- remainingFhirTypes.push(typeName);
1867
- }
1868
- }
1869
- logger.log('[DEBUG separated]', { localTypes, remainingFhirTypes });
1870
- // Remove ALL old fhir/r4 import lines
1871
- output = output.replace(/import \{[^}]+\} from "fhir\/r4";\n?/g, '');
1872
- // Add back the imports at the top
1873
- let newImports = '';
1874
- if (remainingFhirTypes.length > 0) {
1875
- newImports += `import { ${remainingFhirTypes.join(', ')} } from "fhir/r4";\n`;
1876
- }
1877
- for (const localType of localTypes) {
1878
- newImports += `import { ${localType} } from "./${localType}";\n`;
1879
- }
1880
- // Insert new imports after any existing custom imports or at the top
1881
- if (newImports) {
1882
- const customImportMatch = output.match(/^(import \{[^}]+\} from "\.\/[^"]+";?\n)+/m);
1883
- if (customImportMatch) {
1884
- // Insert after existing custom imports
1885
- output = output.replace(customImportMatch[0], customImportMatch[0] + newImports);
1886
- }
1887
- else {
1888
- // Insert at the very top
1889
- output = newImports + output;
1890
- }
1891
- }
1892
- }
1893
- }
1781
+ // Generate import statements using ImportManager
1782
+ const importStatements = importManager.generateImportStatements();
1783
+ let output = `${importStatements}\n${interfaces.join("\n\n")}`;
1784
+ // Post-processing: prune unused imports and reorganize local types
1785
+ output = importManager.pruneUnusedImports(output);
1786
+ output = importManager.reorganizeImports(output);
1894
1787
  // Final logging to see all interfaces
1895
1788
  if (interfaceName.includes('ConditionDeBasis02')) {
1896
1789
  logger.log('[DEBUG FINAL INTERFACES]', {
@@ -1901,6 +1794,6 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1901
1794
  }
1902
1795
  return {
1903
1796
  content: output,
1904
- referencedExternalProfiles
1797
+ referencedExternalProfiles: importManager.getExternalProfiles()
1905
1798
  };
1906
1799
  }
@@ -249,6 +249,15 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
249
249
  console.warn(`StructureDefinition ${structureDefinition.id || "unknown"} has no elements.`);
250
250
  return { newFields: [], oldFields: baseFields };
251
251
  }
252
+ // Build a map from snapshot for enriching differential elements with binding information
253
+ const snapshotBindings = new Map();
254
+ if (structureDefinition.differential && structureDefinition.snapshot) {
255
+ for (const snapElem of structureDefinition.snapshot.element) {
256
+ if (snapElem.binding) {
257
+ snapshotBindings.set(snapElem.path, snapElem.binding);
258
+ }
259
+ }
260
+ }
252
261
  const newFields = elements.map((element) => {
253
262
  const name = element.path;
254
263
  // Use the profile name if it exists, otherwise fallback to the type code
@@ -327,7 +336,11 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
327
336
  .filter((constraint) => constraint !== null);
328
337
  // ValueSet binding sampling (deterministic): capture binding.valueSet and choose a code from local map if available
329
338
  let bindingMeta = undefined;
330
- const bindingUri = element.binding?.valueSet;
339
+ // First try element's own binding, then fall back to snapshot binding if available
340
+ let bindingUri = element.binding?.valueSet;
341
+ if (!bindingUri && snapshotBindings.has(element.path)) {
342
+ bindingUri = snapshotBindings.get(element.path)?.valueSet;
343
+ }
331
344
  // Extract sample code from pattern if present (e.g., patternCodeableConcept or patternCoding)
332
345
  let patternCode;
333
346
  if (patternValue) {
@@ -60,13 +60,17 @@ export function alignFields(newFields, oldFields) {
60
60
  }
61
61
  export function mergeFields(newFields, oldFields) {
62
62
  const fieldMap = new Map();
63
+ // Create unique key for field that includes sliceName if present
64
+ const getFieldKey = (field) => {
65
+ return field.sliceName ? `${field.name}:${field.sliceName}` : field.name;
66
+ };
63
67
  // Add oldFields to the map
64
68
  for (const field of oldFields) {
65
- fieldMap.set(field.name, field);
69
+ fieldMap.set(getFieldKey(field), field);
66
70
  }
67
71
  // Overwrite or add newFields to the map
68
72
  for (const field of newFields) {
69
- fieldMap.set(field.name, field);
73
+ fieldMap.set(getFieldKey(field), field);
70
74
  }
71
75
  // Return the merged fields as an array
72
76
  return Array.from(fieldMap.values());
@@ -45,6 +45,43 @@ export function generateValidateProfileFunction(interfaceName, fields // Array o
45
45
  });
46
46
  }
47
47
  });
48
+ // Add slice cardinality validation for sliced fields with bindings
49
+ const sliceValidations = [];
50
+ const slicesByBase = new Map();
51
+ // Group slices by their base field name
52
+ fields.forEach(field => {
53
+ if (field.sliceName && field.binding?.uri) {
54
+ const baseName = field.name;
55
+ if (!slicesByBase.has(baseName)) {
56
+ slicesByBase.set(baseName, []);
57
+ }
58
+ slicesByBase.get(baseName).push(field);
59
+ }
60
+ });
61
+ // Generate validation for each sliced field
62
+ slicesByBase.forEach((slices, baseName) => {
63
+ slices.forEach(slice => {
64
+ const min = typeof slice.min === "number" ? slice.min : (slice.isOptional ? 0 : 1);
65
+ if (min > 0 && slice.binding?.codes && slice.binding.codes.length > 0) {
66
+ const relPath = baseName.replace(/^[^.]+\./, "");
67
+ const codes = slice.binding.codes.map(c => `"${c.code}"`).join(", ");
68
+ // Escape double quotes in codes list for use in error message string
69
+ const escapedCodes = slice.binding.codes.map(c => c.code).join(", ");
70
+ const minText = min === 1 ? "at least one" : `at least ${min}`;
71
+ const sliceLabel = slice.sliceName || "slice";
72
+ // Sanitize slice name for use as JavaScript variable name
73
+ const varName = sliceLabel.replace(/[^a-zA-Z0-9_]/g, '_');
74
+ sliceValidations.push(`
75
+ // Slice cardinality check for ${baseName}:${sliceLabel}
76
+ const ${varName}Count = resource.${relPath}?.filter(item =>
77
+ item.coding?.some(coding => [${codes}].includes(coding.code as string))
78
+ ).length || 0;
79
+ if (${varName}Count < ${min}) {
80
+ errors.push("Slice '${sliceLabel}' on ${relPath}: must have ${minText} element(s) with codes: ${escapedCodes}");
81
+ }`);
82
+ }
83
+ });
84
+ });
48
85
  // Filter out constraints that require terminology or reference resolution which our current
49
86
  // synchronous evaluator does not support in parity mode (memberOf, resolve).
50
87
  const filteredConstraints = uniqueConstraints.filter(c => !/(\bmemberOf\b|\bresolve\b)/.test(c.expression || ""));
@@ -61,20 +98,18 @@ export function generateValidateProfileFunction(interfaceName, fields // Array o
61
98
  }`;
62
99
  })
63
100
  .join("\n");
64
- const hasConstraints = filteredConstraints.length > 0;
101
+ const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0;
65
102
  // Always emit a uniform signature validateX(resource: X) so callers don't branch on arity.
66
103
  // Only import fhirpath and run evaluation logic when there are constraints.
67
104
  if (!hasConstraints) {
68
105
  return `
69
106
  export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {\n // Touch the parameter so eslint no-unused-vars doesn't flag it when there are no constraints.\n void resource;\n return { errors: [], warnings: [] };\n}`;
70
107
  }
71
- return `
72
- import fhirpath from "fhirpath";
73
-
74
- export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {
108
+ const fhirpathImport = filteredConstraints.length > 0 ? 'import fhirpath from "fhirpath";\n\n' : '';
109
+ return `${fhirpathImport}export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {
75
110
  const errors: string[] = [];
76
111
  const warnings: string[] = [];
77
- ${validationLogic}
112
+ ${validationLogic}${sliceValidations.join('')}
78
113
  return { errors, warnings };
79
114
  }`;
80
115
  }
@@ -56,7 +56,7 @@ export function generateValueSetTypeScript(valueSet) {
56
56
  /**
57
57
  * Sanitize ValueSet name for use as TypeScript identifier
58
58
  */
59
- function sanitizeValueSetName(name) {
59
+ export function sanitizeValueSetName(name) {
60
60
  // Remove common prefixes
61
61
  let sanitized = name.replace(/^ValueSet[-_]?/i, '');
62
62
  // Split on non-alphanumeric and capitalize each part
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/main.js",
@@ -36,10 +36,30 @@
36
36
  },
37
37
  "keywords": [
38
38
  "fhir",
39
- "typescript"
39
+ "typescript",
40
+ "code-generation",
41
+ "healthcare",
42
+ "hl7",
43
+ "fhir-profiles",
44
+ "implementation-guide",
45
+ "validation",
46
+ "fhir-r4",
47
+ "fhir-to-typescript"
40
48
  ],
41
49
  "author": "Maximilian Nussbaumer",
42
50
  "license": "ISC",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/quotentiroler/BabelFHIR-ts.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/quotentiroler/BabelFHIR-ts/issues"
57
+ },
58
+ "homepage": "https://github.com/quotentiroler/BabelFHIR-ts#readme",
59
+ "engines": {
60
+ "node": ">=18.0.0",
61
+ "npm": ">=9.0.0"
62
+ },
43
63
  "devDependencies": {
44
64
  "@eslint/js": "^9.24.0",
45
65
  "@types/node": "^22.14.1",