babelfhir-ts 1.5.2 → 1.5.3

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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Backbone slice typing: generates discriminated union types for sliced backbone elements.
3
+ *
4
+ * When a profile defines named slices on a backbone element array (e.g., Observation.component),
5
+ * this module generates per-slice interfaces extending the base backbone type and updates
6
+ * the parent array field to use a union of those interfaces.
7
+ *
8
+ * Example output for BloodPressure.component with systolic/diastolic slices:
9
+ * ```ts
10
+ * export interface BloodPressureComponentSystolic extends ObservationComponent {
11
+ * code: { coding: [{ system: "http://loinc.org"; code: "8480-6" }] };
12
+ * }
13
+ * export interface BloodPressureComponentDiastolic extends ObservationComponent {
14
+ * code: { coding: [{ system: "http://loinc.org"; code: "8462-4" }] };
15
+ * }
16
+ * export interface BloodPressure extends Observation {
17
+ * component?: (BloodPressureComponentSystolic | BloodPressureComponentDiastolic | ObservationComponent)[];
18
+ * }
19
+ * ```
20
+ */
21
+ import { capitalize, sanitizeIdentifier } from '../../core/utils.js';
22
+ import { getRules } from '../../fhir/versionContext.js';
23
+ import { logger } from '../../../logger.js';
24
+ const log = logger.withTag('backbone-slices');
25
+ /**
26
+ * Post-process backbone element slices: generate per-slice interfaces and update
27
+ * the parent array property to a union type.
28
+ */
29
+ export function generateBackboneSliceTypes(ctx) {
30
+ const { interfaceName, baseResource, newFields, baseFields, interfaces, importManager, inferBackboneType } = ctx;
31
+ const rules = getRules();
32
+ // 1. Find all sliced backbone element arrays
33
+ const sliceGroups = collectSliceGroups(newFields, baseResource);
34
+ if (sliceGroups.length === 0)
35
+ return;
36
+ for (const group of sliceGroups) {
37
+ const { parentField, sliceFields, childFields, slicingRules } = group;
38
+ const fieldName = parentField.name.split('.').pop();
39
+ // Determine the base backbone type for this field
40
+ let backboneType = inferBackboneType(baseResource, fieldName);
41
+ if (!backboneType) {
42
+ // Try from base fields — only accept concrete FHIR types that we can
43
+ // actually extend (not 'BackboneElement', 'Element', 'any', or unknown types).
44
+ // BackboneElement/Element are too generic: the parent field expects a specific
45
+ // subtype (e.g. ExplanationOfBenefitItemAdjudication) and extending the raw
46
+ // generic type produces TS2430.
47
+ const baseFld = baseFields.find(f => f.name === parentField.name && !f.sliceName);
48
+ const rawType = baseFld?.type || parentField.type;
49
+ if (rawType && rawType !== 'BackboneElement' && rawType !== 'Element' && rules.isFhirType(rawType)) {
50
+ backboneType = rawType;
51
+ }
52
+ }
53
+ if (!backboneType) {
54
+ log.debug(`No backbone type found for ${parentField.name}, skipping slice typing`);
55
+ continue;
56
+ }
57
+ // Ensure the backbone type is imported
58
+ if (rules.isFhirType(backboneType)) {
59
+ importManager.addFhirType(backboneType);
60
+ }
61
+ // 2. Generate per-slice interfaces
62
+ const sliceTypeNames = [];
63
+ for (const sliceField of sliceFields) {
64
+ const sliceName = sliceField.sliceName;
65
+ const sliceTypeName = `${interfaceName}${capitalize(fieldName)}${capitalize(sanitizeIdentifier(sliceName))}`;
66
+ // Collect children for this specific slice
67
+ const sliceElementId = sliceField.elementId || `${parentField.name}:${sliceName}`;
68
+ const sliceChildren = childFields.filter(f => {
69
+ const childId = f.elementId || f.name;
70
+ return childId.startsWith(sliceElementId + '.');
71
+ });
72
+ // Build interface body lines from slice children and the slice's own constraints
73
+ const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType);
74
+ if (bodyLines.length === 0) {
75
+ // No constrainable properties — skip this slice interface
76
+ log.debug(`No constraints for slice ${sliceName} of ${parentField.name}, skipping`);
77
+ continue;
78
+ }
79
+ // Register any FHIR types used in the slice body
80
+ for (const fhirType of referencedFhirTypes) {
81
+ importManager.addFhirType(fhirType);
82
+ }
83
+ // Emit the slice interface
84
+ const sliceInterface = `export interface ${sliceTypeName} extends ${backboneType} {\n${bodyLines.map(l => ` ${l}`).join('\n')}\n}`;
85
+ // Add before the root interface to maintain declaration order
86
+ const rootIdx = interfaces.findIndex(i => i.startsWith(`export interface ${interfaceName} `) || i.startsWith(`export interface ${interfaceName}<`));
87
+ if (rootIdx >= 0) {
88
+ interfaces.splice(rootIdx, 0, sliceInterface);
89
+ }
90
+ else {
91
+ interfaces.push(sliceInterface);
92
+ }
93
+ sliceTypeNames.push(sliceTypeName);
94
+ }
95
+ if (sliceTypeNames.length === 0)
96
+ continue;
97
+ // 3. Update the parent field's type in the root interface to use the union
98
+ const isOpen = slicingRules !== 'closed';
99
+ const unionMembers = [...sliceTypeNames];
100
+ if (isOpen) {
101
+ unionMembers.push(backboneType);
102
+ }
103
+ const unionType = unionMembers.length === 1
104
+ ? `${unionMembers[0]}[]`
105
+ : `(${unionMembers.join(' | ')})[]`;
106
+ // Find and update the field in the root interface
107
+ updateRootInterfaceField(interfaces, interfaceName, fieldName, parentField.isOptional, unionType, baseResource);
108
+ }
109
+ }
110
+ /**
111
+ * Collect sliced backbone element groups from the fields.
112
+ */
113
+ function collectSliceGroups(fields, baseResource) {
114
+ const groups = [];
115
+ const seenPaths = new Set();
116
+ for (const field of fields) {
117
+ if (field.sliceName)
118
+ continue; // Skip slice entries themselves
119
+ if (!field.isArray)
120
+ continue; // Slicing is only on arrays
121
+ const path = field.name;
122
+ if (seenPaths.has(path))
123
+ continue;
124
+ // Check if there are named slices at this path
125
+ const sliceFields = fields.filter(f => f.name === path && f.sliceName);
126
+ if (sliceFields.length === 0)
127
+ continue;
128
+ // Must be a true backbone element — only process fields whose base FHIR type
129
+ // is BackboneElement or Element (inline nested types). Skip complex DataTypes
130
+ // like Identifier, CodeableConcept, ContactPoint, etc. — those have slices
131
+ // handled by the main type mapping and profiled type reference system.
132
+ const baseType = field.baseTypeCode || field.type || '';
133
+ if (baseType !== 'BackboneElement' && baseType !== 'Element')
134
+ continue;
135
+ // Skip extension slices — handled separately by postProcessExtensions
136
+ const fieldName = path.split('.').pop() || '';
137
+ if (fieldName === 'extension' || fieldName === 'modifierExtension')
138
+ continue;
139
+ // Skip Bundle.entry and similar resource-container slices — these use profiled
140
+ // type references that are handled by the external profile resolution system
141
+ if (fieldName === 'entry')
142
+ continue;
143
+ // Must be a root-level field (direct child of the base resource)
144
+ const parts = path.split('.');
145
+ if (parts.length !== 2)
146
+ continue;
147
+ if (baseResource && parts[0] !== baseResource)
148
+ continue;
149
+ // Gather all child fields for any slice of this element
150
+ const childFields = fields.filter(f => {
151
+ if (f.name === path)
152
+ return false; // Skip the parent itself
153
+ const childId = f.elementId || f.name;
154
+ // Match children that belong to ANY slice of this element
155
+ return childId.startsWith(`${path}:`) || (f.name.startsWith(`${path}.`) && f.name !== path);
156
+ });
157
+ // Determine slicing rules from the parent field's metadata
158
+ // The slicing rules come from the SD's element.slicing.rules
159
+ // We default to 'open' since that's the FHIR default
160
+ const slicingRules = field.slicingRules || 'open';
161
+ seenPaths.add(path);
162
+ groups.push({ parentField: field, sliceFields, childFields, slicingRules });
163
+ }
164
+ return groups;
165
+ }
166
+ /**
167
+ * Build the body lines for a slice-specific interface.
168
+ */
169
+ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType) {
170
+ const lines = [];
171
+ const referencedFhirTypes = [];
172
+ // Add pattern constraint from the slice field itself (discriminator value)
173
+ if (sliceField.patternConstraint && typeof sliceField.patternConstraint === 'object') {
174
+ const pattern = sliceField.patternConstraint;
175
+ if ('coding' in pattern && Array.isArray(pattern.coding)) {
176
+ // patternCodeableConcept on the slice itself (rare — usually on a child like .code)
177
+ const codingLiteral = formatCodingLiteral(pattern.coding);
178
+ if (codingLiteral) {
179
+ lines.push(`coding: ${codingLiteral};`);
180
+ }
181
+ }
182
+ }
183
+ // Process direct children of the slice
184
+ for (const child of sliceChildren) {
185
+ const childId = child.elementId || child.name;
186
+ const relativePath = childId.substring(sliceElementId.length + 1);
187
+ // Only handle direct children (no dots in relative path)
188
+ if (relativePath.includes('.'))
189
+ continue;
190
+ // Skip slice notation in child paths
191
+ if (relativePath.includes(':'))
192
+ continue;
193
+ const propName = relativePath.replace(/\[x\]$/, '');
194
+ if (child.fixedValue !== undefined) {
195
+ // Fixed value constraint
196
+ const fixedLiteral = JSON.stringify(child.fixedValue);
197
+ lines.push(`${propName}: ${child.isArray ? `[${fixedLiteral}]` : fixedLiteral};`);
198
+ }
199
+ else if (child.patternConstraint && typeof child.patternConstraint === 'object') {
200
+ const pattern = child.patternConstraint;
201
+ if ('coding' in pattern && Array.isArray(pattern.coding)) {
202
+ // patternCodeableConcept
203
+ const codingLiteral = formatCodingLiteral(pattern.coding);
204
+ if (codingLiteral) {
205
+ const objectLiteral = `{ coding: ${codingLiteral} }`;
206
+ lines.push(`${propName}: ${child.isArray ? `[${objectLiteral}]` : objectLiteral};`);
207
+ }
208
+ }
209
+ else if ('code' in pattern && typeof pattern.code === 'string') {
210
+ // patternCoding
211
+ const parts = [];
212
+ if ('system' in pattern && typeof pattern.system === 'string') {
213
+ parts.push(`system: "${pattern.system}"`);
214
+ }
215
+ parts.push(`code: "${pattern.code}"`);
216
+ const objectLiteral = `{ ${parts.join('; ')} }`;
217
+ lines.push(`${propName}: ${child.isArray ? `[${objectLiteral}]` : objectLiteral};`);
218
+ }
219
+ }
220
+ else if (!child.isOptional && child.type) {
221
+ // Required field without constraints — mark as required in the interface.
222
+ // Skip children whose type can't produce a valid extends:
223
+ // - BackboneElement/Element: parent expects a concrete subtype
224
+ // - 'any': unresolvable type (e.g. content references like Parameters.parameter.part)
225
+ // that mapTypeToTS would map to 'string', producing an incompatible field.
226
+ if (child.type === 'BackboneElement' || child.type === 'Element' || child.type === 'any')
227
+ continue;
228
+ const rawType = getRules().mapTypeToTS(child.type);
229
+ const mappedType = sanitizeIdentifier(rawType);
230
+ // Skip profiled type references that would need separate imports
231
+ // (e.g., Composition-uv-ips) — these are better handled by the main processor
232
+ if (mappedType !== rawType && child.isProfiled)
233
+ continue;
234
+ const isArray = child.isArray ? '[]' : '';
235
+ lines.push(`${propName}: ${mappedType}${isArray};`);
236
+ // Track FHIR types that need importing
237
+ if (getRules().isFhirType(mappedType)) {
238
+ referencedFhirTypes.push(mappedType);
239
+ }
240
+ }
241
+ }
242
+ return { lines, referencedFhirTypes };
243
+ }
244
+ /**
245
+ * Format an array of coding objects as a TypeScript literal type.
246
+ */
247
+ function formatCodingLiteral(codings) {
248
+ const entries = [];
249
+ for (const coding of codings) {
250
+ if (!coding || typeof coding !== 'object')
251
+ continue;
252
+ const c = coding;
253
+ const parts = [];
254
+ if (typeof c.system === 'string')
255
+ parts.push(`system: "${c.system}"`);
256
+ if (typeof c.code === 'string')
257
+ parts.push(`code: "${c.code}"`);
258
+ if (parts.length > 0) {
259
+ entries.push(`{ ${parts.join('; ')} }`);
260
+ }
261
+ }
262
+ if (entries.length === 0)
263
+ return null;
264
+ return `[${entries.join(', ')}]`;
265
+ }
266
+ /**
267
+ * Update the root interface to use the union type for a sliced backbone field.
268
+ */
269
+ function updateRootInterfaceField(interfaces, interfaceName, fieldName, isOptional, unionType, baseResource) {
270
+ const optMark = isOptional ? '?' : '';
271
+ const newFieldLine = ` ${fieldName}${optMark}: ${unionType};`;
272
+ // Try to find an existing interface declaration
273
+ const rootIdx = interfaces.findIndex(i => i.startsWith(`export interface ${interfaceName} `) || i.startsWith(`export interface ${interfaceName}<`));
274
+ if (rootIdx >= 0) {
275
+ const iface = interfaces[rootIdx];
276
+ const fieldPattern = new RegExp(`^(\\s+)${fieldName}[?]?:\\s*.+;`, 'm');
277
+ if (fieldPattern.test(iface)) {
278
+ // Replace existing field line
279
+ interfaces[rootIdx] = iface.replace(fieldPattern, newFieldLine);
280
+ }
281
+ else {
282
+ // Field not yet in the interface — inject it before the closing brace
283
+ const closingBrace = iface.lastIndexOf('}');
284
+ if (closingBrace > 0) {
285
+ interfaces[rootIdx] = iface.slice(0, closingBrace) + newFieldLine + '\n' + iface.slice(closingBrace);
286
+ }
287
+ }
288
+ return;
289
+ }
290
+ // Try to find a type alias (e.g., "export type BloodPressure = Observation;")
291
+ const aliasIdx = interfaces.findIndex(i => i.startsWith(`export type ${interfaceName} =`));
292
+ if (aliasIdx >= 0) {
293
+ // Convert type alias to interface with the field
294
+ const aliasMatch = interfaces[aliasIdx].match(/export type \S+ = (\S+);/);
295
+ const baseType = aliasMatch ? aliasMatch[1] : baseResource || '';
296
+ interfaces[aliasIdx] = `export interface ${interfaceName} extends ${baseType} {\n${newFieldLine}\n}`;
297
+ }
298
+ }
@@ -6,6 +6,7 @@ import { ImportManager } from './importManager.js';
6
6
  import { getRules, ctx as versionCtx } from '../../fhir/versionContext.js';
