babelfhir-ts 1.5.4 → 1.5.6
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)
|
|
@@ -70,7 +70,7 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
70
70
|
return childId.startsWith(sliceElementId + '.');
|
|
71
71
|
});
|
|
72
72
|
// Build interface body lines from slice children and the slice's own constraints
|
|
73
|
-
const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType);
|
|
73
|
+
const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType, baseFields, parentField.name);
|
|
74
74
|
if (bodyLines.length === 0) {
|
|
75
75
|
// No constrainable properties — skip this slice interface
|
|
76
76
|
log.debug(`No constraints for slice ${sliceName} of ${parentField.name}, skipping`);
|
|
@@ -197,7 +197,7 @@ function gatherChildFields(fields, path) {
|
|
|
197
197
|
/**
|
|
198
198
|
* Build the body lines for a slice-specific interface.
|
|
199
199
|
*/
|
|
200
|
-
function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType) {
|
|
200
|
+
function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType, baseFields, parentPath) {
|
|
201
201
|
const lines = [];
|
|
202
202
|
const referencedFhirTypes = [];
|
|
203
203
|
// Add pattern constraint from the slice field itself (discriminator value)
|
|
@@ -211,16 +211,58 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
//
|
|
214
|
+
// Pre-scan: identify choice-type parent elements (value[x]) that have
|
|
215
|
+
// sub-slices (value[x]:valueQuantity, etc.). These parents carry only slicing
|
|
216
|
+
// metadata and should not be emitted as typed properties.
|
|
217
|
+
// Match only direct choice-type slices like "value[x]:valueString" — NOT deeper
|
|
218
|
+
// elements like "value[x].extension:questionnaireDisplay" where the colon is
|
|
219
|
+
// on a different segment.
|
|
220
|
+
const choiceTypeParents = new Set();
|
|
221
|
+
const choiceTypeSlices = [];
|
|
222
|
+
const choiceSlicePattern = /^([^.]+\[x]):([^.]+)$/; // e.g. "value[x]:valueString"
|
|
215
223
|
for (const child of sliceChildren) {
|
|
216
224
|
const childId = child.elementId || child.name;
|
|
217
225
|
const relativePath = childId.substring(sliceElementId.length + 1);
|
|
218
|
-
|
|
219
|
-
if (
|
|
226
|
+
const match = choiceSlicePattern.exec(relativePath);
|
|
227
|
+
if (match) {
|
|
228
|
+
choiceTypeSlices.push(child);
|
|
229
|
+
choiceTypeParents.add(match[1]); // e.g. "value[x]"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Collect deeply nested fixed values for object literal synthesis.
|
|
233
|
+
// e.g. code.coding.code = "confidence" → code: { coding: [{ code: "confidence" }] }
|
|
234
|
+
const nestedFixedByProp = new Map();
|
|
235
|
+
// Process children of the slice
|
|
236
|
+
for (const child of sliceChildren) {
|
|
237
|
+
const childId = child.elementId || child.name;
|
|
238
|
+
const relativePath = childId.substring(sliceElementId.length + 1);
|
|
239
|
+
// Nested children (dots in relative path) — collect fixed values for synthesis
|
|
240
|
+
if (relativePath.includes('.')) {
|
|
241
|
+
if (child.fixedValue !== undefined) {
|
|
242
|
+
const segments = relativePath.split('.');
|
|
243
|
+
const rawPropName = segments[0];
|
|
244
|
+
const propName = rawPropName.replace(/\[x\]$/, '');
|
|
245
|
+
// Skip nested values under:
|
|
246
|
+
// - choice-type elements ([x]) — handled by direct child or sub-slices
|
|
247
|
+
// - slice notation (part:LineItem.name) — deeper-level constraints
|
|
248
|
+
// - extension/modifierExtension — handled by postProcessExtensions
|
|
249
|
+
if (!rawPropName.includes('[x]') && !rawPropName.includes(':')
|
|
250
|
+
&& propName !== 'extension' && propName !== 'modifierExtension') {
|
|
251
|
+
const nestedPath = segments.slice(1).join('.');
|
|
252
|
+
if (!nestedFixedByProp.has(propName))
|
|
253
|
+
nestedFixedByProp.set(propName, new Map());
|
|
254
|
+
nestedFixedByProp.get(propName).set(nestedPath, String(child.fixedValue));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
220
257
|
continue;
|
|
221
|
-
|
|
258
|
+
}
|
|
259
|
+
// Choice-type sub-slices (value[x]:valueXxx) are processed separately below
|
|
222
260
|
if (relativePath.includes(':'))
|
|
223
261
|
continue;
|
|
262
|
+
// Skip choice-type parents that have sub-slices — they carry only slicing
|
|
263
|
+
// metadata and their inherited type (e.g. Quantity) is misleading
|
|
264
|
+
if (choiceTypeParents.has(relativePath))
|
|
265
|
+
continue;
|
|
224
266
|
const propName = relativePath.replace(/\[x\]$/, '');
|
|
225
267
|
if (child.fixedValue !== undefined) {
|
|
226
268
|
// Fixed value constraint
|
|
@@ -270,6 +312,43 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
270
312
|
}
|
|
271
313
|
}
|
|
272
314
|
}
|
|
315
|
+
// Emit choice-type sub-slice properties (e.g. value[x]:valueQuantity → valueQuantity: Quantity)
|
|
316
|
+
for (const child of choiceTypeSlices) {
|
|
317
|
+
if (!child.sliceName || !child.type)
|
|
318
|
+
continue;
|
|
319
|
+
if (child.type === 'BackboneElement' || child.type === 'Element' || child.type === 'any')
|
|
320
|
+
continue;
|
|
321
|
+
const rawType = getRules().mapTypeToTS(child.type);
|
|
322
|
+
const mappedType = sanitizeIdentifier(rawType);
|
|
323
|
+
if (mappedType !== rawType && child.isProfiled)
|
|
324
|
+
continue;
|
|
325
|
+
const optMark = child.isOptional ? '?' : '';
|
|
326
|
+
lines.push(`${child.sliceName}${optMark}: ${mappedType};`);
|
|
327
|
+
if (getRules().isFhirType(mappedType)) {
|
|
328
|
+
referencedFhirTypes.push(mappedType);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Emit synthesized nested object literals from deeply nested fixed values.
|
|
332
|
+
// Skip properties already emitted by the direct child or choice-type loops
|
|
333
|
+
// to prevent TS2717 (duplicate property) and TS2430 (incompatible extends).
|
|
334
|
+
const emittedProps = new Set(lines.map(l => l.match(/^(\w+)[?]?:/)?.[1]).filter(Boolean));
|
|
335
|
+
for (const [propName, paths] of nestedFixedByProp) {
|
|
336
|
+
if (emittedProps.has(propName))
|
|
337
|
+
continue;
|
|
338
|
+
// Check if this property is an array — look in both slice children and
|
|
339
|
+
// base fields. Synthesized literals for array properties need wrapping
|
|
340
|
+
// in [] to avoid TS2430 (incompatible extends).
|
|
341
|
+
const propFullPath = `${parentPath}.${propName}`;
|
|
342
|
+
const isArrayProp = sliceChildren.some(c => {
|
|
343
|
+
const cId = c.elementId || c.name;
|
|
344
|
+
const rel = cId.substring(sliceElementId.length + 1);
|
|
345
|
+
return rel === propName && c.isArray;
|
|
346
|
+
}) || baseFields.some(f => f.name === propFullPath && f.isArray);
|
|
347
|
+
const literal = synthesizeNestedLiteral(paths);
|
|
348
|
+
if (literal) {
|
|
349
|
+
lines.push(`${propName}: ${isArrayProp ? `[${literal}]` : literal};`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
273
352
|
return { lines, referencedFhirTypes };
|
|
274
353
|
}
|
|
275
354
|
/**
|
|
@@ -294,6 +373,39 @@ function formatCodingLiteral(codings) {
|
|
|
294
373
|
return null;
|
|
295
374
|
return `[${entries.join(', ')}]`;
|
|
296
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Synthesize a nested object literal from deeply nested fixed values.
|
|
378
|
+
*
|
|
379
|
+
* Given a map of nested paths → values (e.g. { "coding.code" → "confidence" }),
|
|
380
|
+
* produces an object literal like `{ coding: [{ code: "confidence" }] }`.
|
|
381
|
+
*
|
|
382
|
+
* Special handling: `coding` segments are wrapped in array brackets because
|
|
383
|
+
* `CodeableConcept.coding` is always an array in FHIR.
|
|
384
|
+
*/
|
|
385
|
+
function synthesizeNestedLiteral(paths) {
|
|
386
|
+
const codingFields = new Map();
|
|
387
|
+
const scalarFields = [];
|
|
388
|
+
for (const [path, value] of paths) {
|
|
389
|
+
if (path.startsWith('coding.')) {
|
|
390
|
+
codingFields.set(path.substring('coding.'.length), value);
|
|
391
|
+
}
|
|
392
|
+
else if (!path.includes('.')) {
|
|
393
|
+
scalarFields.push([path, value]);
|
|
394
|
+
}
|
|
395
|
+
// Deeper nesting (3+ levels) is uncommon; skip gracefully
|
|
396
|
+
}
|
|
397
|
+
const parts = [];
|
|
398
|
+
if (codingFields.size > 0) {
|
|
399
|
+
const codingParts = [...codingFields.entries()].map(([k, v]) => `${k}: "${v}"`);
|
|
400
|
+
parts.push(`coding: [{ ${codingParts.join('; ')} }]`);
|
|
401
|
+
}
|
|
402
|
+
for (const [field, value] of scalarFields) {
|
|
403
|
+
parts.push(`${field}: "${value}"`);
|
|
404
|
+
}
|
|
405
|
+
if (parts.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
return `{ ${parts.join('; ')} }`;
|
|
408
|
+
}
|
|
297
409
|
/**
|
|
298
410
|
* Update the root interface to use the union type for a sliced backbone field.
|
|
299
411
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
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",
|