babelfhir-ts 1.6.8 → 1.6.10
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 +15 -5
- package/out/src/generator/core/utils.js +26 -0
- package/out/src/generator/emitters/prefab/prefabDepProvider.js +104 -0
- package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -2
- package/out/src/generator/emitters/prefab/prefabRenderer.js +23 -7
- package/out/src/generator/emitters/prefab/prefabTypeMapper.js +28 -3
- package/out/src/generator/emitters/requiredBindings.js +22 -8
- package/out/src/generator/emitters/zod/zodStubInstaller.js +1 -14
- package/out/src/generator/index.js +19 -2
- package/out/src/generator/sdProcessor.js +1 -1
- package/package.json +2 -2
|
@@ -112,14 +112,24 @@ 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.12 is the floor, for what the emitted form puts on a coded field.
|
|
123
|
+
* `AutoFormField` gained `options` in 0.3.10 — below that the property is not
|
|
124
|
+
* in the type, `autoForm` ignores it, and a bound field renders as free text.
|
|
125
|
+
* 0.3.12 then moved `required` onto every stateful control and added
|
|
126
|
+
* `Select({ multiple })`: before it, `autoForm` dropped `required` on the
|
|
127
|
+
* Select path because the component could not carry it, and a repeating coded
|
|
128
|
+
* element had no multi-select to render as. On 0.3.10 or 0.3.11 the form still
|
|
129
|
+
* builds and still offers the right choices, but a required element is not
|
|
130
|
+
* marked required and a repeating one takes a single value.
|
|
121
131
|
*/
|
|
122
|
-
prefab: '^0.3.
|
|
132
|
+
prefab: '^0.3.12',
|
|
123
133
|
/**
|
|
124
134
|
* Generated FHIR client base package (`@babelfhir-ts/client-<slug>`).
|
|
125
135
|
*
|
|
@@ -341,6 +341,32 @@ export function mergeFields(newFields, oldFields) {
|
|
|
341
341
|
// Return the merged fields as an array
|
|
342
342
|
return Array.from(fieldMap.values());
|
|
343
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Copies a directory tree, optionally keeping only the files a predicate accepts.
|
|
346
|
+
*
|
|
347
|
+
* Shared by the compile-time dependency providers, which each need part of an
|
|
348
|
+
* installed package inside the output's `node_modules` — the zod runtime whole,
|
|
349
|
+
* prefab's declarations only.
|
|
350
|
+
*
|
|
351
|
+
* @param filter - Called per file with its absolute source path. Omit to copy everything.
|
|
352
|
+
* A directory left empty by the filter is not created.
|
|
353
|
+
*/
|
|
354
|
+
export function copyDirRecursive(src, dest, filter) {
|
|
355
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
356
|
+
const srcPath = path.join(src, entry.name);
|
|
357
|
+
const destPath = path.join(dest, entry.name);
|
|
358
|
+
if (entry.isDirectory()) {
|
|
359
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
360
|
+
copyDirRecursive(srcPath, destPath, filter);
|
|
361
|
+
if (fs.readdirSync(destPath).length === 0)
|
|
362
|
+
fs.rmdirSync(destPath);
|
|
363
|
+
}
|
|
364
|
+
else if (!filter || filter(srcPath)) {
|
|
365
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
366
|
+
fs.copyFileSync(srcPath, destPath);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
344
370
|
/**
|
|
345
371
|
* Ensures the given directory exists, creating it if necessary.
|
|
346
372
|
*/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `@maxhealth.tech/prefab` resolvable while the generated output compiles.
|
|
3
|
+
*
|
|
4
|
+
* `--prefab` emits renderers that import the real prefab API, and the emitted
|
|
5
|
+
* package declares prefab as a dependency — but nothing puts it where the
|
|
6
|
+
* generator's own `tsc` pass can see it. Every emitted renderer then failed
|
|
7
|
+
* TS2307 and the whole package failed to compile, so a `--prefab` run produced
|
|
8
|
+
* no `prefab/` in the shipped package at all. It only ever passed when the
|
|
9
|
+
* output happened to sit inside a tree that already had prefab installed, which
|
|
10
|
+
* is why it looks fine from this repo's own `output/` and fails in CI.
|
|
11
|
+
*
|
|
12
|
+
* The real declarations are used rather than a stub. The emitted renderers are
|
|
13
|
+
* checked against prefab's actual component signatures, and their `.d.ts` quotes
|
|
14
|
+
* prefab's types back to the consumer — a hand-written stub would both drift
|
|
15
|
+
* from the library and erase those types from the emitted package.
|
|
16
|
+
*
|
|
17
|
+
* Two sources, in order: a copy already installed alongside the generator (this
|
|
18
|
+
* repo, where prefab is a devDependency), else a one-package install from the
|
|
19
|
+
* registry (an `npx babelfhir-ts` run, which ships no devDependencies). Only
|
|
20
|
+
* the declarations are copied — `tsc` reads no JavaScript — and whatever this
|
|
21
|
+
* added is removed after the compile, because the consumer installs the real
|
|
22
|
+
* dependency themselves.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { spawnSync } from 'child_process';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
import { copyDirRecursive } from '../../core/utils.js';
|
|
29
|
+
import { EMITTED_RANGES } from '../../core/generatedPackageDeps.js';
|
|
30
|
+
import { logger } from '../../../logger.js';
|
|
31
|
+
const log = logger.withTag('prefab');
|
|
32
|
+
const PACKAGE_NAME = '@maxhealth.tech/prefab';
|
|
33
|
+
const PACKAGE_SEGMENTS = PACKAGE_NAME.split('/');
|
|
34
|
+
/** How far up a tree to look for a `node_modules` holding the package. */
|
|
35
|
+
const MAX_LOOKUP_DEPTH = 8;
|
|
36
|
+
/** The package directory under a given tree root, whether or not it exists. */
|
|
37
|
+
function packageDirIn(root) {
|
|
38
|
+
return path.join(root, 'node_modules', ...PACKAGE_SEGMENTS);
|
|
39
|
+
}
|
|
40
|
+
/** Walk up from `start`, returning the first installed copy found. */
|
|
41
|
+
function findInstalledCopy(start) {
|
|
42
|
+
let dir = start;
|
|
43
|
+
for (let i = 0; i < MAX_LOOKUP_DEPTH; i++) {
|
|
44
|
+
const candidate = packageDirIn(dir);
|
|
45
|
+
if (fs.existsSync(path.join(candidate, 'package.json')))
|
|
46
|
+
return candidate;
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir)
|
|
49
|
+
break;
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Install the package into the output dir, declarations included. */
|
|
55
|
+
function installFromRegistry(outputDir) {
|
|
56
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
57
|
+
const spec = `${PACKAGE_NAME}@${EMITTED_RANGES.prefab}`;
|
|
58
|
+
log.debug(`Installing ${spec} for the output typecheck`);
|
|
59
|
+
const result = spawnSync(npm, ['install', '--no-save', '--no-package-lock', '--no-audit', '--no-fund',
|
|
60
|
+
'--prefix', outputDir, spec], { stdio: 'pipe', encoding: 'utf8' });
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
log.warn(`Could not install ${spec}: ${result.stderr?.trim() || `exit ${result.status}`}`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return fs.existsSync(path.join(packageDirIn(outputDir), 'package.json'));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ensure the emitted `prefab/` files can be typechecked from `outputDir`.
|
|
69
|
+
*
|
|
70
|
+
* @returns a cleanup that removes what this added, and does nothing when the
|
|
71
|
+
* package was already resolvable.
|
|
72
|
+
*/
|
|
73
|
+
export function providePrefabTypes(outputDir) {
|
|
74
|
+
const alreadyResolvable = findInstalledCopy(outputDir);
|
|
75
|
+
if (alreadyResolvable) {
|
|
76
|
+
log.debug(`Typechecking prefab output against ${alreadyResolvable}`);
|
|
77
|
+
return () => { };
|
|
78
|
+
}
|
|
79
|
+
const target = packageDirIn(outputDir);
|
|
80
|
+
const generatorDir = path.dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
const alongsideGenerator = findInstalledCopy(generatorDir);
|
|
82
|
+
if (alongsideGenerator) {
|
|
83
|
+
fs.mkdirSync(target, { recursive: true });
|
|
84
|
+
// Declarations only: `tsc` never reads the JavaScript, and prefab ships
|
|
85
|
+
// browser bundles and source maps that would triple the copy for nothing.
|
|
86
|
+
copyDirRecursive(alongsideGenerator, target, src => src.endsWith('.d.ts') || path.basename(src) === 'package.json');
|
|
87
|
+
}
|
|
88
|
+
else if (!installFromRegistry(outputDir)) {
|
|
89
|
+
throw new Error(`--prefab needs ${PACKAGE_NAME} to typecheck the emitted renderers, and it could not be ` +
|
|
90
|
+
`resolved next to the generator or installed from the registry. Install ` +
|
|
91
|
+
`${PACKAGE_NAME}@${EMITTED_RANGES.prefab} where generation runs, or drop --prefab.`);
|
|
92
|
+
}
|
|
93
|
+
return () => {
|
|
94
|
+
try {
|
|
95
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
catch { /* ignore */ }
|
|
98
|
+
// Leave `@maxhealth.tech/` behind only while it still holds something else.
|
|
99
|
+
try {
|
|
100
|
+
fs.rmdirSync(path.dirname(target));
|
|
101
|
+
}
|
|
102
|
+
catch { /* not empty — fine */ }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -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 = [];
|
|
@@ -78,8 +89,12 @@ function emitFormFields(descriptors) {
|
|
|
78
89
|
// Forms can't capture complex nested structures cleanly; skip them.
|
|
79
90
|
if (d.kind === 'nested' || d.kind === 'json' || d.kind === 'image' || d.kind === 'dicom')
|
|
80
91
|
continue;
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const options = d.enumOptions?.length ? d.enumOptions : undefined;
|
|
93
|
+
// A repeating element is only expressible as a form field when the choices
|
|
94
|
+
// are enumerated: prefab's multi-select submits a list under the name, which
|
|
95
|
+
// is the element's own shape. Any other array still has no control.
|
|
96
|
+
if (d.isArray && !options)
|
|
97
|
+
continue;
|
|
83
98
|
let type = 'text';
|
|
84
99
|
if (d.kind === 'number')
|
|
85
100
|
type = 'number';
|
|
@@ -87,14 +102,15 @@ function emitFormFields(descriptors) {
|
|
|
87
102
|
type = 'switch';
|
|
88
103
|
else if (d.kind === 'date')
|
|
89
104
|
type = 'date';
|
|
90
|
-
else if (
|
|
105
|
+
else if (options)
|
|
91
106
|
type = 'select';
|
|
92
107
|
const extra = [`name: '${d.name}'`, `label: ${JSON.stringify(d.label)}`, `type: '${type}'`];
|
|
93
108
|
if (d.required)
|
|
94
109
|
extra.push('required: true');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
if (options)
|
|
111
|
+
extra.push(`options: ${emitOptionsLiteral(options)}`);
|
|
112
|
+
if (d.isArray)
|
|
113
|
+
extra.push('multiple: true');
|
|
98
114
|
lines.push(` { ${extra.join(', ')} },`);
|
|
99
115
|
seen.add(d.name);
|
|
100
116
|
}
|
|
@@ -206,7 +222,7 @@ function emitBrowserTitleExpr(titleDescriptor, interfaceName) {
|
|
|
206
222
|
: `selected.dot('${titleDescriptor.name}')`;
|
|
207
223
|
}
|
|
208
224
|
export function generatePrefabRenderer(interfaceName, fields, options) {
|
|
209
|
-
const rawDescriptors = fields.flatMap(describeField);
|
|
225
|
+
const rawDescriptors = fields.flatMap(f => describeField(f, options?.valueSets));
|
|
210
226
|
const descriptors = dedupeDescriptors(rawDescriptors);
|
|
211
227
|
const resourceType = options?.resourceType;
|
|
212
228
|
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) {
|
|
@@ -12,6 +12,7 @@ import path from 'path';
|
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { versionSlug } from '../../fhir/versionContext.js';
|
|
14
14
|
import { getZodPackageTypes } from './zodTypes.js';
|
|
15
|
+
import { copyDirRecursive } from '../../core/utils.js';
|
|
15
16
|
/** Resolve the repo root from this file's location (src/generator/emitters/zod/) */
|
|
16
17
|
function findRepoRoot() {
|
|
17
18
|
const selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -26,20 +27,6 @@ function findRepoRoot() {
|
|
|
26
27
|
}
|
|
27
28
|
return path.resolve(selfDir, '..', '..', '..', '..');
|
|
28
29
|
}
|
|
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
|
-
}
|
|
43
30
|
export function installBaseZodTypes(outputDir) {
|
|
44
31
|
const slug = versionSlug();
|
|
45
32
|
const pkgDir = path.join(outputDir, 'node_modules', '@babelfhir-ts', 'zod');
|
|
@@ -619,10 +619,19 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
619
619
|
// Install fhirpath type stub so tsc can resolve validator imports
|
|
620
620
|
const { installFhirpathStub, removeFhirpathStub } = await import('./emitters/validator/fhirpathStubInstaller.js');
|
|
621
621
|
installFhirpathStub(outputDir);
|
|
622
|
+
// Emitted renderers import the real prefab API, which nothing has installed here yet
|
|
623
|
+
const releasePrefabTypes = flags?.prefab
|
|
624
|
+
? (await import('./emitters/prefab/prefabDepProvider.js')).providePrefabTypes(outputDir)
|
|
625
|
+
: undefined;
|
|
622
626
|
// Compile TypeScript to JavaScript
|
|
623
627
|
endPhase = startPhase('compile');
|
|
624
628
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
625
|
-
|
|
629
|
+
try {
|
|
630
|
+
await compileTypeScriptToJS(outputDir);
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
releasePrefabTypes?.();
|
|
634
|
+
}
|
|
626
635
|
endPhase(); // compile
|
|
627
636
|
// Remove fhirpath stub — the real package is a peer dependency
|
|
628
637
|
removeFhirpathStub(outputDir);
|
|
@@ -841,9 +850,17 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
841
850
|
}
|
|
842
851
|
const { installFhirpathStub, removeFhirpathStub } = await import('./emitters/validator/fhirpathStubInstaller.js');
|
|
843
852
|
installFhirpathStub(outputDir);
|
|
853
|
+
const releasePrefabTypes = flags?.prefab
|
|
854
|
+
? (await import('./emitters/prefab/prefabDepProvider.js')).providePrefabTypes(outputDir)
|
|
855
|
+
: undefined;
|
|
844
856
|
endPhase = startPhase('compile');
|
|
845
857
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
846
|
-
|
|
858
|
+
try {
|
|
859
|
+
await compileTypeScriptToJS(outputDir);
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
releasePrefabTypes?.();
|
|
863
|
+
}
|
|
847
864
|
endPhase(); // compile
|
|
848
865
|
removeFhirpathStub(outputDir);
|
|
849
866
|
const babelfhirDir = path.join(outputDir, 'node_modules', '@babelfhir-ts');
|
|
@@ -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.10",
|
|
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.12",
|
|
91
91
|
"@types/node": "^25.9.5",
|
|
92
92
|
"@types/semver": "^7.8.0",
|
|
93
93
|
"@types/unzipper": "^0.10.11",
|