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