babelfhir-ts 1.0.38 → 1.0.40

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 CHANGED
@@ -48,11 +48,11 @@ Every pull request runs two independent CI pipelines that validate generated cod
48
48
 
49
49
  The first pipeline validates generated resources using the [Firely .NET SDK validator](https://docs.fire.ly/projects/Firely-NET-SDK/) (v3.0.1). Results are published as live badges:
50
50
 
51
- ![US Core 8.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-us-core.json)
52
- ![ISiK Basis 4.0.3](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-isik-basis.json)
53
- ![IPS 2.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-ips.json)
54
- ![SMART 2.2.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-smart.json)
55
- ![CH Core 5.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-ch-core.json)
51
+ ![US Core 8.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-us-core.json)
52
+ ![ISiK Basis 4.0.3](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-isik-basis.json)
53
+ ![IPS 2.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ips.json)
54
+ ![SMART 2.2.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-smart.json)
55
+ ![CH Core 5.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ch-core.json)
56
56
 
57
57
  > 11 profiles are excluded from this pipeline due to schema loading issues in the Firely SDK. These profiles validate successfully with the HL7 Java Validator below. Details in [docs/FIRELY-VALIDATOR-BUGS.md](./docs/FIRELY-VALIDATOR-BUGS.md).
58
58
 
@@ -60,15 +60,15 @@ The first pipeline validates generated resources using the [Firely .NET SDK vali
60
60
 
61
61
  The second pipeline validates using the [official HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator) (v6.3.11), the reference implementation for FHIR conformance checking:
62
62
 
63
- ![US Core 8.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-hl7-us-core.json)
64
- ![ISiK Basis 4.0.3](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-hl7-isik-basis.json)
65
- ![IPS 2.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-hl7-ips.json)
66
- ![SMART 2.2.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-hl7-smart.json)
67
- ![CH Core 5.0.0](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f/raw/badge-hl7-ch-core.json)
63
+ ![US Core 8.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-us-core.json)
64
+ ![ISiK Basis 4.0.3](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-isik-basis.json)
65
+ ![IPS 2.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ips.json)
66
+ ![SMART 2.2.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-smart.json)
67
+ ![CH Core 5.0.0](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ch-core.json)
68
68
 
69
69
  > Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/r4` during generation to expand ValueSets and produce valid codes.
70
70
 
71
- 📊 **[Full Report & Badge Data](https://gist.github.com/quotentiroler/e9bc625cfe88a0a67a73abc06660f40f)**
71
+ 📊 **[Full Report](https://max-health-inc.github.io/BabelFHIR-TS/)**
72
72
  <!-- HL7-PARITY-BADGES:END -->
73
73
  <!-- PARITY-BADGES:END -->
74
74
 
@@ -238,7 +238,7 @@ function applyRequiredFieldHeuristic(rf, ctx) {
238
238
  return { expr: `skeletonCodeableConcept('http://loinc.org', ['72166-2'])` };
239
239
  }
240
240
  if (baseResource === 'Medication') {
241
- return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '1049502', 'Amoxicillin 250mg', 'Amoxicillin'), isObjectLiteral: true };
241
+ return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '1049502'), isObjectLiteral: true };
242
242
  }
243
243
  if (baseResource === 'Immunization') {
244
244
  return { expr: makeCodeableConcept('http://hl7.org/fhir/sid/cvx', '207', 'COVID-19 mRNA vaccine', 'COVID-19 mRNA vaccine'), isObjectLiteral: true };
@@ -248,7 +248,7 @@ function applyRequiredFieldHeuristic(rf, ctx) {
248
248
  return { expr: makeCodeableConcept('http://hl7.org/fhir/sid/cvx', '207', 'COVID-19 mRNA vaccine', 'COVID-19 mRNA vaccine'), isObjectLiteral: true };
249
249
  }
250
250
  if (rf.name === 'medicationCodeableConcept') {
251
- return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '313782', 'Amoxicillin 250mg', 'Amoxicillin'), isObjectLiteral: true };
251
+ return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '313782'), isObjectLiteral: true };
252
252
  }
253
253
  if (rf.name === 'valueCodeableConcept') {
254
254
  // Skip heuristic if there are nestedCodingSlices - let the dedicated handler take care of it
@@ -760,12 +760,15 @@ function generateSliceElement(slice, elementType) {
760
760
  const profileUrl = profileUrls[0]; // Primary profile URL
761
761
  log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
762
762
  const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
763
- // Extract resource type from profile URL if available (e.g., Composition-uv-ips -> Composition)
764
- // Also check child.type which may contain the base resource type
763
+ // Extract resource type: prefer baseTypeCode (authoritative from StructureDefinition type[].code),
764
+ // then try parsing profile URL, then fall back to slice name heuristics
765
765
  let resourceType = 'Composition'; // default
766
- if (profileUrl) {
766
+ if (child.baseTypeCode) {
767
+ resourceType = capitalise(child.baseTypeCode);
768
+ }
769
+ else if (profileUrl) {
767
770
  const profileName = profileUrl.split('/').pop() || '';
768
- // Extract base type from profile name (e.g., Composition-uv-ips, Patient-uv-ips)
771
+ // Extract base type from profile name (e.g., Composition-uv-ips -> Composition)
769
772
  if (profileName.includes('-')) {
770
773
  resourceType = capitalise(profileName.split('-')[0]);
771
774
  }
@@ -798,6 +801,12 @@ function generateSliceElement(slice, elementType) {
798
801
  else if (resourceType === 'Observation') {
799
802
  resourceExpr = `{ resourceType: 'Observation', id: randomId()${metaProfile}, status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, subject: { reference: 'Patient/' + randomId() } }`;
800
803
  }
804
+ else if (resourceType === 'Claim') {
805
+ resourceExpr = `{ resourceType: 'Claim', id: randomId()${metaProfile}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], provider: { reference: 'Practitioner/' + randomId() }, priority: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/processpriority', code: 'normal' }] }, insurance: [{ sequence: 1, focal: true, coverage: { reference: 'Coverage/' + randomId() } }] }`;
806
+ }
807
+ else if (resourceType === 'ClaimResponse') {
808
+ resourceExpr = `{ resourceType: 'ClaimResponse', id: randomId()${metaProfile}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], insurer: { reference: 'Organization/' + randomId() }, outcome: 'complete' }`;
809
+ }
801
810
  else if (resourceType === 'Composition') {
802
811
  // Composition requires special handling for different profiles
803
812
  // ISiKBerichtSubSysteme requires: text.status='extensions', LOINC code 55112-7, type.text required
@@ -924,6 +933,14 @@ function generateSliceElement(slice, elementType) {
924
933
  // Device slice
925
934
  return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Device', id: randomId()${metaProfileExpr}, type: { coding: [{ system: 'http://snomed.info/sct', code: '49062001', display: 'Device' }] } } }`;
926
935
  }
936
+ if (sliceName.includes('claimresponse')) {
937
+ // ClaimResponse slice
938
+ return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'ClaimResponse', id: randomId()${metaProfileExpr}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], insurer: { reference: 'Organization/' + randomId() }, outcome: 'complete' } }`;
939
+ }
940
+ if (sliceName.includes('claim')) {
941
+ // Claim slice (must come after claimresponse check)
942
+ return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Claim', id: randomId()${metaProfileExpr}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], provider: { reference: 'Practitioner/' + randomId() }, priority: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/processpriority', code: 'normal' }] }, insurance: [{ sequence: 1, focal: true, coverage: { reference: 'Coverage/' + randomId() } }] } }`;
943
+ }
927
944
  // Handle non-Bundle element types - don't return fullUrl structure for simple types
