babelfhir-ts 1.5.5 → 1.5.7
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.
|
@@ -125,47 +125,53 @@ function npmInstall(packagePath) {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
128
|
+
* Bust stale bun cache entries for a local-tarball dependency.
|
|
129
|
+
*
|
|
130
|
+
* Bun caches extracted tgz content keyed by file *path*, not content hash.
|
|
131
|
+
* Replacing a tgz at the same path silently serves stale content — even with
|
|
132
|
+
* `--force`. Two cache locations are relevant:
|
|
133
|
+
*
|
|
134
|
+
* 1. **node_modules/.bun/** (bun ≥ 1.1) — per-project extraction cache.
|
|
135
|
+
* 2. **node_modules/<packageName>/** — the already-installed copy. When we run
|
|
136
|
+
* `bun install` (bare, dep already in package.json) bun skips re-extraction
|
|
137
|
+
* if the directory already exists, regardless of whether the underlying tgz
|
|
138
|
+
* changed.
|
|
139
|
+
*
|
|
140
|
+
* We walk up the directory tree (monorepo-safe) to find both locations and
|
|
141
|
+
* remove matching entries so the next install re-extracts the fresh tarball.
|
|
142
|
+
*
|
|
143
|
+
* No-op when bun is not the package manager.
|
|
135
144
|
*/
|
|
136
145
|
function clearBunCache(packageName) {
|
|
137
146
|
const pm = detectPackageManager();
|
|
138
147
|
if (!pm.cmd.includes('bun'))
|
|
139
148
|
return;
|
|
140
|
-
// Walk up to find node_modules/.bun — in monorepos it's at the workspace root.
|
|
141
|
-
let bunCacheDir = null;
|
|
142
149
|
let dir = process.cwd();
|
|
143
150
|
while (true) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
const nodeModules = path.join(dir, 'node_modules');
|
|
152
|
+
// (1) Remove .bun/<pkg>@… extraction cache (bun ≥ 1.1)
|
|
153
|
+
const bunCacheDir = path.join(nodeModules, '.bun');
|
|
154
|
+
if (fs.existsSync(bunCacheDir)) {
|
|
155
|
+
const prefix = `${packageName}@`;
|
|
156
|
+
for (const entry of fs.readdirSync(bunCacheDir, { withFileTypes: true })) {
|
|
157
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
158
|
+
fs.rmSync(path.join(bunCacheDir, entry.name), { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// (2) Remove the already-installed node_modules/<pkg> directory.
|
|
163
|
+
// Without this, `bun install` (bare) sees the existing directory and
|
|
164
|
+
// skips re-extraction even though the tarball content has changed.
|
|
165
|
+
const installedDir = path.join(nodeModules, packageName);
|
|
166
|
+
if (fs.existsSync(installedDir)) {
|
|
167
|
+
fs.rmSync(installedDir, { recursive: true, force: true });
|
|
168
|
+
console.log(`Cleared stale node_modules/${packageName} for re-extraction`);
|
|
148
169
|
}
|
|
149
170
|
const parent = path.dirname(dir);
|
|
150
171
|
if (parent === dir)
|
|
151
172
|
break;
|
|
152
173
|
dir = parent;
|
|
153
174
|
}
|
|
154
|
-
if (!bunCacheDir)
|
|
155
|
-
return;
|
|
156
|
-
// Entries look like: <pkg>@<encoded-path>+<hash>
|
|
157
|
-
// The package name may contain dots/scopes — match by startsWith.
|
|
158
|
-
const prefix = `${packageName}@`;
|
|
159
|
-
let removed = 0;
|
|
160
|
-
for (const entry of fs.readdirSync(bunCacheDir, { withFileTypes: true })) {
|
|
161
|
-
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
162
|
-
fs.rmSync(path.join(bunCacheDir, entry.name), { recursive: true, force: true });
|
|
163
|
-
removed++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (removed > 0) {
|
|
167
|
-
console.log(`Cleared ${removed} stale bun cache entr${removed === 1 ? 'y' : 'ies'} for ${packageName}`);
|
|
168
|
-
}
|
|
169
175
|
}
|
|
170
176
|
export async function handleInstallCommand(opts) {
|
|
171
177
|
const { packageToInstall, registry, generationFlags, downloadPackage } = opts;
|
|
@@ -405,7 +405,9 @@ function clearBunCacheForPackage(packageName) {
|
|
|
405
405
|
return;
|
|
406
406
|
let dir = process.cwd();
|
|
407
407
|
while (true) {
|
|
408
|
-
const
|
|
408
|
+
const nm = path.join(dir, 'node_modules');
|
|
409
|
+
// Remove .bun/<pkg>@… extraction cache (bun ≥ 1.1)
|
|
410
|
+
const candidate = path.join(nm, '.bun');
|
|
409
411
|
if (fs.existsSync(candidate)) {
|
|
410
412
|
const prefix = `${packageName}@`;
|
|
411
413
|
for (const entry of fs.readdirSync(candidate, { withFileTypes: true })) {
|
|
@@ -413,7 +415,11 @@ function clearBunCacheForPackage(packageName) {
|
|
|
413
415
|
fs.rmSync(path.join(candidate, entry.name), { recursive: true, force: true });
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
|
-
|
|
418
|
+
}
|
|
419
|
+
// Remove the installed node_modules/<pkg> directory so bun re-extracts
|
|
420
|
+
const installedDir = path.join(nm, packageName);
|
|
421
|
+
if (fs.existsSync(installedDir)) {
|
|
422
|
+
fs.rmSync(installedDir, { recursive: true, force: true });
|
|
417
423
|
}
|
|
418
424
|
const parent = path.dirname(dir);
|
|
419
425
|
if (parent === dir)
|
|
@@ -353,10 +353,11 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
// Conservative mode: if we failed to fetch base metadata (no base fields) skip emitting certain core properties
|
|
356
|
-
// to avoid degrading their types to primitives. Only skip if not profiled and
|
|
356
|
+
// to avoid degrading their types to primitives. Only skip if not profiled, not fixed, and no narrowing binding.
|
|
357
357
|
if (baseFieldByLastSegment.size === 0 && parentInterfaceName === interfaceName && fieldParts.length === 2 && fieldParts[0] === baseResource) {
|
|
358
358
|
const conservativeSkip = new Set(['gender', 'status', 'intent', 'hospitalization', 'payor', 'entry', 'concept', 'identifier', 'use', 'system', 'priority']);
|
|
359
|
-
|
|
359
|
+
const hasNarrowingBinding = !!(field.binding?.codes && field.binding.codes.length > 0 && field.binding.strength === 'required');
|
|
360
|
+
if (!field.isProfiled && field.fixedValue === undefined && !hasNarrowingBinding && conservativeSkip.has(fieldName)) {
|
|
360
361
|
debug('conservative skip (no base metadata)', field.name);
|
|
361
362
|
return;
|
|
362
363
|
}
|
|
@@ -13,10 +13,13 @@ export function handleFlatField(args) {
|
|
|
13
13
|
const conservative = baseFieldByLastSegment.size === 0;
|
|
14
14
|
const isRootInterface = parentInterfaceName === interfaceName;
|
|
15
15
|
const isNested = isRootInterface ? fieldParts.length > 1 : fieldParts.length >= 1;
|
|
16
|
+
// Does this field carry a narrowing required ValueSet binding with resolved codes?
|
|
17
|
+
// If so, it should bypass skip heuristics so we can emit a union literal type.
|
|
18
|
+
const hasNarrowingBinding = !!(field.binding?.codes && field.binding.codes.length > 0 && field.binding.strength === 'required');
|
|
16
19
|
// Skip deeply nested enumerated fields at root level - they'll be handled by nested interfaces
|
|
17
20
|
// (e.g., Patient.telecom.system should be emitted in UKCorePatientTelecom, not UKCorePatient)
|
|
18
21
|
const enumeratedCodeFields = new Set(['system', 'use', 'status', 'gender', 'intent', 'priority', 'class', 'category', 'clinicalStatus', 'verificationStatus']);
|
|
19
|
-
if (fieldParts.length >= 3 && enumeratedCodeFields.has(fieldName) && !field.fixedValue) {
|
|
22
|
+
if (fieldParts.length >= 3 && enumeratedCodeFields.has(fieldName) && !field.fixedValue && !hasNarrowingBinding) {
|
|
20
23
|
debug('skip deeply nested enumerated field at root level', field.name);
|
|
21
24
|
return true;
|
|
22
25
|
}
|
|
@@ -73,7 +76,7 @@ export function handleFlatField(args) {
|
|
|
73
76
|
// Even if cardinality or mustSupport changed, we should NOT emit if it would widen the type
|
|
74
77
|
// (e.g., base has category?: ('food'|'medication'|'environment'|'biologic')[] but we'd emit category: string[])
|
|
75
78
|
// TypeScript doesn't allow assigning string[] to a more specific string literal union array
|
|
76
|
-
if (isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !field.isProfiled && !field.fixedValue) {
|
|
79
|
+
if (isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !field.isProfiled && !field.fixedValue && !hasNarrowingBinding) {
|
|
77
80
|
debug('skip primitive override of base child - would widen type', field.name, 'mappedType=', mapped);
|
|
78
81
|
return true;
|
|
79
82
|
}
|
|
@@ -118,7 +121,7 @@ export function handleFlatField(args) {
|
|
|
118
121
|
const enumeratedSkip = new Set(['status', 'gender', 'class', 'category', 'concept', 'entry', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority']);
|
|
119
122
|
const mustSupportChanged = baseField && (!baseField.mustSupport && field.mustSupport);
|
|
120
123
|
const cardinalityChanged = baseField && (baseField.isOptional !== field.isOptional || baseField.isArray !== field.isArray);
|
|
121
|
-
if (parentInterfaceName === interfaceName && !field.fixedValue && !mustSupportChanged && !cardinalityChanged) {
|
|
124
|
+
if (parentInterfaceName === interfaceName && !field.fixedValue && !hasNarrowingBinding && !mustSupportChanged && !cardinalityChanged) {
|
|
122
125
|
if (enumeratedSkip.has(fieldName))
|
|
123
126
|
return true;
|
|
124
127
|
if (baseResource === 'UsageContext' && fieldName === 'code' && field.type === 'string')
|
|
@@ -160,7 +163,7 @@ export function handleFlatField(args) {
|
|
|
160
163
|
}
|
|
161
164
|
// Special cases: known enumerated code fields (status, gender, intent, class, category) -> avoid overriding unless fixed or mustSupport changed
|
|
162
165
|
const mustSupportChanged = baseField.mustSupport !== field.mustSupport && field.mustSupport === true;
|
|
163
|
-
if (!field.fixedValue && !mustSupportChanged && ['status', 'gender', 'intent', 'class', 'category', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority'].includes(fieldName)) {
|
|
166
|
+
if (!field.fixedValue && !hasNarrowingBinding && !mustSupportChanged && ['status', 'gender', 'intent', 'class', 'category', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'use', 'system', 'priority'].includes(fieldName)) {
|
|
164
167
|
return true;
|
|
165
168
|
}
|
|
166
169
|
// CodeSystem.concept & Bundle.entry etc complex arrays: avoid overriding unless truly adding constraints
|
|
@@ -171,11 +174,12 @@ export function handleFlatField(args) {
|
|
|
171
174
|
if (['concept', 'entry', 'content'].includes(fieldName) && !field.fixedValue)
|
|
172
175
|
return true;
|
|
173
176
|
// Even if mustSupport changes, we should inherit the base enum type, not override with string
|
|
174
|
-
if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'content', 'use', 'system', 'priority'].includes(fieldName) && isPrimitiveType(mapped)) {
|
|
177
|
+
if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'content', 'use', 'system', 'priority'].includes(fieldName) && isPrimitiveType(mapped) && !hasNarrowingBinding) {
|
|
175
178
|
return true;
|
|
176
179
|
}
|
|
177
180
|
// Skip if identical to base (same type, array flag, optionality, AND mustSupport)
|
|
178
|
-
|
|
181
|
+
// Exception: fields with narrowing bindings should still be emitted (they constrain the type)
|
|
182
|
+
if (!field.fixedValue && !hasExtensionSlices && !hasNarrowingBinding) {
|
|
179
183
|
const mappedType = mapTypeToTS(field.type || 'string');
|
|
180
184
|
const mustSupportSame = baseField.mustSupport === field.mustSupport;
|
|
181
185
|
if ((baseField.type === field.type || baseField.type === mappedType) && baseField.isArray === field.isArray && baseField.isOptional === field.isOptional && mustSupportSame) {
|
|
@@ -449,6 +453,25 @@ export function handleFlatField(args) {
|
|
|
449
453
|
}
|
|
450
454
|
// Apply ValueSet binding constraints for CodeableConcept types
|
|
451
455
|
fieldType = applyBindingConstraint(fieldType, field, fields);
|
|
456
|
+
// Apply ValueSet binding constraints for primitive code fields (generates union literal type)
|
|
457
|
+
if (isPrimitiveType(fieldType) && hasNarrowingBinding && field.binding?.codes) {
|
|
458
|
+
const vsUri = field.binding.uri?.split('|')[0];
|
|
459
|
+
const vs = vsUri && valueSets ? (valueSets.get(vsUri) || valueSets.get(field.binding.uri)) : undefined;
|
|
460
|
+
if (vs && vs.isSmall && vs.concepts.length > 0) {
|
|
461
|
+
// Import the ValueSet Code type from the generated valuesets directory
|
|
462
|
+
const sanitizedVSName = sanitizeValueSetName(vs.name);
|
|
463
|
+
const vsTypeName = `${sanitizedVSName}Code`;
|
|
464
|
+
importManager.addPathBasedImport(vsTypeName, './valuesets/ValueSet-' + sanitizedVSName);
|
|
465
|
+
fieldType = vsTypeName;
|
|
466
|
+
debug(`Applied code binding constraint for ${field.name}: ${vsTypeName}`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Fall back to inline union from binding.codes (wrap in parens for array compatibility)
|
|
470
|
+
const inlineUnion = field.binding.codes.map(c => JSON.stringify(c.code)).join(' | ');
|
|
471
|
+
fieldType = field.binding.codes.length > 1 ? `(${inlineUnion})` : inlineUnion;
|
|
472
|
+
debug(`Applied inline code binding constraint for ${field.name}: ${fieldType}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
452
475
|
// Preserve arrayness from base when known; if base metadata is missing and field name is a known repeating element (e.g., 'coverage' on Account),
|
|
453
476
|
// default to array to avoid incorrect narrowing.
|
|
454
477
|
// Skip adding [] if fieldType already contains array syntax (e.g., from identifier/extension union types)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
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",
|