babelfhir-ts 1.6.8 → 1.6.9
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/out/src/generator/core/generatedPackageDeps.js +11 -5
- package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -2
- package/out/src/generator/emitters/prefab/prefabRenderer.js +16 -5
- package/out/src/generator/emitters/prefab/prefabTypeMapper.js +28 -3
- package/out/src/generator/emitters/requiredBindings.js +22 -8
- package/out/src/generator/sdProcessor.js +1 -1
- package/package.json +2 -2
|
@@ -112,14 +112,20 @@ export const EMITTED_RANGES = {
|
|
|
112
112
|
/**
|
|
113
113
|
* Prefab UI runtime for `--prefab` output.
|
|
114
114
|
*
|
|
115
|
-
* 0.3.0
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
115
|
+
* 0.3.0 introduced wire protocol 0.3, where the theme is compiled into the
|
|
116
|
+
* top-level `css` array, `stylesheets` carries external URLs, and `mode`
|
|
117
|
+
* forces a colour scheme. Emitted renderers pass `theme` and spread
|
|
118
|
+
* `PrefabAppOptions` into `PrefabApp`, so they rely on a >= 0.3
|
|
119
119
|
* serializer/renderer pair to apply themes. A 0.2.x renderer would silently
|
|
120
120
|
* drop them.
|
|
121
|
+
*
|
|
122
|
+
* 0.3.10 is the floor because `AutoFormField` gained `options` there, and the
|
|
123
|
+
* emitted form now carries the bound ValueSet's concepts on every coded
|
|
124
|
+
* field. Below it the property is not in the type and is ignored by
|
|
125
|
+
* `autoForm`, so a required-binding field renders as free text: the form
|
|
126
|
+
* still builds, and every constraint the binding states is gone from it.
|
|
121
127
|
*/
|
|
122
|
-
prefab: '^0.3.
|
|
128
|
+
prefab: '^0.3.10',
|
|
123
129
|
/**
|
|
124
130
|
* Generated FHIR client base package (`@babelfhir-ts/client-<slug>`).
|
|
125
131
|
*
|
|
@@ -28,9 +28,9 @@ function ensurePrefabDir(outputDir) {
|
|
|
28
28
|
* Emit `prefab/<InterfaceName>Prefab.ts` for a single profile.
|
|
29
29
|
* Registers the profile so {@link finalizePrefabOutput} can build the barrel.
|
|
30
30
|
*/
|
|
31
|
-
export function writePrefabProfile(outputDir, interfaceName, fields, resourceType) {
|
|
31
|
+
export function writePrefabProfile(outputDir, interfaceName, fields, resourceType, valueSets) {
|
|
32
32
|
const dir = ensurePrefabDir(outputDir);
|
|
33
|
-
const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields, { resourceType });
|
|
33
|
+
const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields, { resourceType, valueSets });
|
|
34
34
|
const filePath = path.join(dir, `${interfaceName}Prefab.ts`);
|
|
35
35
|
fs.writeFileSync(filePath, code);
|
|
36
36
|
let map = emittedProfiles.get(outputDir);
|
|
@@ -68,6 +68,17 @@ function emitToRowBody(descriptors) {
|
|
|
68
68
|
const lines = descriptors.map(d => ` ${d.name}: ${rowAccessor(d)},`);
|
|
69
69
|
return lines.join('\n');
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Emit an `AutoFormOption[]` literal for a bound field.
|
|
73
|
+
*
|
|
74
|
+
* The label is omitted when the concept carried no display, because prefab
|
|
75
|
+
* already falls back to the value and restating it would only make the
|
|
76
|
+
* generated file longer.
|
|
77
|
+
*/
|
|
78
|
+
function emitOptionsLiteral(options) {
|
|
79
|
+
const entries = options.map(o => o.label ? `{ value: ${JSON.stringify(o.value)}, label: ${JSON.stringify(o.label)} }` : `{ value: ${JSON.stringify(o.value)} }`);
|
|
80
|
+
return `[${entries.join(', ')}]`;
|
|
81
|
+
}
|
|
71
82
|
/** Emit `autoForm` field descriptors. */
|
|
72
83
|
function emitFormFields(descriptors) {
|
|
73
84
|
const lines = [];
|
|
@@ -80,6 +91,7 @@ function emitFormFields(descriptors) {
|
|
|
80
91
|
continue;
|
|
81
92
|
if (d.isArray)
|
|
82
93
|
continue; // v1: scalars only
|
|
94
|
+
const options = d.enumOptions?.length ? d.enumOptions : undefined;
|
|
83
95
|
let type = 'text';
|
|
84
96
|
if (d.kind === 'number')
|
|
85
97
|
type = 'number';
|
|
@@ -87,14 +99,13 @@ function emitFormFields(descriptors) {
|
|
|
87
99
|
type = 'switch';
|
|
88
100
|
else if (d.kind === 'date')
|
|
89
101
|
type = 'date';
|
|
90
|
-
else if (
|
|
102
|
+
else if (options)
|
|
91
103
|
type = 'select';
|
|
92
104
|
const extra = [`name: '${d.name}'`, `label: ${JSON.stringify(d.label)}`, `type: '${type}'`];
|
|
93
105
|
if (d.required)
|
|
94
106
|
extra.push('required: true');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// type is widened. See prefab-ui#TODO.
|
|
107
|
+
if (options)
|
|
108
|
+
extra.push(`options: ${emitOptionsLiteral(options)}`);
|
|
98
109
|
lines.push(` { ${extra.join(', ')} },`);
|
|
99
110
|
seen.add(d.name);
|
|
100
111
|
}
|
|
@@ -206,7 +217,7 @@ function emitBrowserTitleExpr(titleDescriptor, interfaceName) {
|
|
|
206
217
|
: `selected.dot('${titleDescriptor.name}')`;
|
|
207
218
|
}
|
|
208
219
|
export function generatePrefabRenderer(interfaceName, fields, options) {
|
|
209
|
-
const rawDescriptors = fields.flatMap(describeField);
|
|
220
|
+
const rawDescriptors = fields.flatMap(f => describeField(f, options?.valueSets));
|
|
210
221
|
const descriptors = dedupeDescriptors(rawDescriptors);
|
|
211
222
|
const resourceType = options?.resourceType;
|
|
212
223
|
const tableDescriptors = pickTableDescriptors(descriptors);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* This module is deliberately small and data-driven so it can later be
|
|
9
9
|
* extracted into a shared codegen-spec package (see design notes).
|
|
10
10
|
*/
|
|
11
|
+
import { lookupBoundValueSet } from '../requiredBindings.js';
|
|
12
|
+
import { ctx } from '../../fhir/versionContext.js';
|
|
11
13
|
/** Lower-cased FHIR primitive type codes. */
|
|
12
14
|
const PRIMITIVE_TEXT = new Set([
|
|
13
15
|
'string', 'code', 'uri', 'url', 'canonical', 'id', 'oid', 'uuid', 'markdown', 'xhtml',
|
|
@@ -23,6 +25,29 @@ function labelFor(name) {
|
|
|
23
25
|
const spaced = name.replace(/([A-Z])/g, ' $1').replace(/[._-]/g, ' ').trim();
|
|
24
26
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the choice list for a bound field, or undefined when there is none to
|
|
30
|
+
* offer.
|
|
31
|
+
*
|
|
32
|
+
* The parsed ValueSet is preferred over the codes the SD parser sampled onto
|
|
33
|
+
* the binding: it carries the concept displays, and it is the whole set rather
|
|
34
|
+
* than a subset. Both paths are capped at the same `STORE_CODES` threshold the
|
|
35
|
+
* parser and the interface emitter already use to decide a ValueSet is small
|
|
36
|
+
* enough to enumerate — past it a Select stops being a usable control and the
|
|
37
|
+
* field stays a free-text input.
|
|
38
|
+
*/
|
|
39
|
+
function selectOptionsFor(field, valueSets) {
|
|
40
|
+
const limit = ctx().valueSetThresholds.STORE_CODES;
|
|
41
|
+
const resolved = lookupBoundValueSet(field.binding?.uri, valueSets);
|
|
42
|
+
const concepts = resolved?.valueSet.concepts;
|
|
43
|
+
if (concepts?.length && concepts.length <= limit) {
|
|
44
|
+
return concepts.map(c => ({ value: c.code, ...(c.display ? { label: c.display } : {}) }));
|
|
45
|
+
}
|
|
46
|
+
const sampled = field.binding?.codes?.filter(c => c.code).map(c => ({ value: c.code }));
|
|
47
|
+
if (sampled?.length && sampled.length <= limit)
|
|
48
|
+
return sampled;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
26
51
|
function kindForType(fhirType) {
|
|
27
52
|
if (PRIMITIVE_TEXT.has(fhirType))
|
|
28
53
|
return 'text';
|
|
@@ -86,7 +111,7 @@ function kindForType(fhirType) {
|
|
|
86
111
|
* on the typed interface. Detail views for nested backbone elements are a
|
|
87
112
|
* follow-up once slicing-aware tree rendering lands.
|
|
88
113
|
*/
|
|
89
|
-
export function describeField(field) {
|
|
114
|
+
export function describeField(field, valueSets) {
|
|
90
115
|
if (field.isForbidden)
|
|
91
116
|
return [];
|
|
92
117
|
if (!field.type)
|
|
@@ -132,7 +157,7 @@ export function describeField(field) {
|
|
|
132
157
|
label: labelFor(variantPropName),
|
|
133
158
|
valueSetUri: field.binding?.uri,
|
|
134
159
|
bindingStrength: field.binding?.strength,
|
|
135
|
-
|
|
160
|
+
enumOptions: selectOptionsFor(field, valueSets),
|
|
136
161
|
});
|
|
137
162
|
}
|
|
138
163
|
return out;
|
|
@@ -158,7 +183,7 @@ export function describeField(field) {
|
|
|
158
183
|
label: labelFor(bareName),
|
|
159
184
|
valueSetUri: field.binding?.uri,
|
|
160
185
|
bindingStrength: field.binding?.strength,
|
|
161
|
-
|
|
186
|
+
enumOptions: selectOptionsFor(field, valueSets),
|
|
162
187
|
}];
|
|
163
188
|
}
|
|
164
189
|
/**
|
|
@@ -21,17 +21,31 @@ import { stripVersionFromCanonicalUrl } from '../core/utils.js';
|
|
|
21
21
|
import { sanitizeValueSetName } from './valueset/valueSetGenerator.js';
|
|
22
22
|
/** Types FHIR permits a binding on. */
|
|
23
23
|
const BINDABLE_TYPES = ['code', 'Coding', 'CodeableConcept', 'Quantity', 'string', 'uri'];
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a binding URI to its parsed ValueSet, versioned key first.
|
|
26
|
+
*
|
|
27
|
+
* A binding may cite `…/ValueSet/foo|1.0.0` while the package indexes the
|
|
28
|
+
* ValueSet under its unversioned canonical, so both keys have to be tried.
|
|
29
|
+
* Every emitter that reads concepts off a binding needs the same two lookups,
|
|
30
|
+
* which is why it lives here rather than in each of them.
|
|
31
|
+
*/
|
|
32
|
+
export function lookupBoundValueSet(bindingUri, valueSets) {
|
|
25
33
|
if (!bindingUri || !valueSets)
|
|
26
34
|
return undefined;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const direct = valueSets.get(bindingUri);
|
|
36
|
+
if (direct)
|
|
37
|
+
return { valueSet: direct, resolvedUri: bindingUri };
|
|
38
|
+
if (!bindingUri.includes('|'))
|
|
39
|
+
return undefined;
|
|
40
|
+
const strippedUri = stripVersionFromCanonicalUrl(bindingUri);
|
|
41
|
+
const stripped = valueSets.get(strippedUri);
|
|
42
|
+
return stripped ? { valueSet: stripped, resolvedUri: strippedUri } : undefined;
|
|
43
|
+
}
|
|
44
|
+
export function resolveRequiredBindingValueSet(bindingUri, valueSets) {
|
|
45
|
+
const resolved = lookupBoundValueSet(bindingUri, valueSets);
|
|
46
|
+
if (!resolved || !bindingUri)
|
|
34
47
|
return undefined;
|
|
48
|
+
const { valueSet: vs, resolvedUri } = resolved;
|
|
35
49
|
const displayName = resolvedUri.split('/').pop() || vs.name;
|
|
36
50
|
if (vs.concepts.length === 0) {
|
|
37
51
|
if (vs.systems && vs.systems.length > 0) {
|
|
@@ -750,7 +750,7 @@ export async function processStructureDefinition(sd, ctx) {
|
|
|
750
750
|
}
|
|
751
751
|
if (flags?.prefab) {
|
|
752
752
|
const { writePrefabProfile } = await import('./emitters/prefab/prefabEmitter.js');
|
|
753
|
-
writePrefabProfile(outputDir, interfaceName, aligned.alignedNewFields, sd.type);
|
|
753
|
+
writePrefabProfile(outputDir, interfaceName, aligned.alignedNewFields, sd.type, valueSets);
|
|
754
754
|
}
|
|
755
755
|
logger.log(`Generated artifacts for ${interfaceName}`);
|
|
756
756
|
return interfaceName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.9",
|
|
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@eslint/js": "^10.0.1",
|
|
89
89
|
"@max-health-inc/config": "^3.1.0",
|
|
90
|
-
"@maxhealth.tech/prefab": "^0.3.
|
|
90
|
+
"@maxhealth.tech/prefab": "^0.3.11",
|
|
91
91
|
"@types/node": "^25.9.5",
|
|
92
92
|
"@types/semver": "^7.8.0",
|
|
93
93
|
"@types/unzipper": "^0.10.11",
|