babelfhir-ts 1.4.3 → 1.5.0
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/README.md +2 -0
- package/out/src/generator/emitters/prefab/prefabEmitter.js +160 -0
- package/out/src/generator/emitters/prefab/prefabRenderer.js +528 -0
- package/out/src/generator/emitters/prefab/prefabRuntimeAssets.js +492 -0
- package/out/src/generator/emitters/prefab/prefabTypeMapper.js +276 -0
- package/out/src/generator/index.js +35 -0
- package/out/src/generator/sdProcessor.js +4 -0
- package/out/src/main.js +16 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -233,6 +233,8 @@ Options:
|
|
|
233
233
|
--no-client Skip FHIR client generation (client generated by default)
|
|
234
234
|
--schema <format> Generate schema files alongside outputs (supported: zod)
|
|
235
235
|
--dicomweb Generate DICOMweb helpers typed to ImagingStudy profiles in the IG
|
|
236
|
+
--prefab Generate Prefab UI render functions per profile (@maxhealth.tech/prefab)
|
|
237
|
+
--prefab-styles <path> Path to styles module (.ts/.js) copied into the generated prefab/ folder
|
|
236
238
|
--recursive (update only) Recursively search subdirectories for lib/ folders
|
|
237
239
|
--force (update only) Force regeneration even if version and flags haven't changed
|
|
238
240
|
--outDir <dir> Output directory (alias for second positional argument)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefab UI emitter — writes per-profile render functions and the shared
|
|
3
|
+
* `prefab/` runtime files (fhirFormat.ts, styles.ts, index.ts).
|
|
4
|
+
*
|
|
5
|
+
* Called twice per generation pipeline:
|
|
6
|
+
* - {@link writePrefabProfile} once per processed profile during sdProcessor emission
|
|
7
|
+
* - {@link finalizePrefabOutput} once after all profiles, to drop shared files + barrel
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { generatePrefabRenderer } from './prefabRenderer.js';
|
|
12
|
+
import { FHIR_FORMAT_TS, DEFAULT_STYLES_TS, REGISTER_FHIR_PIPES_TS, buildPrefabIndexTs, } from './prefabRuntimeAssets.js';
|
|
13
|
+
import { logger } from '../../../logger.js';
|
|
14
|
+
const log = logger.withTag('prefab');
|
|
15
|
+
/** In-memory registry of profiles for which we've written renderer files. outputDir → (interfaceName → resourceType) */
|
|
16
|
+
const emittedProfiles = new Map();
|
|
17
|
+
function prefabDirFor(outputDir) {
|
|
18
|
+
return path.join(outputDir, 'prefab');
|
|
19
|
+
}
|
|
20
|
+
function ensurePrefabDir(outputDir) {
|
|
21
|
+
const dir = prefabDirFor(outputDir);
|
|
22
|
+
if (!fs.existsSync(dir))
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Emit `prefab/<InterfaceName>Prefab.ts` for a single profile.
|
|
28
|
+
* Registers the profile so {@link finalizePrefabOutput} can build the barrel.
|
|
29
|
+
*/
|
|
30
|
+
export function writePrefabProfile(outputDir, interfaceName, fields, resourceType) {
|
|
31
|
+
const dir = ensurePrefabDir(outputDir);
|
|
32
|
+
const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields);
|
|
33
|
+
const filePath = path.join(dir, `${interfaceName}Prefab.ts`);
|
|
34
|
+
fs.writeFileSync(filePath, code);
|
|
35
|
+
let map = emittedProfiles.get(outputDir);
|
|
36
|
+
if (!map) {
|
|
37
|
+
map = new Map();
|
|
38
|
+
emittedProfiles.set(outputDir, map);
|
|
39
|
+
}
|
|
40
|
+
map.set(interfaceName, resourceType ?? interfaceName);
|
|
41
|
+
log.debug(`Emitted prefab renderer for ${interfaceName} (${fieldCount} fields)`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write shared runtime assets (`fhirFormat.ts`, `styles.ts`, `index.ts`) and
|
|
45
|
+
* the barrel exporting every profile renderer.
|
|
46
|
+
*
|
|
47
|
+
* When `prefabStylesPath` is provided, its contents are copied verbatim as
|
|
48
|
+
* `prefab/styles.ts` so the user's theme travels with the generated package.
|
|
49
|
+
*/
|
|
50
|
+
export function finalizePrefabOutput(outputDir, opts = {}) {
|
|
51
|
+
const profileMap = emittedProfiles.get(outputDir);
|
|
52
|
+
if (!profileMap || profileMap.size === 0) {
|
|
53
|
+
log.debug('finalizePrefabOutput: no profiles emitted, skipping');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const names = [...profileMap.keys()];
|
|
57
|
+
const dir = ensurePrefabDir(outputDir);
|
|
58
|
+
fs.writeFileSync(path.join(dir, 'fhirFormat.ts'), FHIR_FORMAT_TS);
|
|
59
|
+
fs.writeFileSync(path.join(dir, 'registerFhirPipes.ts'), REGISTER_FHIR_PIPES_TS);
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'styles.ts'), resolveStylesContent(opts.prefabStylesPath));
|
|
61
|
+
fs.writeFileSync(path.join(dir, 'index.ts'), buildPrefabIndexTs(names));
|
|
62
|
+
fs.writeFileSync(path.join(dir, 'mcpViews.ts'), buildMcpViewsTs(profileMap));
|
|
63
|
+
log.success(`Generated prefab UI renderers for ${names.length} profile(s)${opts.prefabStylesPath ? ` (using styles from ${opts.prefabStylesPath})` : ''}`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the styles file content.
|
|
67
|
+
* - If `prefabStylesPath` is given and exists, its contents are returned.
|
|
68
|
+
* - Otherwise, the baked-in default is returned.
|
|
69
|
+
*/
|
|
70
|
+
function resolveStylesContent(prefabStylesPath) {
|
|
71
|
+
if (!prefabStylesPath)
|
|
72
|
+
return DEFAULT_STYLES_TS;
|
|
73
|
+
const resolved = path.resolve(prefabStylesPath);
|
|
74
|
+
if (!fs.existsSync(resolved)) {
|
|
75
|
+
log.warn(`--prefab-styles file not found: ${resolved} — falling back to defaults`);
|
|
76
|
+
return DEFAULT_STYLES_TS;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
log.warn(`Failed to read --prefab-styles file: ${err.message} — using defaults`);
|
|
83
|
+
return DEFAULT_STYLES_TS;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Test / CI helper: reset the in-memory registry between generations. */
|
|
87
|
+
export function resetPrefabState() {
|
|
88
|
+
emittedProfiles.clear();
|
|
89
|
+
}
|
|
90
|
+
// ── MCP View Registry generator ──────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Lower-case the first character(s) of a name, preserving acronym runs.
|
|
93
|
+
* Mirrors the same logic in prefabRenderer.ts.
|
|
94
|
+
*/
|
|
95
|
+
function lowerFirst(name) {
|
|
96
|
+
if (!name)
|
|
97
|
+
return name;
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < name.length && name[i] >= 'A' && name[i] <= 'Z')
|
|
100
|
+
i++;
|
|
101
|
+
if (i <= 1)
|
|
102
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
103
|
+
const acronymEnd = i === name.length ? i : i - 1;
|
|
104
|
+
return name.slice(0, acronymEnd).toLowerCase() + name.slice(acronymEnd);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the `mcpViews.ts` source — a `FhirViewRegistry` that maps FHIR
|
|
108
|
+
* resource types to their generated prefab table/detail components.
|
|
109
|
+
*
|
|
110
|
+
* When multiple profiles target the same base resource type (e.g.
|
|
111
|
+
* CHCorePatient and CHCorePatientEPR both extend Patient), the first
|
|
112
|
+
* one wins. Users can override via programmatic API if needed.
|
|
113
|
+
*/
|
|
114
|
+
function buildMcpViewsTs(profiles) {
|
|
115
|
+
// Group by resource type — first profile per type wins
|
|
116
|
+
const byResourceType = new Map();
|
|
117
|
+
for (const [interfaceName, resourceType] of profiles) {
|
|
118
|
+
if (!byResourceType.has(resourceType)) {
|
|
119
|
+
byResourceType.set(resourceType, interfaceName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const imports = [];
|
|
123
|
+
const tableEntries = [];
|
|
124
|
+
const detailEntries = [];
|
|
125
|
+
for (const [resourceType, interfaceName] of byResourceType) {
|
|
126
|
+
const varBase = lowerFirst(interfaceName);
|
|
127
|
+
imports.push(`import { ${varBase}Table, ${varBase}Detail } from './${interfaceName}Prefab.js';`);
|
|
128
|
+
tableEntries.push(` ${JSON.stringify(resourceType)}: (resources) => ${varBase}Table(resources as any),`);
|
|
129
|
+
detailEntries.push(` ${JSON.stringify(resourceType)}: (resource) => ${varBase}Detail(resource as any),`);
|
|
130
|
+
}
|
|
131
|
+
return `/**
|
|
132
|
+
* AUTO-GENERATED by babelfhir-ts --prefab — do not edit.
|
|
133
|
+
*
|
|
134
|
+
* FhirViewRegistry mapping FHIR resource types to profile-specific
|
|
135
|
+
* prefab components. Pass to \`createFhirMcpServer({ views })\` for
|
|
136
|
+
* rich, profile-aware UIs instead of generic auto-layout.
|
|
137
|
+
*
|
|
138
|
+
* @see https://babelfhir-ts.dev/generated-code
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
import type { FhirViewRegistry } from '@babelfhir-ts/mcp';
|
|
142
|
+
${imports.join('\n')}
|
|
143
|
+
|
|
144
|
+
const TABLE_MAP: Record<string, (resources: unknown[]) => unknown> = {
|
|
145
|
+
${tableEntries.join('\n')}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const DETAIL_MAP: Record<string, (resource: unknown) => unknown> = {
|
|
149
|
+
${detailEntries.join('\n')}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const views: FhirViewRegistry = {
|
|
153
|
+
table: (type, resources) => TABLE_MAP[type]?.(resources),
|
|
154
|
+
detail: (resource) => {
|
|
155
|
+
const type = (resource as { resourceType?: string }).resourceType;
|
|
156
|
+
return type ? DETAIL_MAP[type]?.(resource) : undefined;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
`;
|
|
160
|
+
}
|