928
945
  if (elementType === 'CodeableConcept') {
929
946
  // For CodeableConcept slices (like category:us-core), return a valid CodeableConcept
@@ -1327,6 +1344,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1327
1344
  // Try to get system from slice's patternConstraint or profileUrls
1328
1345
  let system = 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor';
1329
1346
  let code = `'code-' + randomId().slice(0, 6)`;
1347
+ let codeResolved = false;
1330
1348
  let version;
1331
1349
  // Check if slice has a pattern constraint with system
1332
1350
  if (slice.patternConstraint && typeof slice.patternConstraint === 'object') {
@@ -1336,6 +1354,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1336
1354
  }
1337
1355
  if (pattern.code && typeof pattern.code === 'string') {
1338
1356
  code = `'${pattern.code}'`;
1357
+ codeResolved = true;
1339
1358
  }
1340
1359
  if (pattern.version && typeof pattern.version === 'string') {
1341
1360
  version = pattern.version;
@@ -1367,6 +1386,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1367
1386
  system = slice.binding.sampleCode.system;
1368
1387
  }
1369
1388
  code = JSON.stringify(slice.binding.sampleCode.code);
1389
+ codeResolved = true;
1370
1390
  }
1371
1391
  else if (slice.binding?.codes && slice.binding.codes.length > 0) {
1372
1392
  // Fallback to first code from the binding's codes array
@@ -1375,6 +1395,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1375
1395
  system = firstCode.system;
1376
1396
  }
1377
1397
  code = JSON.stringify(firstCode.code);
1398
+ codeResolved = true;
1378
1399
  }
1379
1400
  // Check if parent field has a binding that matches this slice's system
1380
1401
  // E.g., ISiKSchwangerschaftErwarteterEntbindungstermin binds code to SchwangerschaftEtMethodeVS
@@ -1383,6 +1404,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1383
1404
  // If parent has a binding and this slice uses LOINC, prefer parent's code
1384
1405
  if (parentBindingCode.system === 'http://loinc.org' || !parentBindingCode.system) {
1385
1406
  code = JSON.stringify(parentBindingCode.code);
1407
+ codeResolved = true;
1386
1408
  }
1387
1409
  }
1388
1410
  // Check requiredChildren from type profile for required fields like 'version' and 'system'
@@ -1414,13 +1436,23 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1414
1436
  }
1415
1437
  }
1416
1438
  }
1417
- codingProps.push(`system: ${JSON.stringify(system)}`);
1418
- if (version) {
1419
- codingProps.push(`version: ${JSON.stringify(version)}`);
1439
+ // If no static code was resolved, try runtime resolution via _codeResolver
1440
+ const bindingUri = slice.binding?.uri || rf.bindingUri;
1441
+ if (!codeResolved && bindingUri) {
1442
+ // Emit a spread expression that resolves at runtime from ValueSetRegistry
1443
+ const versionArg = version ? `, ${JSON.stringify(version)}` : '';
1444
+ codingElements.push(`{ ...resolveSliceCode(${className}._codeResolver, ${JSON.stringify(bindingUri)}, ${JSON.stringify(system)}${versionArg}) }`);
1445
+ }
1446
+ else {
1447
+ // Static code available or no binding URI - use traditional inline approach
1448
+ codingProps.push(`system: ${JSON.stringify(system)}`);
1449
+ if (version) {
1450
+ codingProps.push(`version: ${JSON.stringify(version)}`);
1451
+ }
1452
+ codingProps.push(`code: ${code}`);
1453
+ codingProps.push(`display: 'disp-' + randomId().slice(0, 6)`);
1454
+ codingElements.push(`{ ${codingProps.join(', ')} }`);
1420
1455
  }
1421
- codingProps.push(`code: ${code}`);
1422
- codingProps.push(`display: 'disp-' + randomId().slice(0, 6)`);
1423
- codingElements.push(`{ ${codingProps.join(', ')} }`);
1424
1456
  }
