babelfhir-ts 1.6.9 → 1.6.11
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/canonicalUrl.js +24 -0
- package/out/src/generator/core/constants.js +1 -1
- package/out/src/generator/core/generatedPackageDeps.js +10 -6
- package/out/src/generator/core/utils.js +31 -12
- package/out/src/generator/emitters/prefab/prefabDepProvider.js +104 -0
- package/out/src/generator/emitters/prefab/prefabRenderer.js +7 -2
- package/out/src/generator/emitters/prefab/prefabRuntimeAssets.js +57 -16
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +11 -6
- package/out/src/generator/emitters/zod/zodStubInstaller.js +1 -14
- package/out/src/generator/fhir/createRules.js +0 -3
- package/out/src/generator/fhir/createRules.ts +0 -4
- package/out/src/generator/index.js +19 -2
- package/package.json +14 -14
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical-URL string handling, with no dependencies of its own.
|
|
3
|
+
*
|
|
4
|
+
* A leaf on purpose. This lived in `utils.ts`, which imports the FHIR version
|
|
5
|
+
* context, so `constants.ts` calling it pulled the whole generator graph in
|
|
6
|
+
* behind a config module: constants → utils → versionContext → types →
|
|
7
|
+
* createRules / fetchUtils → constants. Node evaluates that cycle in an order
|
|
8
|
+
* that leaves `TS` and `USER_AGENT` uninitialized whenever `constants.ts` is the
|
|
9
|
+
* entry point, which is exactly what a script importing it directly does — so
|
|
10
|
+
* `scripts/check-ig-versions.ts` failed on import with `Cannot access 'TS'
|
|
11
|
+
* before initialization` while the CLI, entered through `main.ts`, was fine.
|
|
12
|
+
*
|
|
13
|
+
* Keep this module free of imports so both layers can depend on it.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A canonical URL with any version suffix removed:
|
|
17
|
+
* `http://…/ValueSet/foo|4.0.1` -> `http://…/ValueSet/foo`.
|
|
18
|
+
*/
|
|
19
|
+
export function stripVersionFromCanonicalUrl(url) {
|
|
20
|
+
if (!url)
|
|
21
|
+
return url;
|
|
22
|
+
const pipeIndex = url.indexOf('|');
|
|
23
|
+
return pipeIndex >= 0 ? url.substring(0, pipeIndex) : url;
|
|
24
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* maintenance trivial.
|
|
7
7
|
*/
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
|
-
import { stripVersionFromCanonicalUrl } from './
|
|
9
|
+
import { stripVersionFromCanonicalUrl } from './canonicalUrl.js';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
@@ -119,13 +119,17 @@ export const EMITTED_RANGES = {
|
|
|
119
119
|
* serializer/renderer pair to apply themes. A 0.2.x renderer would silently
|
|
120
120
|
* drop them.
|
|
121
121
|
*
|
|
122
|
-
* 0.3.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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.
|
|
127
131
|
*/
|
|
128
|
-
prefab: '^0.3.
|
|
132
|
+
prefab: '^0.3.12',
|
|
129
133
|
/**
|
|
130
134
|
* Generated FHIR client base package (`@babelfhir-ts/client-<slug>`).
|
|
131
135
|
*
|
|
@@ -2,18 +2,11 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { logger } from "../../logger.js";
|
|
4
4
|
import { ctx } from '../fhir/versionContext.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export function stripVersionFromCanonicalUrl(url) {
|
|
12
|
-
if (!url)
|
|
13
|
-
return url;
|
|
14
|
-
const pipeIndex = url.indexOf('|');
|
|
15
|
-
return pipeIndex >= 0 ? url.substring(0, pipeIndex) : url;
|
|
16
|
-
}
|
|
5
|
+
// Defined in a leaf module so `constants.ts` can call it without importing this
|
|
6
|
+
// one — see canonicalUrl.ts. Re-exported here because 20+ call sites import it
|
|
7
|
+
// from utils, and used below in this file too.
|
|
8
|
+
import { stripVersionFromCanonicalUrl } from "./canonicalUrl.js";
|
|
9
|
+
export { stripVersionFromCanonicalUrl };
|
|
17
10
|
/**
|
|
18
11
|
* An element path with any slice suffix removed: `identifier:NPI` -> `identifier`.
|
|
19
12
|
*
|
|
@@ -341,6 +334,32 @@ export function mergeFields(newFields, oldFields) {
|
|
|
341
334
|
// Return the merged fields as an array
|
|
342
335
|
return Array.from(fieldMap.values());
|
|
343
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Copies a directory tree, optionally keeping only the files a predicate accepts.
|
|
339
|
+
*
|
|
340
|
+
* Shared by the compile-time dependency providers, which each need part of an
|
|
341
|
+
* installed package inside the output's `node_modules` — the zod runtime whole,
|
|
342
|
+
* prefab's declarations only.
|
|
343
|
+
*
|
|
344
|
+
* @param filter - Called per file with its absolute source path. Omit to copy everything.
|
|
345
|
+
* A directory left empty by the filter is not created.
|
|
346
|
+
*/
|
|
347
|
+
export function copyDirRecursive(src, dest, filter) {
|
|
348
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
349
|
+
const srcPath = path.join(src, entry.name);
|
|
350
|
+
const destPath = path.join(dest, entry.name);
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
353
|
+
copyDirRecursive(srcPath, destPath, filter);
|
|
354
|
+
if (fs.readdirSync(destPath).length === 0)
|
|
355
|
+
fs.rmdirSync(destPath);
|
|
356
|
+
}
|
|
357
|
+
else if (!filter || filter(srcPath)) {
|
|
358
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
359
|
+
fs.copyFileSync(srcPath, destPath);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
344
363
|
/**
|
|
345
364
|
* Ensures the given directory exists, creating it if necessary.
|
|
346
365
|
*/
|
|
@@ -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
|
+
}
|
|
@@ -89,9 +89,12 @@ function emitFormFields(descriptors) {
|
|
|
89
89
|
// Forms can't capture complex nested structures cleanly; skip them.
|
|
90
90
|
if (d.kind === 'nested' || d.kind === 'json' || d.kind === 'image' || d.kind === 'dicom')
|
|
91
91
|
continue;
|
|
92
|
-
if (d.isArray)
|
|
93
|
-
continue; // v1: scalars only
|
|
94
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;
|
|
95
98
|
let type = 'text';
|
|
96
99
|
if (d.kind === 'number')
|
|
97
100
|
type = 'number';
|
|
@@ -106,6 +109,8 @@ function emitFormFields(descriptors) {
|
|
|
106
109
|
extra.push('required: true');
|
|
107
110
|
if (options)
|
|
108
111
|
extra.push(`options: ${emitOptionsLiteral(options)}`);
|
|
112
|
+
if (d.isArray)
|
|
113
|
+
extra.push('multiple: true');
|
|
109
114
|
lines.push(` { ${extra.join(', ')} },`);
|
|
110
115
|
seen.add(d.name);
|
|
111
116
|
}
|
|
@@ -20,7 +20,7 @@ export interface CodeableConceptLike { text?: string; coding?: CodingLike[] }
|
|
|
20
20
|
export interface ReferenceLike { reference?: string; display?: string; type?: string; identifier?: IdentifierLike }
|
|
21
21
|
export interface QuantityLike { value?: number; unit?: string; code?: string; comparator?: string }
|
|
22
22
|
export interface PeriodLike { start?: string; end?: string }
|
|
23
|
-
export interface IdentifierLike { use?: string; system?: string; value?: string }
|
|
23
|
+
export interface IdentifierLike { use?: string; system?: string; value?: string; type?: CodeableConceptLike }
|
|
24
24
|
export interface AddressLike { line?: string[]; city?: string; state?: string; postalCode?: string; country?: string; text?: string; use?: string }
|
|
25
25
|
export interface ContactPointLike { system?: string; value?: string; use?: string }
|
|
26
26
|
export interface AttachmentLike { contentType?: string; url?: string; data?: string; title?: string }
|
|
@@ -30,8 +30,14 @@ export interface RangeLike { low?: QuantityLike; high?: QuantityLike }
|
|
|
30
30
|
export function formatHumanName(n?: NameLike): string | undefined {
|
|
31
31
|
if (!n) return undefined;
|
|
32
32
|
if (n.text) return n.text;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// prefix and suffix are part of the name, not decoration: dropping them
|
|
34
|
+
// renders "Jane Doe" for a record that says "Dr. Jane Doe MD".
|
|
35
|
+
const full = [
|
|
36
|
+
(n.prefix ?? []).join(' '),
|
|
37
|
+
(n.given ?? []).join(' '),
|
|
38
|
+
n.family,
|
|
39
|
+
(n.suffix ?? []).join(' '),
|
|
40
|
+
].filter(Boolean).join(' ').trim();
|
|
35
41
|
return full || undefined;
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -83,12 +89,17 @@ export function formatQuantity(q?: QuantityLike): string | undefined {
|
|
|
83
89
|
export function formatPeriod(p?: PeriodLike): string | undefined {
|
|
84
90
|
if (!p) return undefined;
|
|
85
91
|
if (!p.start && !p.end) return undefined;
|
|
86
|
-
|
|
92
|
+
// An absent end means the period has not closed, which is not the same as
|
|
93
|
+
// an end nobody recorded.
|
|
94
|
+
return \`\${p.start ?? '?'} – \${p.end ?? 'ongoing'}\`;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
export function formatIdentifier(i?: IdentifierLike): string | undefined {
|
|
90
|
-
if (!i) return undefined;
|
|
91
|
-
|
|
98
|
+
if (!i?.value) return undefined;
|
|
99
|
+
// A bare "12345" says nothing next to a second identifier; the type is what
|
|
100
|
+
// tells the reader which number they are looking at.
|
|
101
|
+
const label = formatCodeableConcept(i.type);
|
|
102
|
+
return label ? \`\${label}: \${i.value}\` : i.value;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
export function formatAddress(a?: AddressLike): string | undefined {
|
|
@@ -237,10 +248,11 @@ import { registerPipe, type PipeFn } from '@maxhealth.tech/prefab';
|
|
|
237
248
|
const humanName: PipeFn = (value) => {
|
|
238
249
|
const format = (n: unknown): string => {
|
|
239
250
|
if (!n || typeof n !== 'object') return '';
|
|
240
|
-
const name = n as { text?: string; given?: string[]; family?: string };
|
|
251
|
+
const name = n as { text?: string; prefix?: string[]; given?: string[]; family?: string; suffix?: string[] };
|
|
241
252
|
if (name.text) return name.text;
|
|
242
|
-
const
|
|
243
|
-
return [given, name.family
|
|
253
|
+
const joined = (v: unknown): string => (Array.isArray(v) ? v.join(' ') : '');
|
|
254
|
+
return [joined(name.prefix), joined(name.given), name.family, joined(name.suffix)]
|
|
255
|
+
.filter(Boolean).join(' ').trim();
|
|
244
256
|
};
|
|
245
257
|
if (Array.isArray(value)) {
|
|
246
258
|
return value.map(format).filter(Boolean).join(', ');
|
|
@@ -273,13 +285,25 @@ const address: PipeFn = (value) => {
|
|
|
273
285
|
return format(value);
|
|
274
286
|
};
|
|
275
287
|
|
|
276
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Format a ContactPoint (telecom).
|
|
290
|
+
*
|
|
291
|
+
* One contact renders bare, because whatever labels it already says which
|
|
292
|
+
* system it is. A list mixes systems, so each entry names its own — without it
|
|
293
|
+
* a phone number and a fax number are two indistinguishable strings.
|
|
294
|
+
*/
|
|
277
295
|
const contactPoint: PipeFn = (value) => {
|
|
278
296
|
const format = (cp: unknown): string => {
|
|
279
297
|
if (!cp || typeof cp !== 'object') return '';
|
|
280
298
|
return String((cp as { value?: unknown }).value ?? '');
|
|
281
299
|
};
|
|
282
|
-
|
|
300
|
+
const labelled = (cp: unknown): string => {
|
|
301
|
+
const val = format(cp);
|
|
302
|
+
if (!val) return '';
|
|
303
|
+
const sys = (cp as { system?: unknown }).system;
|
|
304
|
+
return typeof sys === 'string' && sys ? sys + ': ' + val : val;
|
|
305
|
+
};
|
|
306
|
+
if (Array.isArray(value)) return value.map(labelled).filter(Boolean).join(', ');
|
|
283
307
|
return format(value);
|
|
284
308
|
};
|
|
285
309
|
|
|
@@ -294,7 +318,12 @@ const contactPointBySystem: PipeFn = (value, system) => {
|
|
|
294
318
|
.join(', ');
|
|
295
319
|
};
|
|
296
320
|
|
|
297
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Format ContactPoint[] entries whose \`system\` is neither 'phone' nor 'email'.
|
|
323
|
+
*
|
|
324
|
+
* This is the leftover bucket — fax, pager, url, sms — so each entry names its
|
|
325
|
+
* system, exactly as a mixed list does above.
|
|
326
|
+
*/
|
|
298
327
|
const contactPointOther: PipeFn = (value) => {
|
|
299
328
|
if (!Array.isArray(value)) return '';
|
|
300
329
|
return value
|
|
@@ -302,16 +331,28 @@ const contactPointOther: PipeFn = (value) => {
|
|
|
302
331
|
const s = cp && (cp as { system?: unknown }).system;
|
|
303
332
|
return typeof s === 'string' && s !== 'phone' && s !== 'email';
|
|
304
333
|
})
|
|
305
|
-
.map((cp) =>
|
|
334
|
+
.map((cp) => {
|
|
335
|
+
const val = String((cp as { value?: unknown }).value ?? '');
|
|
336
|
+
return val ? String((cp as { system?: unknown }).system) + ': ' + val : '';
|
|
337
|
+
})
|
|
306
338
|
.filter(Boolean)
|
|
307
339
|
.join(', ');
|
|
308
340
|
};
|
|
309
341
|
|
|
310
|
-
/** Format an Identifier
|
|
342
|
+
/** Format an Identifier as \`type: value\`, or the bare value when untyped. */
|
|
311
343
|
const identifier: PipeFn = (value) => {
|
|
312
344
|
const format = (i: unknown): string => {
|
|
313
345
|
if (!i || typeof i !== 'object') return '';
|
|
314
|
-
|
|
346
|
+
const ident = i as {
|
|
347
|
+
value?: unknown;
|
|
348
|
+
type?: { text?: string; coding?: Array<{ display?: string; code?: string }> };
|
|
349
|
+
};
|
|
350
|
+
const val = String(ident.value ?? '');
|
|
351
|
+
if (!val) return '';
|
|
352
|
+
const type = ident.type;
|
|
353
|
+
const first = type && Array.isArray(type.coding) ? type.coding[0] : undefined;
|
|
354
|
+
const label = (type && type.text) || (first && (first.display ?? first.code)) || '';
|
|
355
|
+
return label ? label + ': ' + val : val;
|
|
315
356
|
};
|
|
316
357
|
if (Array.isArray(value)) return value.map(format).filter(Boolean).join(', ');
|
|
317
358
|
return format(value);
|
|
@@ -417,7 +458,7 @@ const period: PipeFn = (value) => {
|
|
|
417
458
|
if (!p || typeof p !== 'object') return '';
|
|
418
459
|
const per = p as { start?: string; end?: string };
|
|
419
460
|
if (!per.start && !per.end) return '';
|
|
420
|
-
return \`\${per.start ?? '?'} – \${per.end ?? '
|
|
461
|
+
return \`\${per.start ?? '?'} – \${per.end ?? 'ongoing'}\`;
|
|
421
462
|
};
|
|
422
463
|
if (Array.isArray(value)) return value.map(format).filter(Boolean).join(', ');
|
|
423
464
|
return format(value);
|
|
@@ -263,12 +263,17 @@ emitInvariants = false) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
// Collect refinements (pattern constraints, slice cardinality, FHIRPath invariants, required bindings)
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
//
|
|
267
|
+
// Emitted for DataType profiles too, Extension among them. They used to be skipped
|
|
268
|
+
// wholesale because "DataType random() methods don't produce pattern-aware data" —
|
|
269
|
+
// a property of the test fixtures, deciding what conformance rules a shipped
|
|
270
|
+
// schema enforces. The cost was that an Extension profile's zod schema checked its
|
|
271
|
+
// fixed url and nothing else: sdc's AssembleExpectation carries a required binding
|
|
272
|
+
// to the assemble-expectation ValueSet, validate() enforced it, and the schema
|
|
273
|
+
// accepted any code at all. Over a hundred profiles disagreed that way, and the
|
|
274
|
+
// disagreement was recorded as a parity failure rather than as the missing check
|
|
275
|
+
// it was.
|
|
276
|
+
const refinementResult = buildRefinements(allFields, rootPrefix, valueSets, emitInvariants);
|
|
272
277
|
// Merge ValueSet imports from binding refinements
|
|
273
278
|
for (const [name, importPath] of refinementResult.valueSetImports) {
|
|
274
279
|
if (!valueSetImports.has(name)) {
|
|
@@ -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');
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* are genuinely identical across R4 / R4B / R5 so they live here as shared
|
|
11
11
|
* constants.
|
|
12
12
|
*/
|
|
13
|
-
import { TS } from '../core/constants.js';
|
|
14
13
|
// ── Shared constants (version-independent) ──────────────────────────────────
|
|
15
14
|
/** FHIR primitive type → TypeScript type mapping (same for R4/R4B/R5). */
|
|
16
15
|
const PRIMITIVE_MAP = {
|
|
@@ -46,8 +45,6 @@ export const VALUESET_THRESHOLDS = {
|
|
|
46
45
|
UNION_TYPE: 20,
|
|
47
46
|
STORE_CODES: 50,
|
|
48
47
|
};
|
|
49
|
-
/** Re-export centralized terminology systems for backward compatibility. */
|
|
50
|
-
export const FHIR_TERMINOLOGY_SYSTEMS = TS;
|
|
51
48
|
// ── Cardinality / child-type helpers ────────────────────────────────────────
|
|
52
49
|
/**
|
|
53
50
|
* Fields whose `use` / `type` properties are enum codes that should *not*
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import type { FhirRules } from './types.js';
|
|
15
15
|
import type { DerivedVersionData } from './corePackageResolver.js';
|
|
16
|
-
import { TS } from '../core/constants.js';
|
|
17
16
|
|
|
18
17
|
// ── Shared constants (version-independent) ──────────────────────────────────
|
|
19
18
|
|
|
@@ -54,9 +53,6 @@ export const VALUESET_THRESHOLDS = {
|
|
|
54
53
|
STORE_CODES: 50,
|
|
55
54
|
} as const;
|
|
56
55
|
|
|
57
|
-
/** Re-export centralized terminology systems for backward compatibility. */
|
|
58
|
-
export const FHIR_TERMINOLOGY_SYSTEMS = TS;
|
|
59
|
-
|
|
60
56
|
// ── Cardinality / child-type helpers ────────────────────────────────────────
|
|
61
57
|
|
|
62
58
|
/**
|
|
@@ -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');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.11",
|
|
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",
|
|
@@ -86,32 +86,32 @@
|
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@eslint/js": "^10.0.1",
|
|
89
|
-
"@max-health-inc/config": "^3.
|
|
90
|
-
"@maxhealth.tech/prefab": "^0.3.
|
|
91
|
-
"@types/node": "^
|
|
89
|
+
"@max-health-inc/config": "^3.3.0",
|
|
90
|
+
"@maxhealth.tech/prefab": "^0.3.12",
|
|
91
|
+
"@types/node": "^26.4.0",
|
|
92
92
|
"@types/semver": "^7.8.0",
|
|
93
93
|
"@types/unzipper": "^0.10.11",
|
|
94
|
-
"@vitest/coverage-v8": "^4.1.
|
|
95
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
96
|
-
"@vitest/ui": "^4.1.
|
|
94
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
95
|
+
"@vitest/eslint-plugin": "^1.6.27",
|
|
96
|
+
"@vitest/ui": "^4.1.11",
|
|
97
97
|
"cross-env": "^10.1.0",
|
|
98
|
-
"eslint": "^10.
|
|
98
|
+
"eslint": "^10.9.1",
|
|
99
99
|
"fhirpath": "^5.2.0",
|
|
100
|
-
"globals": "^17.
|
|
100
|
+
"globals": "^17.11.0",
|
|
101
101
|
"rimraf": "^6.1.3",
|
|
102
102
|
"semver": "^7.8.5",
|
|
103
103
|
"typescript": "^6.0.3",
|
|
104
|
-
"typescript-eslint": "^8.
|
|
104
|
+
"typescript-eslint": "^8.68.0",
|
|
105
105
|
"vitepress": "^1.6.4",
|
|
106
|
-
"vitepress-plugin-llms": "^1.13.
|
|
107
|
-
"vitest": "^4.1.
|
|
108
|
-
"zod": "4.3
|
|
106
|
+
"vitepress-plugin-llms": "^1.13.5",
|
|
107
|
+
"vitest": "^4.1.11",
|
|
108
|
+
"zod": "4.4.3"
|
|
109
109
|
},
|
|
110
110
|
"dependencies": {
|
|
111
111
|
"@types/fhir": "^0.0.44",
|
|
112
112
|
"consola": "^3.4.2",
|
|
113
113
|
"tar": "^7.5.22",
|
|
114
|
-
"tsx": "^4.23.
|
|
114
|
+
"tsx": "^4.23.12",
|
|
115
115
|
"unzipper": "^0.12.5"
|
|
116
116
|
},
|
|
117
117
|
"overrides": {
|