7
7
  import { postProcessExtensions, safetyNetRootExtensions } from './postProcessExtensions.js';
8
8
  import { processFields } from './interfaceFieldProcessor.js';
9
+ import { generateBackboneSliceTypes } from './backboneSliceTyping.js';
9
10
  const log = logger.withTag('interfaces');
10
11
  export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap, profileIdToName, profileUrlToName, isLogicalModel = false) {
11
12
  const debug = (...args) => log.debug(...args);
@@ -243,6 +244,17 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
243
244
  interfaces.push(`export interface ${interfaceName} {}`);
244
245
  }
245
246
  }
247
+ // Post-pass: generate discriminated union types for sliced backbone elements.
248
+ // Must run after type alias generation so it can convert aliases to interfaces if needed.
249
+ generateBackboneSliceTypes({
250
+ interfaceName,
251
+ baseResource,
252
+ newFields,
253
+ baseFields,
254
+ interfaces,
255
+ importManager,
256
+ inferBackboneType,
257
+ });
246
258
  // Generate import statements using ImportManager
247
259
  const importStatements = importManager.generateImportStatements();
248
260
  let output = `${importStatements}\n${interfaces.join("\n\n")}`;
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Zod refinement builder for pattern constraints, slice cardinalities, and FHIRPath invariants.
2
+ * Zod refinement builder for pattern constraints, slice cardinalities, FHIRPath invariants,
3
+ * and required ValueSet bindings.
3
4
  */