1425
1457
  const expr = `{ coding: [${codingElements.join(', ')}], text: 'text-' + randomId().slice(0, 6) }`;
1426
1458
  const initExpr = buildValueExpression(rf, expr, { isObjectLiteral: true });
@@ -1509,6 +1541,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1509
1541
  if (requiredInitBlock.includes('randomNPI(')) {
1510
1542
  helperImports.add('randomNPI');
1511
1543
  }
1544
+ if (requiredInitBlock.includes('resolveSliceCode(')) {
1545
+ helperImports.add('resolveSliceCode');
1546
+ }
1512
1547
  for (const helper of usedSkeletons) {
1513
1548
  helperImports.add(helper);
1514
1549
  }
@@ -32,6 +32,8 @@ export function generateClient(options) {
32
32
  generateSmartClient(clientDir, resourceTypes);
33
33
  generateIndex(clientDir, resourceTypes);
34
34
  generateReadme(clientDir);
35
+ // Install base client type declarations so tsc can resolve @babelfhir-ts/client-r4
36
+ installBaseClientTypes(options.outputDir);
35
37
  log.success(`Generated FHIR client with ${resourceTypes.length} resource types + SMART auth`);
36
38
  }
37
39
  /**
@@ -525,64 +527,50 @@ export function parseBundle(bundle: Bundle<FhirResource>): BundleParser {
525
527
  * Generate fhir-client.ts
526
528
  */
