babelfhir-ts 1.3.9 → 1.4.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 +12 -3
- package/out/src/bin.js +11 -0
- package/out/src/cli/installCommand.js +52 -1
- package/out/src/cli/updateCommand.js +361 -0
- package/out/src/generator/core/cacheConfig.js +4 -4
- package/out/src/generator/core/utils.js +0 -20
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +4 -6
- package/out/src/generator/emitters/interface/interfaceGenerator.js +2 -1
- package/out/src/generator/emitters/interface/postProcessExtensions.js +44 -19
- package/out/src/generator/emitters/interface/processNestedField.js +12 -4
- package/out/src/generator/emitters/validator/sliceBackboneValidation.js +11 -7
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +8 -4
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +69 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +78 -1
- package/out/src/generator/emitters/validator/validatorGenerator.js +5 -4
- package/out/src/generator/generationHelpers.js +2 -2
- package/out/src/generator/index.js +27 -11
- package/out/src/generator/parser/packageParser.js +20 -6
- package/out/src/generator/parser/sdFetcher.js +14 -14
- package/out/src/generator/parser/sdParser.js +4 -2
- package/out/src/generator/parser/txClient.js +9 -4
- package/out/src/generator/sdProcessor.js +2 -2
- package/out/src/generator/sdProcessorHelpers.js +2 -1
- package/out/src/main.js +91 -32
- package/package.json +4 -4
- /package/out/src/generator/fhir/r4/{base/backport-subscription.json → backport-subscription.json} +0 -0
- /package/out/src/generator/fhir/r4/{base/well-known-system-codes.json → well-known-system-codes.json} +0 -0
package/README.md
CHANGED
|
@@ -206,7 +206,8 @@ BabelFHIR-TS: Generate TypeScript interfaces from FHIR StructureDefinitions
|
|
|
206
206
|
|
|
207
207
|
Usage:
|
|
208
208
|
babelfhir-ts [options] [<input> [output]]
|
|
209
|
-
babelfhir-ts install [--package] <pkg@version|path> [--registry <url>] [options]
|
|
209
|
+
babelfhir-ts install [--package] <pkg[@version]|path> [--registry <url>] [options]
|
|
210
|
+
babelfhir-ts update [<pkg@version>] [--recursive] [options]
|
|
210
211
|
|
|
211
212
|
Arguments:
|
|
212
213
|
input Input can be:
|
|
@@ -219,6 +220,7 @@ Arguments:
|
|
|
219
220
|
|
|
220
221
|
Commands:
|
|
221
222
|
install Download, process, and npm install package as dependency
|
|
223
|
+
update Regenerate all installed packages (or a specific one) with current babelfhir-ts
|
|
222
224
|
|
|
223
225
|
Options:
|
|
224
226
|
-h, --help Show this help message
|
|
@@ -231,11 +233,14 @@ Options:
|
|
|
231
233
|
--no-client Skip FHIR client generation (client generated by default)
|
|
232
234
|
--schema <format> Generate schema files alongside outputs (supported: zod)
|
|
233
235
|
--dicomweb Generate DICOMweb helpers typed to ImagingStudy profiles in the IG
|
|
236
|
+
--recursive (update only) Recursively search subdirectories for lib/ folders
|
|
237
|
+
--outDir <dir> Output directory (alias for second positional argument)
|
|
234
238
|
--fhir-version <ver> FHIR version to target: r4, r4b, or r5 (auto-detected from package if omitted)
|
|
235
|
-
--package <pkg@version> Download FHIR package from registry and process it
|
|
239
|
+
--package <pkg[@version]> Download FHIR package from registry and process it (latest if no version)
|
|
236
240
|
--registry <url> FHIR package registry URL (default: https://packages.simplifier.net)
|
|
237
241
|
--tx-server <url> Terminology server URL for ValueSet expansion (e.g., https://tx.fhir.org/r4)
|
|
238
242
|
When set, expands ValueSets without explicit codes using $expand operation
|
|
243
|
+
--display-language <lang> BCP-47 language for display terms (e.g., de, fr). Passed to $expand displayLanguage
|
|
239
244
|
|
|
240
245
|
Examples:
|
|
241
246
|
babelfhir-ts # Process ./input to ./output
|
|
@@ -246,10 +251,14 @@ Examples:
|
|
|
246
251
|
babelfhir-ts --package hl7.fhir.us.core@8.0.0 # Download and process from default registry
|
|
247
252
|
babelfhir-ts --package hl7.fhir.us.core@8.0.0 output/ # Download and output to directory
|
|
248
253
|
babelfhir-ts --package pkg@version --log console --log-level debug # With verbose logging
|
|
249
|
-
babelfhir-ts install de.gematik.isik-basismodul
|
|
254
|
+
babelfhir-ts install de.gematik.isik-basismodul # Download latest, process, and install
|
|
255
|
+
babelfhir-ts install de.gematik.isik-basismodul@3.1.0 # Download specific version
|
|
250
256
|
babelfhir-ts install ./package.tgz # Install from local package file
|
|
251
257
|
babelfhir-ts install hl7.fhir.us.core@8.0.0 --registry <url> # Install from custom registry
|
|
252
258
|
babelfhir-ts install --package hl7.fhir.us.core@8.0.0 --registry <url> # Alternative syntax
|
|
259
|
+
babelfhir-ts update # Regenerate all packages in ./lib
|
|
260
|
+
babelfhir-ts update hl7.fhir.us.core@8.0.0 # Regenerate a specific package
|
|
261
|
+
babelfhir-ts update --recursive # Regenerate packages in all subdirectories
|
|
253
262
|
```
|
|
254
263
|
<!-- CLI_HELP_END -->
|
|
255
264
|
|
package/out/src/bin.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from './main.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
run()
|
|
5
|
+
.catch(err => {
|
|
6
|
+
console.error('Generation failed:', err instanceof Error ? err.message : err);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
})
|
|
9
|
+
.finally(() => {
|
|
10
|
+
logger.close();
|
|
11
|
+
});
|
|
@@ -63,6 +63,9 @@ export function buildInstallArgs(opts) {
|
|
|
63
63
|
const deps = projectPkg.dependencies || {};
|
|
64
64
|
for (const [, value] of Object.entries(deps)) {
|
|
65
65
|
if (typeof value === 'string' && (value === tgzRelPath || value === packagePath)) {
|
|
66
|
+
// Dep already points to the tgz. Use a plain `install` (no add)
|
|
67
|
+
// to avoid `bun add` creating duplicate keys in package.json.
|
|
68
|
+
// The .bun cache nuke (clearBunCache) ensures bun re-extracts.
|
|
66
69
|
return { args: ['install'] };
|
|
67
70
|
}
|
|
68
71
|
}
|
|
@@ -72,7 +75,7 @@ export function buildInstallArgs(opts) {
|
|
|
72
75
|
deps[name] = tgzRelPath;
|
|
73
76
|
projectPkg.dependencies = deps;
|
|
74
77
|
return {
|
|
75
|
-
args: [
|
|
78
|
+
args: [...pmArgs, tgzRelPath],
|
|
76
79
|
updatedPackageJson: JSON.stringify(projectPkg, null, 2) + '\n',
|
|
77
80
|
};
|
|
78
81
|
}
|
|
@@ -120,6 +123,49 @@ function npmInstall(packagePath) {
|
|
|
120
123
|
});
|
|
121
124
|
});
|
|
122
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Bun caches extracted tgz content in `node_modules/.bun/<pkg>@<path-hash>/`.
|
|
128
|
+
* The cache key is derived from the file *path*, not its content, so replacing a
|
|
129
|
+
* tgz at the same path silently serves stale content — even with `--force`.
|
|
130
|
+
* This function removes those cache entries so the next `bun add` re-extracts.
|
|
131
|
+
* In monorepos the .bun cache lives at the workspace root, not in the app subdir,
|
|
132
|
+
* so we walk up the directory tree to find it (same as detectPackageManager).
|
|
133
|
+
* No-op when bun is not the package manager or no .bun cache dir is found.
|
|
134
|
+
*/
|
|
135
|
+
function clearBunCache(packageName) {
|
|
136
|
+
const pm = detectPackageManager();
|
|
137
|
+
if (!pm.cmd.includes('bun'))
|
|
138
|
+
return;
|
|
139
|
+
// Walk up to find node_modules/.bun — in monorepos it's at the workspace root.
|
|
140
|
+
let bunCacheDir = null;
|
|
141
|
+
let dir = process.cwd();
|
|
142
|
+
while (true) {
|
|
143
|
+
const candidate = path.join(dir, 'node_modules', '.bun');
|
|
144
|
+
if (fs.existsSync(candidate)) {
|
|
145
|
+
bunCacheDir = candidate;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const parent = path.dirname(dir);
|
|
149
|
+
if (parent === dir)
|
|
150
|
+
break;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
if (!bunCacheDir)
|
|
154
|
+
return;
|
|
155
|
+
// Entries look like: <pkg>@<encoded-path>+<hash>
|
|
156
|
+
// The package name may contain dots/scopes — match by startsWith.
|
|
157
|
+
const prefix = `${packageName}@`;
|
|
158
|
+
let removed = 0;
|
|
159
|
+
for (const entry of fs.readdirSync(bunCacheDir, { withFileTypes: true })) {
|
|
160
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
161
|
+
fs.rmSync(path.join(bunCacheDir, entry.name), { recursive: true, force: true });
|
|
162
|
+
removed++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (removed > 0) {
|
|
166
|
+
console.log(`Cleared ${removed} stale bun cache entr${removed === 1 ? 'y' : 'ies'} for ${packageName}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
123
169
|
export async function handleInstallCommand(opts) {
|
|
124
170
|
const { packageToInstall, registry, generationFlags, downloadPackage } = opts;
|
|
125
171
|
// Keep cache by default — dependency packages are large (hl7.fhir.r4.core ~50 MB)
|
|
@@ -171,6 +217,11 @@ export async function handleInstallCommand(opts) {
|
|
|
171
217
|
cwd: generatedDir,
|
|
172
218
|
prefix: 'package'
|
|
173
219
|
}, ['.']);
|
|
220
|
+
// Bun caches extracted tgz content keyed by file path, not content hash.
|
|
221
|
+
// When the tgz is regenerated at the same path, bun serves stale content.
|
|
222
|
+
// Workaround: nuke the .bun cache entry so bun is forced to re-extract.
|
|
223
|
+
// See: https://github.com/oven-sh/bun/issues/29372
|
|
224
|
+
clearBunCache(packageName);
|
|
174
225
|
console.log(`Installing ${packageName}...`);
|
|
175
226
|
await npmInstall(tarballPath);
|
|
176
227
|
// Clean up extracted package tree (but keep the tarball under ./lib)
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `babelfhir-ts update` command.
|
|
3
|
+
*
|
|
4
|
+
* Discovers generated tgz packages under ./lib (or recursively), regenerates
|
|
5
|
+
* them with the current babelfhir-ts version, repacks the tgz, and reinstalls.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* babelfhir-ts update # update all in current project
|
|
9
|
+
* babelfhir-ts update --recursive # walk subdirectories too
|
|
10
|
+
* babelfhir-ts update <package-name@version> # update a single package
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { generateIntoPackageDirect, resetFetchFailureTracking } from '../generator/index.js';
|
|
16
|
+
import { getFhirPackagesCacheDir } from '../generator/core/cacheConfig.js';
|
|
17
|
+
import { resetDiagnostics, buildQualityReport, formatReportSummary } from '../generator/core/sdDiagnostics.js';
|
|
18
|
+
import { detectPackageManager } from './installCommand.js';
|
|
19
|
+
import { spawn } from 'child_process';
|
|
20
|
+
// ── Public entry point ──────────────────────────────────────────────────────
|
|
21
|
+
export async function handleUpdateCommand(opts) {
|
|
22
|
+
const { packageFilter, recursive, generationFlags, registry, downloadPackage, confirm } = opts;
|
|
23
|
+
const startDir = process.cwd();
|
|
24
|
+
// 1. Discover tgz files
|
|
25
|
+
const libDirs = discoverLibDirs(startDir, recursive ?? false);
|
|
26
|
+
if (libDirs.length === 0) {
|
|
27
|
+
if (packageFilter) {
|
|
28
|
+
return await promptInstallRedirect(packageFilter, confirm);
|
|
29
|
+
}
|
|
30
|
+
console.log('No lib/ directories found. Nothing to update.');
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
// 2. Read metadata from each tgz
|
|
34
|
+
const allTgz = await discoverTgzMetadata(libDirs);
|
|
35
|
+
if (allTgz.length === 0) {
|
|
36
|
+
if (packageFilter) {
|
|
37
|
+
return await promptInstallRedirect(packageFilter, confirm);
|
|
38
|
+
}
|
|
39
|
+
console.log('No generated packages found in lib/ directories.');
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
// 3. Filter if a specific package was requested
|
|
43
|
+
const targets = packageFilter ? filterBySpec(allTgz, packageFilter) : allTgz;
|
|
44
|
+
if (targets.length === 0 && packageFilter) {
|
|
45
|
+
console.log(`Package "${packageFilter}" is not installed.`);
|
|
46
|
+
if (allTgz.length > 0) {
|
|
47
|
+
console.log('Installed packages:');
|
|
48
|
+
for (const t of allTgz)
|
|
49
|
+
console.log(` ${t.igName}@${t.igVersion}`);
|
|
50
|
+
}
|
|
51
|
+
return await promptInstallRedirect(packageFilter, confirm);
|
|
52
|
+
}
|
|
53
|
+
console.log(`Found ${targets.length} package${targets.length === 1 ? '' : 's'} to update:\n`);
|
|
54
|
+
for (const t of targets)
|
|
55
|
+
console.log(` ${t.igName}@${t.igVersion} → ${path.relative(startDir, t.tgzPath)}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
// 4. Regenerate each package
|
|
58
|
+
let updated = 0;
|
|
59
|
+
let failed = 0;
|
|
60
|
+
const updatedPackages = [];
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
try {
|
|
63
|
+
await regeneratePackage(target, generationFlags, registry, downloadPackage);
|
|
64
|
+
updatedPackages.push({ generatedName: target.generatedName, tgzPath: target.tgzPath });
|
|
65
|
+
updated++;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
failed++;
|
|
69
|
+
console.error(`✗ Failed to update ${target.igName}@${target.igVersion}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 5. Patch bun.lock integrity hashes for regenerated packages (avoids full lockfile delete)
|
|
73
|
+
if (updatedPackages.length > 0) {
|
|
74
|
+
patchBunLockIntegrity(updatedPackages);
|
|
75
|
+
}
|
|
76
|
+
// 6. Single npm install at the end
|
|
77
|
+
if (updated > 0) {
|
|
78
|
+
console.log('\nReinstalling dependencies...');
|
|
79
|
+
await runInstall();
|
|
80
|
+
}
|
|
81
|
+
// 7. Summary
|
|
82
|
+
console.log('');
|
|
83
|
+
if (updated > 0)
|
|
84
|
+
console.log(`✓ Updated ${updated} package${updated === 1 ? '' : 's'}`);
|
|
85
|
+
if (failed > 0)
|
|
86
|
+
console.error(`✗ ${failed} package${failed === 1 ? '' : 's'} failed`);
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
// ── Install redirect prompt ─────────────────────────────────────────────────
|
|
90
|
+
async function promptInstallRedirect(packageFilter, confirm) {
|
|
91
|
+
const promptFn = confirm ?? defaultConfirm;
|
|
92
|
+
const yes = await promptFn(`Would you like to install ${packageFilter} instead? (Y/n) `);
|
|
93
|
+
if (yes)
|
|
94
|
+
return { redirectInstall: packageFilter };
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
/** Default Y/N prompt reading from stdin. */
|
|
98
|
+
function defaultConfirm(question) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
process.stdout.write(question);
|
|
101
|
+
const rl = (async () => {
|
|
102
|
+
const { createInterface } = await import('readline');
|
|
103
|
+
return createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
+
})();
|
|
105
|
+
rl.then(r => {
|
|
106
|
+
r.question('', (answer) => {
|
|
107
|
+
r.close();
|
|
108
|
+
resolve(!answer || answer.toLowerCase().startsWith('y'));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// ── Discovery ───────────────────────────────────────────────────────────────
|
|
114
|
+
/** Find all lib/ directories starting from startDir. Non-recursive returns only startDir/lib. */
|
|
115
|
+
function discoverLibDirs(startDir, recursive) {
|
|
116
|
+
const dirs = [];
|
|
117
|
+
const libDir = path.join(startDir, 'lib');
|
|
118
|
+
if (fs.existsSync(libDir) && fs.statSync(libDir).isDirectory()) {
|
|
119
|
+
dirs.push(libDir);
|
|
120
|
+
}
|
|
121
|
+
if (recursive) {
|
|
122
|
+
walkForLibDirs(startDir, dirs, 0);
|
|
123
|
+
}
|
|
124
|
+
return [...new Set(dirs)]; // deduplicate
|
|
125
|
+
}
|
|
126
|
+
/** Recursively walk looking for lib/ dirs (max depth 5, skip node_modules/.git) */
|
|
127
|
+
function walkForLibDirs(dir, result, depth) {
|
|
128
|
+
if (depth > 5)
|
|
129
|
+
return;
|
|
130
|
+
const SKIP = new Set(['node_modules', '.git', '.cache', '.bun', 'dist', 'build']);
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (!entry.isDirectory() || SKIP.has(entry.name))
|
|
140
|
+
continue;
|
|
141
|
+
const full = path.join(dir, entry.name);
|
|
142
|
+
if (entry.name === 'lib') {
|
|
143
|
+
result.push(full);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
walkForLibDirs(full, result, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Read metadata from all .tgz files in the given lib directories. */
|
|
151
|
+
async function discoverTgzMetadata(libDirs) {
|
|
152
|
+
const results = [];
|
|
153
|
+
const { list } = await import('tar');
|
|
154
|
+
for (const libDir of libDirs) {
|
|
155
|
+
const files = fs.readdirSync(libDir).filter(f => f.endsWith('.tgz'));
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const tgzPath = path.join(libDir, file);
|
|
158
|
+
const meta = await readTgzMetadata(tgzPath, list);
|
|
159
|
+
if (meta)
|
|
160
|
+
results.push(meta);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
/** Extract package.json from a tgz and read fhir.ig + fhir.version metadata. */
|
|
166
|
+
async function readTgzMetadata(tgzPath, listFn) {
|
|
167
|
+
let pkgJsonContent = '';
|
|
168
|
+
// Stream through the tgz looking for package/package.json
|
|
169
|
+
await listFn({
|
|
170
|
+
file: tgzPath,
|
|
171
|
+
onReadEntry: (entry) => {
|
|
172
|
+
const entryPath = entry.path.replace(/\\/g, '/');
|
|
173
|
+
if (entryPath === 'package/package.json' || entryPath === 'package.json') {
|
|
174
|
+
const chunks = [];
|
|
175
|
+
entry.on('data', (chunk) => chunks.push(chunk));
|
|
176
|
+
entry.on('end', () => { pkgJsonContent = Buffer.concat(chunks).toString('utf8'); });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
entry.resume(); // skip other entries
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (!pkgJsonContent)
|
|
184
|
+
return null;
|
|
185
|
+
try {
|
|
186
|
+
const pkg = JSON.parse(pkgJsonContent);
|
|
187
|
+
const igName = pkg?.fhir?.ig;
|
|
188
|
+
const igVersion = pkg?.fhir?.version;
|
|
189
|
+
if (!igName || !igVersion)
|
|
190
|
+
return null;
|
|
191
|
+
return {
|
|
192
|
+
tgzPath,
|
|
193
|
+
generatedName: pkg.name || '',
|
|
194
|
+
igName,
|
|
195
|
+
igVersion,
|
|
196
|
+
...(pkg?.fhir?.txServer && { txServer: pkg.fhir.txServer }),
|
|
197
|
+
...(pkg?.fhir?.displayLanguage && { displayLanguage: pkg.fhir.displayLanguage }),
|
|
198
|
+
...(pkg?.fhir?.dicomweb && { dicomweb: true }),
|
|
199
|
+
...(pkg?.fhir?.noClient && { noClient: true }),
|
|
200
|
+
...(pkg?.fhir?.noClasses && { noClasses: true }),
|
|
201
|
+
...(pkg?.fhir?.schema && { schema: pkg.fhir.schema }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ── Filtering ───────────────────────────────────────────────────────────────
|
|
209
|
+
/** Filter metadata list by a package spec like "hl7.fhir.us.core" or "hl7.fhir.us.core@8.0.0" */
|
|
210
|
+
function filterBySpec(all, spec) {
|
|
211
|
+
const atIdx = spec.lastIndexOf('@');
|
|
212
|
+
if (atIdx > 0) {
|
|
213
|
+
const name = spec.slice(0, atIdx);
|
|
214
|
+
const version = spec.slice(atIdx + 1);
|
|
215
|
+
return all.filter(t => t.igName === name && t.igVersion === version);
|
|
216
|
+
}
|
|
217
|
+
// No version — match by name only
|
|
218
|
+
return all.filter(t => t.igName === spec);
|
|
219
|
+
}
|
|
220
|
+
// ── Regeneration ────────────────────────────────────────────────────────────
|
|
221
|
+
async function regeneratePackage(target, generationFlags, registry, downloadPackage) {
|
|
222
|
+
const spec = `${target.igName}@${target.igVersion}`;
|
|
223
|
+
console.log(`\nUpdating ${spec}...`);
|
|
224
|
+
// Locate the source package: check FHIR cache first, download if needed
|
|
225
|
+
const sourcePath = await resolveSourcePackage(target.igName, target.igVersion, registry, downloadPackage);
|
|
226
|
+
// Merge generation flags from package metadata (CLI flags take priority)
|
|
227
|
+
const mergedFlags = { ...generationFlags };
|
|
228
|
+
if (target.txServer && !mergedFlags.txServer) {
|
|
229
|
+
mergedFlags.txServer = target.txServer;
|
|
230
|
+
console.log(` Using tx-server from package metadata: ${target.txServer}`);
|
|
231
|
+
}
|
|
232
|
+
if (target.displayLanguage && !mergedFlags.displayLanguage) {
|
|
233
|
+
mergedFlags.displayLanguage = target.displayLanguage;
|
|
234
|
+
console.log(` Using display-language from package metadata: ${target.displayLanguage}`);
|
|
235
|
+
}
|
|
236
|
+
if (target.dicomweb && mergedFlags.dicomweb === undefined)
|
|
237
|
+
mergedFlags.dicomweb = true;
|
|
238
|
+
if (target.noClient && mergedFlags.noClient === undefined)
|
|
239
|
+
mergedFlags.noClient = true;
|
|
240
|
+
if (target.noClasses && mergedFlags.noClasses === undefined)
|
|
241
|
+
mergedFlags.noClasses = true;
|
|
242
|
+
if (target.schema && !mergedFlags.schema)
|
|
243
|
+
mergedFlags.schema = target.schema;
|
|
244
|
+
resetFetchFailureTracking();
|
|
245
|
+
resetDiagnostics();
|
|
246
|
+
// Generate
|
|
247
|
+
const { generatedDir, cleanup } = await generateIntoPackageDirect(sourcePath, mergedFlags);
|
|
248
|
+
try {
|
|
249
|
+
// Repack the tgz at the same path
|
|
250
|
+
const { create } = await import('tar');
|
|
251
|
+
await create({ gzip: true, file: target.tgzPath, cwd: generatedDir, prefix: 'package' }, ['.']);
|
|
252
|
+
// Clear bun cache for this package (same as install command)
|
|
253
|
+
clearBunCacheForPackage(target.generatedName);
|
|
254
|
+
console.log(`✓ Regenerated: ${path.basename(target.tgzPath)}`);
|
|
255
|
+
const report = buildQualityReport();
|
|
256
|
+
const summary = formatReportSummary(report);
|
|
257
|
+
if (summary.trim())
|
|
258
|
+
console.log(summary);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
cleanup();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Find the source .tgz in the FHIR cache, or download it. */
|
|
265
|
+
async function resolveSourcePackage(igName, igVersion, registry, downloadPackage) {
|
|
266
|
+
const cacheDir = getFhirPackagesCacheDir();
|
|
267
|
+
const tgzPath = path.join(cacheDir, `${igName}-${igVersion}.tgz`);
|
|
268
|
+
const extractedDir = path.join(cacheDir, `${igName}@${igVersion}`);
|
|
269
|
+
// If extracted dir exists (most common case after first install), return the tgz path
|
|
270
|
+
if (fs.existsSync(extractedDir) && fs.readdirSync(extractedDir).length > 0) {
|
|
271
|
+
// The tgz may or may not exist — generateIntoPackageDirect can work with either
|
|
272
|
+
if (fs.existsSync(tgzPath))
|
|
273
|
+
return tgzPath;
|
|
274
|
+
// If only extracted dir exists, we need to find or create a tgz
|
|
275
|
+
// Try to repack from extracted dir
|
|
276
|
+
const { create } = await import('tar');
|
|
277
|
+
await create({ gzip: true, file: tgzPath, cwd: extractedDir }, ['.']);
|
|
278
|
+
return tgzPath;
|
|
279
|
+
}
|
|
280
|
+
// Not cached — download
|
|
281
|
+
console.log(` Package not in cache, downloading ${igName}@${igVersion}...`);
|
|
282
|
+
return downloadPackage(`${igName}@${igVersion}`, registry);
|
|
283
|
+
}
|
|
284
|
+
// ── Bun lockfile integrity patching ──────────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Surgically update SHA-512 integrity hashes in bun.lock for regenerated tgz files.
|
|
287
|
+
*
|
|
288
|
+
* Bun stores integrity hashes in bun.lock (text format) as the third element
|
|
289
|
+
* of each package entry array: `["pkg@path", {deps}, "sha512-<base64>"]`.
|
|
290
|
+
* When a tgz is regenerated, the old hash becomes stale and `bun install`
|
|
291
|
+
* fails with "Integrity check failed". Instead of deleting the entire lockfile
|
|
292
|
+
* (which re-resolves ALL packages), we compute the new hash and patch only
|
|
293
|
+
* the affected entries.
|
|
294
|
+
*
|
|
295
|
+
* @see https://github.com/oven-sh/bun/issues/29372
|
|
296
|
+
*/
|
|
297
|
+
export function patchBunLockIntegrity(updatedPackages) {
|
|
298
|
+
const pm = detectPackageManager();
|
|
299
|
+
if (!pm.cmd.includes('bun'))
|
|
300
|
+
return;
|
|
301
|
+
const lockPath = path.resolve('bun.lock');
|
|
302
|
+
if (!fs.existsSync(lockPath))
|
|
303
|
+
return;
|
|
304
|
+
let lockContent = fs.readFileSync(lockPath, 'utf8');
|
|
305
|
+
let patched = 0;
|
|
306
|
+
for (const { generatedName, tgzPath } of updatedPackages) {
|
|
307
|
+
// Compute new SHA-512 integrity hash
|
|
308
|
+
const tgzBytes = fs.readFileSync(tgzPath);
|
|
309
|
+
const hash = crypto.createHash('sha512').update(tgzBytes).digest('base64');
|
|
310
|
+
const newIntegrity = `sha512-${hash}`;
|
|
311
|
+
// Match all entries for this package in bun.lock:
|
|
312
|
+
// "hl7.fhir.uv.ips-generated": [..., "sha512-oldHash=="],
|
|
313
|
+
// "workspace/hl7.fhir.uv.ips-generated": [..., "sha512-oldHash=="],
|
|
314
|
+
const pattern = new RegExp(`("(?:[^"]*\\/)?${escapeRegExp(generatedName)}"\\s*:\\s*\\[.*?)(sha512-[A-Za-z0-9+/]+=*)`, 'g');
|
|
315
|
+
lockContent = lockContent.replace(pattern, (_, prefix, _oldHash) => {
|
|
316
|
+
patched++;
|
|
317
|
+
return `${prefix}${newIntegrity}`;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (patched > 0) {
|
|
321
|
+
fs.writeFileSync(lockPath, lockContent, 'utf8');
|
|
322
|
+
console.log(`Patched ${patched} integrity hash${patched === 1 ? '' : 'es'} in bun.lock`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function escapeRegExp(s) {
|
|
326
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
327
|
+
}
|
|
328
|
+
// ── Bun cache workaround ────────────────────────────────────────────────────
|
|
329
|
+
function clearBunCacheForPackage(packageName) {
|
|
330
|
+
const pm = detectPackageManager();
|
|
331
|
+
if (!pm.cmd.includes('bun'))
|
|
332
|
+
return;
|
|
333
|
+
let dir = process.cwd();
|
|
334
|
+
while (true) {
|
|
335
|
+
const candidate = path.join(dir, 'node_modules', '.bun');
|
|
336
|
+
if (fs.existsSync(candidate)) {
|
|
337
|
+
const prefix = `${packageName}@`;
|
|
338
|
+
for (const entry of fs.readdirSync(candidate, { withFileTypes: true })) {
|
|
339
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
340
|
+
fs.rmSync(path.join(candidate, entry.name), { recursive: true, force: true });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
const parent = path.dirname(dir);
|
|
346
|
+
if (parent === dir)
|
|
347
|
+
break;
|
|
348
|
+
dir = parent;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Package manager install ─────────────────────────────────────────────────
|
|
352
|
+
function runInstall() {
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const pm = detectPackageManager();
|
|
355
|
+
const pmName = path.basename(pm.cmd).replace(/\.(cmd|exe)$/, '');
|
|
356
|
+
console.log(`Running ${pmName} install...`);
|
|
357
|
+
const child = spawn(pm.cmd, ['install'], { stdio: 'inherit', shell: true });
|
|
358
|
+
child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`${pmName} install failed (exit ${code})`)));
|
|
359
|
+
child.on('error', (err) => reject(new Error(`Failed to run ${pmName}: ${err.message}`)));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
@@ -67,8 +67,8 @@ export function getCacheConfig() {
|
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Override the cache configuration (useful for testing or CLI options).
|
|
70
|
-
*
|
|
71
|
-
* unless
|
|
70
|
+
* Only rootDir (working cache) is updated; fhirPackagesDir stays at ~/.fhir/packages
|
|
71
|
+
* unless explicitly provided or FHIR_CACHE_ROOT is set.
|
|
72
72
|
*/
|
|
73
73
|
export function setCacheConfig(config) {
|
|
74
74
|
const current = getCacheConfig();
|
|
@@ -77,8 +77,8 @@ export function setCacheConfig(config) {
|
|
|
77
77
|
...current,
|
|
78
78
|
...config,
|
|
79
79
|
rootDir: newRootDir,
|
|
80
|
-
//
|
|
81
|
-
fhirPackagesDir: config.fhirPackagesDir ??
|
|
80
|
+
// Only override fhirPackagesDir if explicitly provided; --cache-dir should NOT move the shared FHIR package cache
|
|
81
|
+
fhirPackagesDir: config.fhirPackagesDir ?? current.fhirPackagesDir,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
@@ -19,26 +19,6 @@ export function writeInterfaceAndValidatorToFile(outputDir, interfaceName, inter
|
|
|
19
19
|
const content = `${interfaceContent}\n\n${validateFunctionContent}`;
|
|
20
20
|
caseAwareWriteFile(filePath, content);
|
|
21
21
|
}
|
|
22
|
-
export function extractFieldsFromInterface(interfaceDecl) {
|
|
23
|
-
return interfaceDecl.getProperties().map((property) => {
|
|
24
|
-
const name = property.getName();
|
|
25
|
-
const type = property.getType().getText();
|
|
26
|
-
const isOptional = property.hasQuestionToken();
|
|
27
|
-
const isArray = type.endsWith("[]");
|
|
28
|
-
return {
|
|
29
|
-
constraints: [], // Placeholder for constraints
|
|
30
|
-
elements: [], // Placeholder for nested elements
|
|
31
|
-
fromBase: true, // Mark as coming from the base resource
|
|
32
|
-
isForbidden: false, // Interfaces extracted this way cannot infer forbidden state
|
|
33
|
-
isArray,
|
|
34
|
-
isOptional,
|
|
35
|
-
name,
|
|
36
|
-
type: isArray ? type.slice(0, -2) : type,
|
|
37
|
-
// Interfaces parsed from existing code cannot know profiling source
|
|
38
|
-
isProfiled: false,
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
22
|
export function capitalize(str) {
|
|
43
23
|
if (!str)
|
|
44
24
|
return str;
|
|
@@ -414,11 +414,6 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
414
414
|
treatAsNested = false;
|
|
415
415
|
}
|
|
416
416
|
else if (!isLogicalModelForNested) {
|
|
417
|
-
// Normally we flatten direct children of the base resource (e.g., DocumentReference.content)
|
|
418
|
-
// so we don't re-define backbone interfaces unnecessarily. However, if there are
|
|
419
|
-
// profiled descendants (e.g., DocumentReference.content.attachment with a profiled Attachment),
|
|
420
|
-
// we MUST keep this as a distinct nested interface so we can override those descendants while
|
|
421
|
-
// still extending the original backbone element type (e.g., DocumentReferenceContent).
|
|
422
417
|
const hasProfiledDescendant = fields.some(f => f.name.startsWith(field.name + '.') && f.isProfiled);
|
|
423
418
|
// Also keep as nested when the element has typed extension slices (e.g., Encounter.hospitalization.extension)
|
|
424
419
|
const hasChildExtSlices = fields.some(f => f.name === `${field.name}.extension` && f.sliceName && (f.profileUrls || []).length > 0);
|
|
@@ -428,8 +423,11 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
428
423
|
else {
|
|
429
424
|
// Special case: primitive direct base child with child extension slices (e.g., gender)
|
|
430
425
|
// should NOT be modeled as a nested interface; the extension must live on sidecar _field Element.
|
|
426
|
+
// Use field.type first — baseFieldByLastSegment can return wrong type due to
|
|
427
|
+
// name collisions (e.g., 'text' resolves to Questionnaire.text=Narrative
|
|
428
|
+
// instead of Questionnaire.item.text=string).
|
|
431
429
|
const baseFld = baseFieldByLastSegment.get(fieldName);
|
|
432
|
-
const mappedBase = mapTypeToTS((
|
|
430
|
+
const mappedBase = mapTypeToTS((field.type || baseFld?.type || 'string'));
|
|
433
431
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
434
432
|
logger.debug('[CHECK treatAsNested for line]', { hasChildExtSlices, isPrimitiveType: isPrimitiveType(mappedBase), mappedBase, baseFld: baseFld?.type, fieldType: field.type });
|
|
435
433
|
}
|
|
@@ -7,7 +7,7 @@ import { getRules, ctx as versionCtx } from '../../fhir/versionContext.js';
|
|
|
7
7
|
import { postProcessExtensions, safetyNetRootExtensions } from './postProcessExtensions.js';
|
|
8
8
|
import { processFields } from './interfaceFieldProcessor.js';
|
|
9
9
|
const log = logger.withTag('interfaces');
|
|
10
|
-
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap, profileIdToName, profileUrlToName) {
|
|
10
|
+
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap, profileIdToName, profileUrlToName, isLogicalModel = false) {
|
|
11
11
|
const debug = (...args) => log.debug(...args);
|
|
12
12
|
// Use FHIR rules from version context
|
|
13
13
|
const rules = getRules();
|
|
@@ -151,6 +151,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
151
151
|
resourceType, existingProfiles, fhirChildTypeMap, profileUrlToName,
|
|
152
152
|
interfaces, localInterfaceNames, writtenLines, generatedAliasTypes,
|
|
153
153
|
importManager, baseFieldByLastSegment, baseFieldAnyArray,
|
|
154
|
+
isLogicalModel,
|
|
154
155
|
debug, addTypeImport, getBaseTypeForField, findInterfaceIndex,
|
|
155
156
|
inferBackboneType, applyBindingConstraint,
|
|
156
157
|
};
|