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.
Files changed (27) hide show
  1. package/README.md +12 -3
  2. package/out/src/bin.js +11 -0
  3. package/out/src/cli/installCommand.js +52 -1
  4. package/out/src/cli/updateCommand.js +361 -0
  5. package/out/src/generator/core/cacheConfig.js +4 -4
  6. package/out/src/generator/core/utils.js +0 -20
  7. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +4 -6
  8. package/out/src/generator/emitters/interface/interfaceGenerator.js +2 -1
  9. package/out/src/generator/emitters/interface/postProcessExtensions.js +44 -19
  10. package/out/src/generator/emitters/interface/processNestedField.js +12 -4
  11. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +11 -7
  12. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +8 -4
  13. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +69 -0
  14. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +78 -1
  15. package/out/src/generator/emitters/validator/validatorGenerator.js +5 -4
  16. package/out/src/generator/generationHelpers.js +2 -2
  17. package/out/src/generator/index.js +27 -11
  18. package/out/src/generator/parser/packageParser.js +20 -6
  19. package/out/src/generator/parser/sdFetcher.js +14 -14
  20. package/out/src/generator/parser/sdParser.js +4 -2
  21. package/out/src/generator/parser/txClient.js +9 -4
  22. package/out/src/generator/sdProcessor.js +2 -2
  23. package/out/src/generator/sdProcessorHelpers.js +2 -1
  24. package/out/src/main.js +91 -32
  25. package/package.json +4 -4
  26. /package/out/src/generator/fhir/r4/{base/backport-subscription.json → backport-subscription.json} +0 -0
  27. /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@3.1.0 # Download, process, and npm install
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: ['install'],
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
- * When rootDir is changed, fhirPackagesDir is also updated to <rootDir>/.fhir/packages/
71
- * unless fhirPackagesDir is explicitly provided.
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
- // If rootDir changed but fhirPackagesDir not explicitly set, derive from new rootDir
81
- fhirPackagesDir: config.fhirPackagesDir ?? (config.rootDir ? path.join(newRootDir, '.fhir', 'packages') : current.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((baseFld?.type || field.type || 'string'));
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
  };