527
529
  function generateFhirClient(clientDir, resourceTypes) {
528
- const readerImports = resourceTypes
529
- .map((rt) => ` type ${rt.profileName}Reader,`)
530
- .join("\n");
531
- const writerImports = resourceTypes
532
- .map((rt) => ` type ${rt.profileName}Writer,`)
533
- .join("\n");
534
530
  const readerMethods = resourceTypes
535
531
  .map((rt) => {
536
532
  const methodName = rt.profileName.charAt(0).toLowerCase() + rt.profileName.slice(1);
537
- return ` ${methodName}(): ${rt.profileName}Reader {
538
- return new FhirResourceReader(this.baseUrl, "${rt.baseResourceType}", this.fetchFn);
533
+ return ` ${methodName}() {
534
+ return this.forType<GeneratedTypes.${rt.profileName}>("${rt.baseResourceType}");
539
535
  }`;
540
536
  })
541
537
  .join("\n\n");
542
538
  const writerMethods = resourceTypes
543
539
  .map((rt) => {
544
540
  const methodName = rt.profileName.charAt(0).toLowerCase() + rt.profileName.slice(1);
545
- return ` ${methodName}(): ${rt.profileName}Writer {
546
- return new FhirResourceWriterImpl(this.baseUrl, "${rt.baseResourceType}", this.fetchFn);
541
+ return ` ${methodName}() {
542
+ return this.forType<GeneratedTypes.${rt.profileName}>("${rt.baseResourceType}");
547
543
  }`;
548
544
  })
549
545
  .join("\n\n");
550
546
  const content = `import {
551
- ${readerImports}
552
- FhirResourceReader,
547
+ FhirReadClient as BaseFhirReadClient,
548
+ FhirWriteClient as BaseFhirWriteClient,
553
549
  type FetchFn,
554
- } from "./resource-reader.js";
555
- import {
556
- ${writerImports}
557
- FhirResourceWriterImpl,
558
- } from "./resource-writer.js";
550
+ } from "@babelfhir-ts/client-r4";
551
+ import type * as GeneratedTypes from "../index.js";
559
552
 
560
553
  /**
561
- * FHIR Client for reading resources
554
+ * Profile-specific FHIR Read Client.
555
+ * Extends the base R4 read client with typed profile accessors.
556
+ * Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
562
557
  */
563
- export class FhirReadClient {
564
- constructor(
565
- private readonly baseUrl: string,
566
- private readonly fetchFn?: FetchFn,
567
- ) {}
568
-
558
+ export class FhirReadClient extends BaseFhirReadClient {
569
559
  ${readerMethods}
570
560
  }
571
561
 
572
562
  /**
573
- * FHIR Client for writing resources
563
+ * Profile-specific FHIR Write Client.
564
+ * Extends the base R4 write client with typed profile accessors.
565
+ * Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
574
566
  */
575
- export class FhirWriteClient {
576
- constructor(
577
- private readonly baseUrl: string,
578
- private readonly fetchFn?: FetchFn,
579
- ) {}
580
-
567
+ export class FhirWriteClient extends BaseFhirWriteClient {
581
568
  ${writerMethods}
582
569
  }
583
570
 
584
571
  /**
585
572
  * Main FHIR Client — works with plain fetch or an authenticated fetch wrapper.
573
+ * Provides both base R4 accessors (inherited) and profile-specific accessors.
586
574
  *
587
575
  * @example
588
576
  * // Unauthenticated
@@ -590,6 +578,12 @@ ${writerMethods}
590
578
  *
591
579
  * // With custom fetch (e.g. from SmartFhirClient)
592
580
  * const client = new FhirClient("https://fhir.example.com", authenticatedFetch);
581
+ *
582
+ * // Base R4 methods (inherited from @babelfhir-ts/client-r4)
583
+ * const pt = await client.read().patient().read("123");
584
+ *
585
+ * // Profile-specific methods (generated)
586
+ * const claim = await client.read().pASClaim().search({ status: "active" });
593
587
  */
594
588
  export class FhirClient {
595
589
  private readonly readClient: FhirReadClient;
@@ -1437,3 +1431,73 @@ const client = new FhirClient('https://fhir.example.com/fhir', loggingFetch);
1437
1431
  `;
1438
1432
  fs.writeFileSync(path.join(clientDir, "README.md"), content);
1439
1433
  }
1434
+ /**
1435
+ * Install @babelfhir-ts/client-r4 type declarations into the generated output's
1436
+ * node_modules so tsc can resolve the import during compilation.
1437
+ * At runtime, the consumer installs the actual package via npm.
1438
+ */
1439
+ function installBaseClientTypes(outputDir) {
1440
+ const pkgDir = path.join(outputDir, "node_modules", "@babelfhir-ts", "client-r4");
1441
+ fs.mkdirSync(pkgDir, { recursive: true });
1442
+ // Minimal package.json
1443
+ fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({
1444
+ name: "@babelfhir-ts/client-r4",
1445
+ version: "0.0.0-types",
1446
+ types: "index.d.ts"
1447
+ }, null, 2));
1448
+ // Type declarations matching the base package's public API
1449
+ const dts = `/// <reference types="@types/fhir" />
1450
+
1451
+ export type FetchFn = typeof globalThis.fetch;
1452
+
1453
+ export type WithId<T> = T & { id: string };
1454
+
1455
+ export type SearchParams = Record<string, boolean | number | string | string[] | undefined>;
1456
+
1457
+ export interface Bundle<T = fhir4.Resource> {
1458
+ resourceType: "Bundle";
1459
+ type: "collection" | "searchset" | "transaction-response" | "transaction" | "batch" | "batch-response" | "document" | "message" | "history";
1460
+ total?: number;
1461
+ link?: { relation: string; url: string }[];
1462
+ entry?: { resource?: T; fullUrl?: string; search?: { mode?: string; score?: number }; request?: { method: string; url: string }; response?: { status: string } }[];
1463
+ }
1464
+
1465
+ export declare class FhirResourceReader<T extends fhir4.Resource> {
1466
+ readonly baseUrl: string;
1467
+ readonly resourceType: string;
1468
+ constructor(baseUrl: string, resourceType: string, fetchFn?: FetchFn);
1469
+ read(id: string): Promise<WithId<T>>;
1470
+ search(params?: SearchParams): Promise<Bundle<WithId<T>>>;
1471
+ searchOne(params?: SearchParams): Promise<WithId<T> | undefined>;
1472
+ searchAll(params?: SearchParams): Promise<WithId<T>[]>;
1473
+ }
1474
+
1475
+ export declare class FhirResourceWriter<T extends fhir4.Resource> {
1476
+ readonly baseUrl: string;
1477
+ readonly resourceType: string;
1478
+ constructor(baseUrl: string, resourceType: string, fetchFn?: FetchFn);
1479
+ create(resource: T): Promise<WithId<T>>;
1480
+ update(resource: WithId<T>): Promise<WithId<T>>;
1481
+ delete(id: string): Promise<void>;
1482
+ createOrUpdate(resource: WithId<T>): Promise<WithId<T>>;
1483
+ }
1484
+
1485
+ export declare class FhirReadClient {
1486
+ constructor(baseUrl: string, fetchFn?: FetchFn);
1487
+ protected forType<T extends fhir4.Resource>(resourceType: string): FhirResourceReader<T>;
1488
+ }
1489
+
1490
+ export declare class FhirWriteClient {
1491
+ constructor(baseUrl: string, fetchFn?: FetchFn);
1492
+ protected forType<T extends fhir4.Resource>(resourceType: string): FhirResourceWriter<T>;
1493
+ }
1494
+
1495
+ export declare class FhirClient {
1496
+ readonly baseUrl: string;
1497
+ constructor(baseUrl: string, fetchFn?: FetchFn);
1498
+ read(): FhirReadClient;
1499
+ write(): FhirWriteClient;
1500
+ }
1501
+ `;
1502
+ fs.writeFileSync(path.join(pkgDir, "index.d.ts"), dts);
1503
+ }
@@ -1295,6 +1295,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
1295
1295
  '@types/fhir': '^0.0.41'
1296
1296
  }
1297
1297
  };
1298
+ // When client is generated, add @babelfhir-ts/client-r4 as dependency (generated client extends base)
1299
+ if (!flags?.noClient) {
1300
+ generatedPackageJson.dependencies['@babelfhir-ts/client-r4'] = '^0.2.0';
1301
+ }
1298
1302
  const generatedPackageJsonPath = path.join(outputDir, 'package.json');
1299
1303
  fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
1300
1304
  logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
@@ -448,19 +448,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
448
448
  if (system) {
449
449
  codingLines.push(` system: "${system}";`);
450
450
  }
451
- // Use valueset type for code if available, otherwise use literal
452
- if (valueSetTypeName && !valueSetSystem) {
453
- // Valueset with mixed systems - constrain code only
454
- codingLines.push(` code: ${valueSetTypeName};`);
455
- }
456
- else if (valueSetTypeName && valueSetSystem && system === valueSetSystem) {
457
- // Valueset with uniform system matching the pattern - use valueset type
458
- codingLines.push(` code: ${valueSetTypeName};`);
459
- }
460
- else {
461
- // No valueset or system mismatch - use literal code
462
- codingLines.push(` code: "${code}";`);
463
- }
451
+ // For FIXED pattern constraints, always use the literal code type.
452
+ // The pattern value may not be a member of the bound ValueSet
453
+ // (e.g., panel code "85354-9" vs individual vital signs VS).
454
+ codingLines.push(` code: "${code}";`);
464
455
  });