5
+ import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
4
6
  /** Escape double-quote characters in refinement messages */
5
7
  function escStr(s) {
6
8
  return s.replace(/"/g, '\\"');
@@ -9,8 +11,9 @@ function escStr(s) {
9
11
  * Build refinement chains for pattern constraints, required slice cardinalities,
10
12
  * and FHIRPath invariants. Returns refinement code strings plus tracked constraint metadata.
11
13
  */
12
- export function buildRefinements(allFields, rootPrefix) {
14
+ export function buildRefinements(allFields, rootPrefix, valueSets) {
13
15
  const refinements = [];
16
+ const bindingVsImports = new Map();
14
17
  // Build set of direct (root-level) field names for safety checks
15
18
  const directFieldNames = new Set();
16
19
  for (const field of allFields) {
@@ -176,5 +179,69 @@ export function buildRefinements(allFields, rootPrefix) {
176
179
  refinements.push(`.refine(d => (d as any).meta?.${subField} !== undefined, { message: "meta.${subField} must be present", when: () => true })`);
177
180
  }
178
181
  }
179
- return { refinements, supportedConstraints, unsupportedConstraints };
182
+ // 5. Required ValueSet bindings on CodeableConcept / Coding fields
183
+ // When a field has binding.strength === 'required' and we have resolved codes,
184
+ // emit a refinement that checks at least one coding.code is in the allowed set.
185
+ for (const field of allFields) {
186
+ const parts = field.name.split('.');
187
+ if (parts.length !== 2 || parts[0] !== rootPrefix)
188
+ continue;
189
+ if (field.sliceName)
190
+ continue;
191
+ const type = field.type || field.baseTypeCode || '';
192
+ if (type !== 'CodeableConcept' && type !== 'Coding')
193
+ continue;
194
+ if (field.binding?.strength !== 'required')
195
+ continue;
196
+ const rel = parts[1];
197
+ // Skip choice-type placeholders (code[x], value[x], etc.) — the binding applies
198
+ // to the resolved variant (e.g. codeCodeableConcept) which we cannot reliably
199
+ // identify here. Emitting d.code[x] would produce invalid TypeScript.
200
+ if (rel.includes('[x]'))
201
+ continue;
202
+ if (!directFieldNames.has(rel))
203
+ continue;
204
+ // Resolve codes: prefer field.binding.codes, then fall back to parsed ValueSets
205
+ let codes;
206
+ let vsDisplayName = '';
207
+ if (field.binding.codes && field.binding.codes.length > 0) {
208
+ codes = field.binding.codes.map(c => c.code);
209
+ vsDisplayName = field.binding.uri?.split('/').pop() || 'ValueSet';
210
+ }
211
+ else if (field.binding.uri && valueSets) {
212
+ const vsUri = field.binding.uri.split('|')[0];
213
+ const vs = valueSets.get(vsUri) || valueSets.get(field.binding.uri);
214
+ if (vs && vs.concepts.length > 0 && vs.concepts.length <= 200) {
215
+ codes = vs.concepts.map(c => c.code);
216
+ vsDisplayName = vs.name || vsUri.split('/').pop() || 'ValueSet';
217
+ // Import the ValueSet type so the codes are available at runtime
218
+ const sanitizedName = sanitizeValueSetName(vs.name);
219
+ bindingVsImports.set(sanitizedName, `./valuesets/ValueSet-${sanitizedName}.js`);
220
+ }
221
+ }
222
+ if (!codes || codes.length === 0)
223
+ continue;
224
+ const codesLiteral = `[${codes.map(c => JSON.stringify(c)).join(', ')}]`;
225
+ const msgBase = `None of the codings provided are in the value set '${escStr(vsDisplayName)}' (${escStr(field.binding.uri || '')})`;
226
+ if (type === 'CodeableConcept') {
227
+ if (field.isArray) {
228
+ // Array of CodeableConcept: each entry must have at least one valid coding
229
+ refinements.push(`.refine(d => { const arr = (d as any).${rel}; if (!Array.isArray(arr)) return true; const allowed = ${codesLiteral}; return arr.every((cc: any) => cc?.coding?.some((c: any) => allowed.includes(c?.code))); }, { message: "${escStr(msgBase)}", when: () => true })`);
230
+ }
231
+ else {
232
+ // Single CodeableConcept: coding must include at least one valid code
233
+ refinements.push(`.refine(d => { const cc = (d as any).${rel}; if (!cc) return true; const allowed = ${codesLiteral}; return cc.coding?.some((c: any) => allowed.includes(c?.code)); }, { message: "${escStr(msgBase)}", when: () => true })`);
234
+ }
235
+ }
236
+ else {
237
+ // Coding field
238
+ if (field.isArray) {
239
+ refinements.push(`.refine(d => { const arr = (d as any).${rel}; if (!Array.isArray(arr)) return true; const allowed = ${codesLiteral}; return arr.every((c: any) => allowed.includes(c?.code)); }, { message: "${escStr(msgBase)}", when: () => true })`);
240
+ }
241
+ else {
242
+ refinements.push(`.refine(d => { const c = (d as any).${rel}; if (!c) return true; const allowed = ${codesLiteral}; return allowed.includes(c.code); }, { message: "${escStr(msgBase)}", when: () => true })`);
243
+ }
244
+ }
245
+ }
246
+ return { refinements, supportedConstraints, unsupportedConstraints, valueSetImports: bindingVsImports };
180
247
  }
@@ -249,13 +249,19 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
249
249
  for (const [vsTypeName, vsPath] of valueSetImports) {
250
250
  lines.push(`import { ${vsTypeName} } from "${vsPath}";`);
251
251
  }
252
- // Collect refinements (pattern constraints, slice cardinality, FHIRPath invariants)
252
+ // Collect refinements (pattern constraints, slice cardinality, FHIRPath invariants, required bindings)
253
253
  // Only emit refinements for Resource profiles (not DataType profiles like Identifier, Coding)
254
254
  // because DataType random() methods don't produce pattern-aware data.
255
255
  const isDataTypeProfile = getZodPackageTypes().has(rootPrefix);
256
256
  const refinementResult = isDataTypeProfile
257
- ? { refinements: [], supportedConstraints: [], unsupportedConstraints: [] }
258
- : buildRefinements(allFields, rootPrefix);
257
+ ? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map() }
258
+ : buildRefinements(allFields, rootPrefix, valueSets);
259
+ // Merge ValueSet imports from binding refinements
260
+ for (const [name, importPath] of refinementResult.valueSetImports) {
261
+ if (!valueSetImports.has(name)) {
262
+ valueSetImports.set(name, importPath);
263
+ }
264
+ }
259
265
  lines.push('');
260
266
  // Emit schema
261
267
  const schemaName = `${interfaceName}Schema`;
@@ -26,6 +26,20 @@ function findRepoRoot() {
26
26
  }
27
27
  return path.resolve(selfDir, '..', '..', '..', '..');
28
28
  }
29
+ /** Recursively copy a directory's contents, handling nested subdirectories. */
30
+ function copyDirRecursive(src, dest) {
31
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
32
+ const srcPath = path.join(src, entry.name);
33
+ const destPath = path.join(dest, entry.name);
34
+ if (entry.isDirectory()) {
35
+ fs.mkdirSync(destPath, { recursive: true });
36
+ copyDirRecursive(srcPath, destPath);
37
+ }
38
+ else {
39
+ fs.copyFileSync(srcPath, destPath);
40
+ }
41
+ }
42
+ }
29
43
  export function installBaseZodTypes(outputDir) {
30
44
  const slug = versionSlug();
31
45
  const pkgDir = path.join(outputDir, 'node_modules', '@babelfhir-ts', 'zod');
@@ -42,9 +56,7 @@ export function installBaseZodTypes(outputDir) {
42
56
  // Copy all runtime dist files into dist/<slug>/ to match package.json exports
43
57
  const distSlugDir = path.join(pkgDir, 'dist', slug);
44
58
  fs.mkdirSync(distSlugDir, { recursive: true });
45
- for (const file of fs.readdirSync(runtimeDist)) {
46
- fs.copyFileSync(path.join(runtimeDist, file), path.join(distSlugDir, file));
47
- }
59
+ copyDirRecursive(runtimeDist, distSlugDir);
48
60
  return;
49
61
  }
50
62
  // Fallback: generate stubs with both types (.d.ts) AND runtime JS (.js)
@@ -26,7 +26,7 @@ export function buildProfileRegistries(structureDefinitions, fhirInterfaceNames)
26
26
  for (const sd of structureDefinitions) {
27
27
  const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
28
28
  let name = toPascalCase(rawInterfaceName);
29
- if (fhirInterfaceNames.includes(name)) {
29
+ if (fhirInterfaceNames.includes(name) && sd.derivation !== 'specialization') {
30
30
  name = `${name}Extension`;
31
31
  }
32
32
  if (name) {
@@ -85,7 +85,7 @@ export async function processStructureDefinition(sd, ctx) {
85
85
  const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
86
86
  let interfaceName = toPascalCase(rawInterfaceName);
87
87
  registerProfile(sd.url || '', interfaceName);
88
- if (versionCtx().interfaceNames.includes(interfaceName)) {
88
+ if (versionCtx().interfaceNames.includes(interfaceName) && sd.derivation !== 'specialization') {
89
89
  logger.log(`Interface name '${interfaceName}' conflicts with FHIR base type, adding 'Extension' suffix`);
90
90
  interfaceName = `${interfaceName}Extension`;
91
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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",