465
456
  // Only generate the coding interface if it has content
466
457
  if (codingLines.length > 0) {
@@ -492,13 +483,9 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
492
483
  if (patternSystem) {
493
484
  codingLines.push(` system: "${patternSystem}";`);
494
485
  }
495
- // Use valueset type for code if available
496
- if (valueSetTypeName && (!valueSetSystem || !patternSystem || valueSetSystem === patternSystem)) {
497
- codingLines.push(` code: ${valueSetTypeName};`);
498
- }
499
- else {
500
- codingLines.push(` code: "${pattern.code}";`);
501
- }
486
+ // For FIXED pattern constraints, always use the literal code type.
487
+ // The pattern value may not be a member of the bound ValueSet.
488
+ codingLines.push(` code: "${pattern.code}";`);
502
489
  importManager.addFhirType('Coding');
503
490
  const codingInterface = `export interface ${codingInterfaceName} extends Coding {\n${codingLines.join('\n')}\n}`;
504
491
  // Check if this interface already exists
@@ -1061,6 +1048,14 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1061
1048
  return;
1062
1049
  const existingLine = emittedLinesByField.get(fieldName);
1063
1050
  if (existingLine) {
1051
+ // Keep existing line if it's more specific (has type intersection or binding constraint)
1052
+ // Only replace if the new line is more specific than the existing one
1053
+ const existingIsConstrained = existingLine.includes(' & ') || existingLine.includes('Array<{');
1054
+ const newIsConstrained = line.includes(' & ') || line.includes('Array<{');
1055
+ if (existingIsConstrained && !newIsConstrained) {
1056
+ // Existing is more specific - skip the less-specific replacement
1057
+ return;
1058
+ }
1064
1059
  const idx = interfaceLines.indexOf(existingLine);
1065
1060
  if (idx >= 0)
1066
1061
  interfaceLines.splice(idx, 1);
@@ -1658,31 +1653,51 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
1658
1653
  addTypeImport(sanitizedParent);
1659
1654
  }
1660
1655
  if (interfaceLines.length > 0) {
1656
+ // Deduplicate fields by name: keep the first (more specific) version of each field.
1657
+ // This handles cases where the same field appears twice — once with a binding
1658
+ // constraint and once plain (e.g., type?: CodeableConcept & {...} vs type?: CodeableConcept).
1659
+ const seenFieldNames = new Set();
1660
+ const deduped = [];
1661
+ for (const line of interfaceLines) {
1662
+ // Extract field name from line (e.g., "type?: CodeableConcept;" -> "type")
1663
+ // Also handle JSDoc comments (/** ... */) which don't start with a field name
1664
+ const fieldMatch = line.match(/^(\w+)[?:]/);
1665
+ if (!fieldMatch) {
1666
+ deduped.push(line); // JSDoc or other non-field lines
1667
+ continue;
1668
+ }
1669
+ const name = fieldMatch[1];
1670
+ if (seenFieldNames.has(name)) {
1671
+ continue; // Skip duplicate — keep the first (more specific) version
1672
+ }
1673
+ seenFieldNames.add(name);
1674
+ deduped.push(line);
1675
+ }
1661
1676
  localInterfaceNames.add(parentInterfaceName);
1662
1677
  if (parentInterfaceName === interfaceName) {
1663
1678
  const sanitizedBaseResource = baseResource ? sanitizeIdentifier(baseResource) : undefined;
1664
1679
  const isBaseAbstract = sanitizedBaseResource === 'Base';
1665
1680
  const extendsClause = (sanitizedBaseResource && !isBaseAbstract && !isPrimitiveType(sanitizedBaseResource)) ? ` extends ${sanitizedBaseResource}` : '';
1666
- debug('emit root interface', parentInterfaceName, 'extends', sanitizedBaseResource, 'fields', interfaceLines.length);
1667
- interfaces.push(`export interface ${parentInterfaceName}${extendsClause} {\n${interfaceLines.map(l => ` ${l}`).join('\n')}\n}`);
1681
+ debug('emit root interface', parentInterfaceName, 'extends', sanitizedBaseResource, 'fields', deduped.length);
1682
+ interfaces.push(`export interface ${parentInterfaceName}${extendsClause} {\n${deduped.map(l => ` ${l}`).join('\n')}\n}`);
1668
1683
  }
1669
1684
  else {
1670
- debug('emit nested interface', parentInterfaceName, 'extends', parentType, 'fields', interfaceLines.length);
1685
+ debug('emit nested interface', parentInterfaceName, 'extends', parentType, 'fields', deduped.length);
1671
1686
  logger.debug('[DEBUG emit ALL nested interfaces]', {
1672
1687
  parentInterfaceName,
1673
1688
  parentType,
1674
1689
  parentFieldType,
1675
1690
  willExtend: parentType && !isPrimitiveType(parentType),
1676
- linesCount: interfaceLines.length
1691
+ linesCount: deduped.length
1677
1692
  });
1678
1693
  if (parentInterfaceName === 'USCoreQuestionnaireResponseProfileItem') {
1679
1694
  logger.debug('[DEBUG emit Item interface]', {
1680
- linesCount: interfaceLines.length,
1681
- lines: interfaceLines,
1682
- answerLines: interfaceLines.filter(l => l.includes('answer'))
1695
+ linesCount: deduped.length,
1696
+ lines: deduped,
1697
+ answerLines: deduped.filter(l => l.includes('answer'))
1683
1698
  });
1684
1699
  }
1685
- interfaces.push(`export interface ${parentInterfaceName}${parentType && !isPrimitiveType(parentType) ? ` extends ${parentType}` : ''} {\n${interfaceLines.map(l => ` ${l}`).join('\n')}\n}`);
1700
+ interfaces.push(`export interface ${parentInterfaceName}${parentType && !isPrimitiveType(parentType) ? ` extends ${parentType}` : ''} {\n${deduped.map(l => ` ${l}`).join('\n')}\n}`);
1686
1701
  }
1687
1702
  }
1688
1703
  }
@@ -75,7 +75,38 @@ export function skeletonContactPoint(): ContactPoint { return { system: 'phone',
75
75
  export function skeletonQuantity(): Quantity { return { value: randomInt(1,100), unit: randomString('u'), system: 'http://unitsofmeasure.org' }; }
76
76
 
77
77
  /**
78
- * Enrich a base resource with common fields based on resourceType and profile patterns.
78
+ * Resolve a coding for a nestedCodingSlice from a ValueSet binding at runtime.
79
+ * When a slice has a binding URI but no statically-known codes, this function
80
+ * attempts to resolve a valid code from the ValueSetRegistry via the codeResolver.
81
+ * Falls back to fake placeholder codes if resolution fails.
82
+ */
83
+ export function resolveSliceCode(
84
+ codeResolver: ((url: string) => { code: string; system: string; display?: string } | undefined) | undefined,
85
+ valueSetUrl: string | undefined,
86
+ fallbackSystem: string,
87
+ fallbackVersion?: string
88
+ ): { system: string; code: string; display: string; version?: string } {
89
+ if (valueSetUrl && codeResolver) {
90
+ const resolved = codeResolver(valueSetUrl);
91
+ if (resolved) {
92
+ const result: { system: string; code: string; display: string; version?: string } = {
93
+ system: resolved.system || fallbackSystem,
94
+ code: resolved.code,
95
+ display: resolved.display || resolved.code,
96
+ };
97
+ if (fallbackVersion) result.version = fallbackVersion;
98
+ return result;
99
+ }
100
+ }
101
+ const result: { system: string; code: string; display: string; version?: string } = {
102
+ system: fallbackSystem,
103
+ code: 'code-' + randomId().slice(0, 6),
104
+ display: 'disp-' + randomId().slice(0, 6),
105
+ };
106
+ if (fallbackVersion) result.version = fallbackVersion;
107
+ return result;
108
+ }
109
+
79
110
  /** Resolved code from a ValueSet binding */
80
111
  export type ResolvedCode = { code: string; system: string; display?: string };
81
112
 
@@ -520,12 +551,16 @@ export function enrichResource(
520
551
  }
521
552
  // Use proper UUIDs for fullUrl values
522
553
  if (!('entry' in base)) base.entry = [{ fullUrl: 'urn:uuid:' + randomUUID(), resource: { resourceType: 'Composition', id: randomId(), status: 'final', type: { coding: [{ system: 'http://loinc.org', code: '60591-5' }] }, subject: { reference: 'Patient/' + randomId() }, date: new Date().toISOString(), author: [{ reference: 'Practitioner/' + randomId() }], title: 'Example Document', section: [{ title: 'Section', text: { status: 'generated', div: '<div xmlns="http://www.w3.org/1999/xhtml">Section content</div>' } }] } }];
523
- // Fix existing entry fullUrls to use proper UUIDs
554
+ // Fix existing entry fullUrls to use proper UUIDs and empty resources
524
555
  if (Array.isArray(base.entry)) {
525
- for (const entry of base.entry as Array<{ fullUrl?: string }>) {
556
+ for (const entry of base.entry as Array<{ fullUrl?: string; resource?: Record<string, unknown> }>) {
526
557
  if (entry.fullUrl && entry.fullUrl.startsWith('urn:uuid:id-')) {
527
558
  entry.fullUrl = 'urn:uuid:' + randomUUID();
528
559
  }
560
+ // Fix empty resource objects — a resource must have at least a resourceType
561
+ if (entry.resource && typeof entry.resource === 'object' && !('resourceType' in entry.resource)) {
562
+ entry.resource = { resourceType: 'Basic', id: randomId(), code: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/basic-resource-type', code: 'study' }] } };
563
+ }
529
564
  }
530
565
  }
531
566
  }
@@ -785,8 +820,8 @@ export function enrichResource(
785
820
  }
786
821
 
787
822
  // Generic narrative for DomainResource derivatives
788
- // Note: Bundle and Binary do NOT support text/narrative elements
789
- if (/DomainResource|Patient|Account|Composition|CodeSystem|Condition|Encounter|Practitioner|Procedure|ValueSet|RelatedPerson|DocumentReference|Coverage/.test(baseResource) && !/Bundle|Binary/.test(baseResource)) {
823
+ // Bundle, Binary, and Parameters are pure Resource (not DomainResource) and do NOT support text/narrative
824
+ if (!/^(Bundle|Binary|Parameters)$/.test(baseResource)) {
790
825
  if (!('text' in base)) {
791
826
  // Check if there's a pattern constraint for text.status
792
827
  const textStatusPattern = getPrimitivePattern('text.status');
@@ -924,6 +959,31 @@ export function enrichResource(
924
959
  }
925
960
  }
926
961
  }
962
+
963
+ // Reorder fields so FHIR base Resource/DomainResource fields appear before domain-specific fields.
964
+ // FHIR element ordering: resourceType, id, meta, implicitRules, language, text, contained,
965
+ // extension, modifierExtension, then domain-specific fields.
966
+ // Firely (and HL7) validators enforce this ordering.
967
+ const fhirBaseFieldOrder = [
968
+ 'resourceType', 'id', 'meta', 'implicitRules', 'language',
969
+ 'text', 'contained', 'extension', 'modifierExtension'
970
+ ];
971
+ const reordered: Record<string, unknown> = {};
972
+ for (const key of fhirBaseFieldOrder) {
973
+ if (key in base) {
974
+ reordered[key] = base[key];
975
+ delete base[key];
976
+ }
977
+ }
978
+ // Copy remaining domain-specific fields in their original order
979
+ for (const key of Object.keys(base)) {
980
+ reordered[key] = base[key];
981
+ }
982
+ // Overwrite base in-place
983
+ for (const key of Object.keys(base)) {
984
+ delete base[key];
985
+ }
986
+ Object.assign(base, reordered);
927
987
  } catch { /* enrichment is best-effort */ }
928
988
  }
929
989
  `;
@@ -372,11 +372,16 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
372
372
  }
373
373
  // Build a map from snapshot for enriching differential elements with binding information
374
374
  const snapshotBindings = new Map();
375
+ // Build a map from snapshot for authoritative min cardinality (resolves inherited min correctly)
376
+ const snapshotMin = new Map();
375
377
  if (structureDefinition.differential && structureDefinition.snapshot) {
376
378
  for (const snapElem of structureDefinition.snapshot.element) {
377
379
  if (snapElem.binding) {
378
380
  snapshotBindings.set(snapElem.path, snapElem.binding);
379
381
  }
382
+ if (typeof snapElem.min === 'number') {
383
+ snapshotMin.set(snapElem.path, snapElem.min);
384
+ }
380
385
  }
381
386
  }
382
387
  const newFields = elements.map((element) => {
@@ -438,22 +443,29 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
438
443
  ? element.min
439
444
  : (typeof element.min === 'string' ? parseInt(element.min, 10) : undefined);
440
445
  let isOptional = element.min === 0;
441
- // If min is not specified in the differential, inherit from base field
442
- // This ensures that when a child profile overrides only type/binding (not cardinality),
443
- // we preserve the min cardinality from the parent profile
444
- if (structureDefinition.differential && baseFields.length > 0 && parsedMin === undefined) {
445
- // First try to find a base field with matching path and slice name
446
- const sliceName = element.sliceName;
447
- let baseField = sliceName
448
- ? baseFields.find(f => f.name === element.path && f.sliceName === sliceName)
449
- : baseFields.find(f => f.name === element.path);
450
- // If no exact match for slice, fall back to parent field without slice name
451
- if (!baseField) {
452
- baseField = baseFields.find(f => f.name === element.path && !f.sliceName);
453
- }
454
- if (baseField && typeof baseField.min === 'number') {
455
- parsedMin = baseField.min;
456
- isOptional = baseField.isOptional;
446
+ // If min is not specified in the differential, use the snapshot's authoritative min first,
447
+ // then fall back to base fields. The snapshot represents the fully merged cardinality
448
+ // and is the single source of truth for the current profile's constraints.
449
+ if (structureDefinition.differential && parsedMin === undefined) {
450
+ // Prefer snapshot min (authoritative merged value for this profile)
451
+ const snapMin = snapshotMin.get(element.path);
452
+ if (snapMin !== undefined) {
453
+ parsedMin = snapMin;
454
+ isOptional = snapMin === 0;
455
+ }
456
+ else if (baseFields.length > 0) {
457
+ // Fall back to base field inheritance when no snapshot is available
458
+ const sliceName = element.sliceName;
459
+ let baseField = sliceName
460
+ ? baseFields.find(f => f.name === element.path && f.sliceName === sliceName)
461
+ : baseFields.find(f => f.name === element.path);
462
+ if (!baseField) {
463
+ baseField = baseFields.find(f => f.name === element.path && !f.sliceName);
464
+ }
465
+ if (baseField && typeof baseField.min === 'number') {
466
+ parsedMin = baseField.min;
467
+ isOptional = baseField.isOptional;
468
+ }
457
469
  }
458
470
  }
459
471
  // Default to 0 if still undefined (base resource default)
@@ -795,6 +807,7 @@ async function aggregateRequiredSlices(fields, baseFields = []) {
795
807
  patternConstraint: childField.patternConstraint,
796
808
  binding: childField.binding,
797
809
  type: resolvedType, // Include type for placeholder generation
810
+ baseTypeCode: childField.baseTypeCode, // Raw FHIR type code for resource identification
798
811
  isRequired,
799
812
  isArray: resolvedIsArray, // Include array info for placeholder wrapping
800
813
  profileUrls: childField.profileUrls, // Profile URLs for discriminator matching
@@ -244,7 +244,7 @@ valueSets // Optional ValueSet map for runtime validation
244
244
  // Pattern constraint for ${humanRel}: at least one element must include coding with system="${system}" and code="${code}"
245
245
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
246
246
  const ${varName}Valid = resource.${rel}.some(item =>
247
- item?.coding?.some(coding => coding?.system === "${system}" && coding?.code === "${code}")
247
+ item?.coding?.some(coding => (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}")
248
248
  );
249
249
  if (!${varName}Valid) {
250
250
  errors.push("${humanRel} must include at least one element with a coding with system '${system}' and code '${code}'");
@@ -256,7 +256,7 @@ valueSets // Optional ValueSet map for runtime validation
256
256
  // Pattern constraint for ${humanRel}: at least one element must include coding with code="${code}"
257
257
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
258
258
  const ${varName}Valid = resource.${rel}.some(item =>
259
- item?.coding?.some(coding => coding?.code === "${code}")
259
+ item?.coding?.some(coding => (coding?.code as string) === "${code}")
260
260
  );
261
261
  if (!${varName}Valid) {
262
262
  errors.push("${humanRel} must include at least one element with a coding with code '${code}'");
@@ -272,7 +272,7 @@ valueSets // Optional ValueSet map for runtime validation
272
272
  // Pattern constraint for ${humanRel}: must include coding with system="${system}" and code="${code}"
273
273
  if (resource.${rel}) {
274
274
  const ${varName}Valid = resource.${rel}.coding?.some(coding =>
275
- coding?.system === "${system}" && coding?.code === "${code}"
275
+ (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}"
276
276
  );
277
277
  if (!${varName}Valid) {
278
278
  errors.push("${humanRel} must include a coding with system '${system}' and code '${code}'");
@@ -284,7 +284,7 @@ valueSets // Optional ValueSet map for runtime validation
284
284
  // Pattern constraint for ${humanRel}: must include coding with code="${code}"
285
285
  if (resource.${rel}) {
286
286
  const ${varName}Valid = resource.${rel}.coding?.some(coding =>
287
- coding?.code === "${code}"
287
+ (coding?.code as string) === "${code}")
288
288
  );
289
289
  if (!${varName}Valid) {
290
290
  errors.push("${humanRel} must include a coding with code '${code}'");
@@ -306,7 +306,7 @@ valueSets // Optional ValueSet map for runtime validation
306
306
  // Pattern constraint for ${humanRel}: at least one element must have system="${system}" and code="${code}"
307
307
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
308
308
  const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some(item =>
309
- item?.system === "${system}" && item?.code === "${code}"
309
+ (item?.system as string) === "${system}" && (item?.code as string) === "${code}"
310
310
  );
311
311
  if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
312
312
  errors.push("${humanRel} must include at least one element with system '${system}' and code '${code}'");
@@ -318,7 +318,7 @@ valueSets // Optional ValueSet map for runtime validation
318
318
  // Pattern constraint for ${humanRel}: at least one element must have code="${code}"
319
319
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
320
320
  const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some(item =>
321
- item?.code === "${code}"
321
+ (item?.code as string) === "${code}")
322
322
  );
323
323
  if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
324
324
  errors.push("${humanRel} must include at least one element with code '${code}'");
@@ -332,7 +332,7 @@ valueSets // Optional ValueSet map for runtime validation
332
332
  patternValidations.push(`
333
333
  // Pattern constraint for ${humanRel}: must have system="${system}" and code="${code}"
334
334
  if (resource.${rel}) {
335
- if (resource.${rel}.system !== "${system}" || resource.${rel}.code !== "${code}") {
335
+ if ((resource.${rel}.system as string) !== "${system}" || (resource.${rel}.code as string) !== "${code}") {
336
336
  errors.push("${humanRel} must have system '${system}' and code '${code}'");
337
337
  }
338
338
  }`);
@@ -341,7 +341,7 @@ valueSets // Optional ValueSet map for runtime validation
341
341
  patternValidations.push(`
342
342
  // Pattern constraint for ${humanRel}: must have code="${code}"
343
343
  if (resource.${rel}) {
344
- if (resource.${rel}.code !== "${code}") {
344
+ if ((resource.${rel}.code as string) !== "${code}") {
345
345
  errors.push("${humanRel} must have code '${code}'");
346
346
  }
347
347
  }`);
package/out/src/main.js CHANGED
@@ -113,8 +113,47 @@ function npmInstall(packagePath) {
113
113
  return new Promise((resolve, reject) => {
114
114
  const pm = detectPackageManager();
115
115
  const pmName = path.basename(pm.cmd).replace(/\.(cmd|exe)$/, '');
116
+ // Check if the package already exists in package.json to avoid dependency loops.
117
+ // When `bun add` targets a tgz that's already a dependency, bun detects a
118
+ // circular resolution. In that case, update the reference and run `install` instead.
119
+ const projectPkgPath = path.join(process.cwd(), 'package.json');
120
+ let useInstallOnly = false;
121
+ if (fs.existsSync(projectPkgPath)) {
122
+ try {
123
+ const tgzRelPath = './' + path.relative(process.cwd(), packagePath).replace(/\\/g, '/');
124
+ const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
125
+ const deps = projectPkg.dependencies || {};
126
+ // Find if any dependency already points to the same tgz (or same package name)
127
+ for (const [name, value] of Object.entries(deps)) {
128
+ if (typeof value === 'string' && (value === tgzRelPath || value === packagePath)) {
129
+ console.log(`Package ${name} already in dependencies, running ${pmName} install`);
130
+ useInstallOnly = true;
131
+ break;
132
+ }
133
+ }
134
+ // If not found, check by reading the tgz package name
135
+ if (!useInstallOnly) {
136
+ const tarPkgJsonPath = path.join(path.dirname(packagePath), '..', '.temp-install', 'generated', 'package.json');
137
+ // The tgz name follows the pattern: <sanitized-package-name>.tgz
138
+ const tgzBaseName = path.basename(packagePath, '.tgz').replace(/^-/, '');
139
+ for (const name of Object.keys(deps)) {
140
+ if (name.replace(/[@/]/g, '-') === tgzBaseName) {
141
+ // Update the dependency to point to the new tgz
142
+ deps[name] = tgzRelPath;
143
+ projectPkg.dependencies = deps;
144
+ fs.writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
145
+ console.log(`Updated ${name} dependency to ${tgzRelPath}`);
146
+ useInstallOnly = true;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ catch { /* ignore, fall through to normal add */ }
153
+ }
154
+ const args = useInstallOnly ? ['install'] : [...pm.args, packagePath];
116
155
  console.log(`Installing package with ${pmName}...`);
117
- const child = spawn(pm.cmd, [...pm.args, packagePath], {
156
+ const child = spawn(pm.cmd, args, {
118
157
  stdio: 'inherit',
119
158
  shell: true
120
159
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",