babelfhir-ts 1.0.16 → 1.0.18

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 (65) hide show
  1. package/README.md +152 -32
  2. package/bin/babelfhir-ts.js +2 -10
  3. package/out/generator/classGenerator.js +2 -2
  4. package/out/generator/index.js +152 -12
  5. package/out/generator/interfaceGenerator.js +3 -3
  6. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionAppointment.ts +94 -0
  7. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionAppointmentClass.ts +194 -0
  8. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionCheckIn.ts +29 -0
  9. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionCheckInClass.ts +194 -0
  10. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionCheckInNoCode.ts +29 -0
  11. package/out/generator/temp/1761563744855-kptdikzwryj/generated/AdmissionCheckInNoCodeClass.ts +194 -0
  12. package/out/generator/temp/1761563744855-kptdikzwryj/generated/RandomSupport.ts +26 -0
  13. package/out/generator/temp/1761563744855-kptdikzwryj/generated/valuesets/ValueSet-AdmissionFlowIds.ts +38 -0
  14. package/out/generator/temp/1761563744855-kptdikzwryj/generated/valuesets/ValueSet-AdmissionReasonCodeGroup.ts +42 -0
  15. package/out/generator/temp/1761563744855-kptdikzwryj/generated/valuesets/index.ts +18 -0
  16. package/out/generator/temp/1761563744855-kptdikzwryj/package/.index.db +0 -0
  17. package/out/generator/temp/1761563744855-kptdikzwryj/package/.index.json +234 -0
  18. package/out/generator/temp/1761563744855-kptdikzwryj/package/CodeSystem-admission-flow-ids.json +1 -0
  19. package/out/generator/temp/1761563744855-kptdikzwryj/package/CodeSystem-admission-reason-codes.json +1 -0
  20. package/out/generator/temp/1761563744855-kptdikzwryj/package/ImplementationGuide-pink.admission.json +1 -0
  21. package/out/generator/temp/1761563744855-kptdikzwryj/package/SearchParameter-SearchParameter-Appointment-AdmissionCheckIn.json +1 -0
  22. package/out/generator/temp/1761563744855-kptdikzwryj/package/SearchParameter-SearchParameter-Appointment-AdmissionCheckInNoCode.json +1 -0
  23. package/out/generator/temp/1761563744855-kptdikzwryj/package/SearchParameter-SearchParameter-Appointment-AdmissionUrl.json +1 -0
  24. package/out/generator/temp/1761563744855-kptdikzwryj/package/SearchParameter-SearchParameter-Appointment-AdmissionUuid.json +1 -0
  25. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionAppointment.json +1 -0
  26. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionCheckIn.json +1 -0
  27. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionCheckInNoCode.json +1 -0
  28. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionDataFlow.json +1 -0
  29. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionDevice.json +1 -0
  30. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionDocumentMetadata.json +1 -0
  31. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionFormData.json +1 -0
  32. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionFormUrl.json +1 -0
  33. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionFormUuid.json +1 -0
  34. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionIntegrationPoints.json +1 -0
  35. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionPatient.json +1 -0
  36. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionPatientData.json +1 -0
  37. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-AdmissionRoutingRule.json +1 -0
  38. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-LocationTomlConfiguration.json +1 -0
  39. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-Pink-Location-With-Toml.json +1 -0
  40. package/out/generator/temp/1761563744855-kptdikzwryj/package/StructureDefinition-PinkLocation.json +1 -0
  41. package/out/generator/temp/1761563744855-kptdikzwryj/package/ValueSet-AdmissionFlowIdsValueSet.json +1 -0
  42. package/out/generator/temp/1761563744855-kptdikzwryj/package/ValueSet-AdmissionReasonCodeGroup.json +1 -0
  43. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/AllergyIntolerance-Example-PenicillinAllergy.json +1 -0
  44. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/Appointment-Example-AdmissionAppointment.json +1 -0
  45. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/Condition-Example-DiabetesCondition.json +1 -0
  46. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/Device-Example-Surgery-Room.json +1 -0
  47. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/DocumentReference-Example-PreOperativeEvaluation.json +1 -0
  48. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/Location-Example-Location-With-Toml.json +1 -0
  49. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/Patient-Example-Patient.json +1 -0
  50. package/out/generator/temp/1761563744855-kptdikzwryj/package/example/QuestionnaireResponse-Example-AdmissionFormResponse.json +1 -0
  51. package/out/generator/temp/1761563744855-kptdikzwryj/package/other/spec.internals +313 -0
  52. package/out/generator/temp/1761563744855-kptdikzwryj/package/other/validation-oo.json +1 -0
  53. package/out/generator/temp/1761563744855-kptdikzwryj/package/other/validation-summary.json +1 -0
  54. package/out/generator/temp/1761563744855-kptdikzwryj/package/package.json +23 -0
  55. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionAppointment.sch +22 -0
  56. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionCheckIn.sch +18 -0
  57. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionCheckInNoCode.sch +18 -0
  58. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionDevice.sch +12 -0
  59. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionFormUrl.sch +18 -0
  60. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionFormUuid.sch +18 -0
  61. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-AdmissionPatient.sch +12 -0
  62. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-Pink-Location-With-Toml.sch +26 -0
  63. package/out/generator/temp/1761563744855-kptdikzwryj/package/xml/StructureDefinition-PinkLocation.sch +12 -0
  64. package/out/main.js +22 -6
  65. package/package.json +1 -2
package/README.md CHANGED
@@ -4,30 +4,23 @@
4
4
 
5
5
  ## Why BabelFHIR-TS?
6
6
 
7
- **The FHIR Challenge**: Implementation Guides define strict profiles (e.g., US Core, German basisprofil) that constrain base FHIR resources with must-support elements, custom extensions, value set bindings, and cardinality rules. Existing TypeScript libraries can't capture these requirements when you need profile-specific types.
7
+ **The FHIR Challenge**: Implementation Guides define strict profiles that constrain base FHIR resources with required or must-support elements, custom extensions, value set bindings, and cardinality rules. Existing TypeScript libraries can't capture these requirements when you need profile-specific types, leading to an overhead when using TypeScript to build apps that interact with FHIR servers.
8
8
 
9
- **The BabelFHIR-TS Solution**: Automatically generates TypeScript interfaces and validation logic directly from StructureDefinition JSON. Your IDE autocompletes must-support fields, flags missing extensions at compile-time, and validates FHIRPath invariants at runtime.
9
+ **The BabelFHIR-TS Solution**: Automatically generates TypeScript interfaces and validation logic directly from StructureDefinition JSON. Your IDE autocompletes required fields, flags missing extensions at compile-time, and validates FHIRPath invariants at runtime.
10
10
 
11
11
  ### What you get
12
12
 
13
- - **Strongly typed interfaces** that merge profile constraints with base FHIR types (built on `@types/fhir`)
13
+ - **Strongly typed interfaces** that merge profile constraints with base FHIR types (types come from `@types/fhir`)
14
+ - **Compiled output by default** — packages ship JavaScript (`.js`) plus TypeScript declarations (`.d.ts`)
14
15
  - **Runtime validation** using FHIRPath expressions from the profile—no external validator required for basic checks
15
16
  - **Type-safe extension handling** with proper slicing and nested extension support
16
17
  - **Random data builders** for testing and development (when class generation is enabled)
17
18
  - **Zero manual mapping**—consume any FHIR package or Implementation Guide directly from registries
19
+ - **Fast and lightweight**—minimal runtime deps; only `fhirpath` is required for validators
20
+ - **Install any FHIR profile as a node module**—use `babelfhir-ts install` to add Implementation Guides directly to your project
18
21
 
19
22
  Use it to build FHIR-compliant APIs, validate incoming resources against profiles, or generate type-safe client SDKs from Implementation Guides.
20
23
 
21
- ## Key features
22
-
23
- - 🎯 **Profile-first design** – generate interfaces that understand your IG's must-support elements, slices, and extensions
24
- - ⚙️ **CLI-first workflow** – install globally or run with `npx`; works against package archives, entire directories, or single `.json` profiles
25
- - ✅ **Built-in validation** – every generated class includes an async `validate()` function that executes profile constraints through FHIRPath *(high-level checks only; see details below)*
26
- - 🔌 **Extension-aware** – properly types extension slices, nested extensions, and sidecar `_field` elements for primitive extensions
27
- - 📦 **Package-friendly** – re-embeds generated code back into `.tgz` archives or expands into a target folder for direct consumption
28
- - 🌐 **Registry integration** – download and process packages directly from Simplifier, HL7, or custom FHIR registries
29
- - ♻️ **Caching + cleanup** – opt into `--no-cache` to remove temporary `.cache` directories after generation
30
-
31
24
  ## Installation
32
25
 
33
26
  Install globally (recommended when using the CLI frequently):
@@ -79,24 +72,59 @@ const patient = USCorePatientClass.random();
79
72
  const { errors, warnings } = await patient.validate();
80
73
  ```
81
74
 
75
+ ## Using the generated code in your project
76
+
77
+ Generated profile packages installed via `babelfhir-ts install` are published as **compiled JavaScript with TypeScript declarations**:
78
+
79
+ - JavaScript for runtime: `index.js`, `*.js`
80
+ - Type declarations for IDE/TS: `index.d.ts`, `*.d.ts`
81
+ - Dependencies:
82
+ - `@types/fhir` is included as a dependency of the generated package (no extra setup in your app)
83
+ - `fhirpath` is a peer dependency (required only if you use the generated validators/classes)
84
+
85
+ Install `fhirpath` in your app if you plan to call `.validate()` or use the generated classes:
86
+
87
+ ```bash
88
+ npm install fhirpath
89
+ ```
90
+
91
+ Import and use:
92
+
93
+ ```ts
94
+ // TypeScript or JavaScript
95
+ import { AdmissionAppointment, AdmissionAppointmentClass } from "pink.admission-generated";
96
+
97
+ // Types are available automatically via .d.ts
98
+ const appt: AdmissionAppointment = {
99
+ resourceType: "Appointment",
100
+ status: "booked",
101
+ start: "2025-10-23T14:00:00Z",
102
+ };
103
+
104
+ // Optional helper class + validation
105
+ const instance = AdmissionAppointmentClass.random();
106
+ const result = await new AdmissionAppointmentClass(appt).validate();
107
+ console.log(result.errors, result.warnings);
108
+ ```
109
+
82
110
  ## CLI reference
83
111
 
84
112
  ```
85
113
  babelfhir-ts [options] [<input> [output]]
86
114
  ```
87
115
 
88
- | Argument / option | Description |
89
- | ----------------- | ----------- |
90
- | `<input>` | Directory of FHIR packages/StructureDefinitions, single package (`.tgz`/`.zip`), or single StructureDefinition `.json`. Defaults to `./input` when omitted. |
91
- | `<output>` | Destination directory or archive. Defaults to `./output` when omitted. |
92
- | `install` | Downloads, processes, and installs a package as a project dependency. |
93
- | `--package <pkg@version>` | Fetch a package from a registry and process it without manual download. |
94
- | `--registry <url>` | Custom registry base URL (default: `https://packages.simplifier.net`). |
95
- | `--log <level>` | Control logging output: `none` (default), `console`, or `file`. |
96
- | `--no-cache` | Remove the `.cache` directory once generation completes. |
97
- | `--no-classes` | Skip emitting helper classes (interfaces & validators only). |
98
- | `-h, --help` | Print usage help. |
99
- | `-v, --version` | Print the BabelFHIR-TS version. |
116
+ | Argument / option | Description |
117
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `<input>` | Directory of FHIR packages/StructureDefinitions, single package (`.tgz`/`.zip`), or single StructureDefinition `.json`. Defaults to `./input` when omitted. |
119
+ | `<output>` | Destination directory or archive. Defaults to `./output` when omitted. |
120
+ | `install` | Downloads, processes, and installs a package as a project dependency. |
121
+ | `--package <pkg@version>` | Fetch a package from a registry and process it without manual download. |
122
+ | `--registry <url>` | Custom registry base URL (default:`https://packages.simplifier.net`). |
123
+ | `--log <level>` | Control logging output:`none` (default), `console`, or `file`. |
124
+ | `--no-cache` | Remove the `.cache` directory once generation completes. |
125
+ | `--no-classes` | Skip emitting helper classes (interfaces & validators only). |
126
+ | `-h, --help` | Print usage help. |
127
+ | `-v, --version` | Print the BabelFHIR-TS version. |
100
128
 
101
129
  ### Supported inputs
102
130
 
@@ -115,21 +143,113 @@ For each StructureDefinition the generator produces:
115
143
  - `validate()` that evaluates FHIR invariant expressions via `fhirpath`
116
144
  - optional validation parity artefacts when you run the test suite (`npm test validatorParity`)
117
145
 
118
- The validator relies on `fhirpath.evaluate(...)` to enforce profile invariants (including min cardinalities, slices, and custom expressions) without requiring a full FHIR server. It intentionally performs **high-level** checks only: terminology expansion, reference resolution, and other server-backed logic are out of scope, so keep a downstream validator (e.g., the HL7 Java validator CLI) in your QA pipeline for full conformance.
146
+ The validator relies on `fhirpath.evaluate(...)` to enforce profile invariants (including min cardinalities, slices, and custom expressions) without requiring a full FHIR server. It intentionally performs **high-level** checks only: terminology expansion, reference resolution, and other server-backed logic are out of scope, so keep a downstream validator (e.g. the HL7 Java validator CLI) in your QA pipeline for full conformance.
147
+
148
+ ### Example: Generated US Core Encounter Profile
149
+
150
+ Given the US Core Encounter profile, BabelFHIR-TS generates TypeScript interfaces that capture all must-support elements and profile extensions:
151
+
152
+ ```typescript
153
+ import { Encounter, Meta, Extension, Identifier, Coding, CodeableConcept,
154
+ Reference, EncounterParticipant, Period, EncounterHospitalization,
155
+ EncounterLocation } from "fhir/r4";
156
+
157
+ // Extension interface with type-safe URL
158
+ export interface UsCoreInterpreterNeeded extends Extension {
159
+ url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed'
160
+ }
161
+
162
+ // Profile interface extending base Encounter with must-support constraints
163
+ export interface USCoreEncounterProfile extends Encounter {
164
+ /** Must Support */
165
+ meta?: Meta;
166
+ /** Must Support */
167
+ identifier?: Identifier[];
168
+ /** Must Support */
169
+ class: Coding;
170
+ /** Must Support */
171
+ type: CodeableConcept[];
172
+ /** Must Support */
173
+ subject: Reference;
174
+ /** Must Support */
175
+ participant?: EncounterParticipant[];
176
+ /** Must Support */
177
+ period?: Period;
178
+ /** Must Support */
179
+ reasonCode?: CodeableConcept[];
180
+ /** Must Support */
181
+ reasonReference?: Reference[];
182
+ /** Must Support */
183
+ hospitalization?: EncounterHospitalization;
184
+ /** Must Support */
185
+ location?: EncounterLocation[];
186
+ /** Must Support */
187
+ serviceProvider?: Reference;
188
+
189
+ // Typed extension slicing
190
+ extension?: (Extension | UsCoreInterpreterNeeded)[];
191
+ }
192
+
193
+ // Generated validator function
194
+ export async function validateUSCoreEncounterProfile(
195
+ resource: USCoreEncounterProfile
196
+ ): Promise<{ errors: string[], warnings: string[] }>;
197
+
198
+ ```
119
199
 
120
200
  ## Scripts for contributors
121
201
 
122
- | Script | Purpose |
123
- | ------ | ------- |
124
- | `npm run generate` | Execute the CLI against the local `input/` folder and refresh `output/`. |
125
- | `npm run generate:check` | End-to-end check: generate, type-check, and lint the emitted output. |
126
- | `npm test` | Type-check and run all Vitest suites (coverage enabled). |
127
- | `npm run download-validator` | Fetch the official HL7 validation jar used for parity testing. |
202
+ | Script | Purpose |
203
+ | ------------------------------ | ---------------------------------------------------------------------------- |
204
+ | `npm run generate` | Execute the CLI against the local `input/` folder and refresh `output/`. |
205
+ | `npm run generate:check` | End-to-end check: generate, type-check, and lint the emitted output. |
206
+ | `npm test` | Type-check and run all Vitest suites (coverage enabled). |
207
+ | `npm run download-validator` | Fetch the official HL7 validation jar used for parity testing. |
128
208
 
129
209
  ## Caching notes
130
210
 
131
211
  The generator caches downloaded StructureDefinitions and packages inside `.cache/`. When you need a clean run, pass `--no-cache` or manually remove the folder. Temporary downloads land in `.temp-*` directories and are cleaned up automatically.
132
212
 
213
+ ## Limitations
214
+
215
+ BabelFHIR-TS is a code generation tool that parses FHIR StructureDefinitions and produces TypeScript interfaces and validators. While it handles many common FHIR profiling patterns, there are important limitations to be aware of:
216
+
217
+ ### Profile Mapping Accuracy
218
+
219
+ - **Not guaranteed for all IGs**: The generator uses heuristics to interpret StructureDefinition constraints, slicing rules, and extensions. Complex or unusual profiling patterns may not map correctly to TypeScript.
220
+ - **Test before production**: Always validate the generated code against your specific Implementation Guide's examples and test cases. We recommend running the official FHIR validator alongside BabelFHIR-TS in your QA pipeline.
221
+ - **Edge cases**: Rare profiling constructs (deeply nested slicing, conditional constraints, complex discriminators) may generate suboptimal or incomplete types.
222
+
223
+ ### Validation Scope
224
+
225
+ - **High-level checks only**: The generated `validate()` functions execute FHIRPath expressions from profile invariants but do **not** perform:
226
+ - Terminology expansion or ValueSet validation
227
+ - Reference resolution (checking that referenced resources exist)
228
+ - Full cardinality enforcement for complex slicing scenarios
229
+ - Server-side business logic or workflow rules
230
+ - **Use the official validator**: For production conformance testing, use the [HL7 FHIR Validator](https://github.com/hapifhir/org.hl7.fhir.core) alongside BabelFHIR-TS.
231
+
232
+ ### TypeScript Limitations
233
+
234
+ - **Runtime type checking is limited**: TypeScript types are erased at compile time. The generated interfaces provide compile-time safety but cannot enforce constraints at runtime without the validator functions.
235
+ - **Extension slicing**: While the generator creates typed extension interfaces, TypeScript cannot enforce that extension arrays contain exactly the required slices at compile time (this is validated at runtime).
236
+ - **Choice types**: FHIR's `[x]` choice types (e.g., `value[x]`) are represented as union types in TypeScript, which may require runtime type narrowing.
237
+
238
+ ### FHIR Version Support
239
+
240
+ - **R4 only**: The current version targets FHIR R4. Support for R5, DSTU2, or STU3 is not yet available.
241
+ - **Dependencies**: Generated code depends on `@types/fhir` (R4 definitions) and `fhirpath` (R4 compatible).
242
+
243
+ ### Reporting Issues
244
+
245
+ If you encounter an Implementation Guide that doesn't generate correctly, please [open an issue](https://github.com/quotentiroler/BabelFHIR-ts/issues) with:
246
+ - The package name and version
247
+ - The specific StructureDefinition URL
248
+ - Expected vs. actual generated output
249
+ - Any validation errors or type mismatches
250
+
251
+ We continuously improve the generator based on real-world IG usage, and your feedback helps make BabelFHIR-TS more robust.
252
+
133
253
  ## License
134
254
 
135
255
  ISC © Maximilian Nussbaumer
@@ -9,24 +9,16 @@ import fs from 'fs';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
 
12
- // Use tsx to run the compiled JavaScript (tsx handles ES module resolution better)
13
- const tsxBin = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
14
- const tsxPath = join(__dirname, '..', 'node_modules', '.bin', tsxBin);
12
+ // Use node directly to run the compiled JavaScript
15
13
  const mainJsPath = join(__dirname, '..', 'out', 'main.js');
16
14
 
17
- // Check if tsx exists
18
- if (!fs.existsSync(tsxPath)) {
19
- console.error('tsx not found. Please install dependencies with: npm install');
20
- process.exit(1);
21
- }
22
-
23
15
  // Check if the compiled file exists
24
16
  if (!fs.existsSync(mainJsPath)) {
25
17
  console.error('Compiled main.js not found. Please ensure the package was built correctly.');
26
18
  process.exit(1);
27
19
  }
28
20
 
29
- const child = spawn(tsxPath, [mainJsPath, ...process.argv.slice(2)], {
21
+ const child = spawn('node', [mainJsPath, ...process.argv.slice(2)], {
30
22
  stdio: 'inherit',
31
23
  cwd: process.cwd(), // Use the current working directory where the command was invoked
32
24
  shell: process.platform === 'win32'
@@ -427,9 +427,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
427
427
  }
428
428
  const helperImportList = Array.from(helperImports).join(', ');
429
429
  return `
430
- import { validate${interfaceName}, ${interfaceName} } from "./${interfaceName}";
430
+ import { validate${interfaceName}, ${interfaceName} } from "./${interfaceName}.js";
431
431
  // Only import helpers actually referenced below (dynamic based on required fields)
432
- import { ${helperImportList} } from "./RandomSupport";
432
+ import { ${helperImportList} } from "./RandomSupport.js";
433
433
 
434
434
  // Helper emitted only when meta.profile enforcement is applicable (resource profiles)
435
435
  ${enableMetaProfile ? `type WithMeta = { meta?: { profile?: string[] } };
@@ -1,15 +1,16 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { generateClass } from './classGenerator';
4
- import { generateInterfaces } from './interfaceGenerator';
5
- import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesFromDir, readValueSetsFromDir } from './packageParser';
6
- import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, buildFhirChildTypeMap } from './sdParser';
7
- import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase } from './utils';
8
- import { generateValidateProfileFunction } from './validatorGenerator';
9
- import { loadExamplesFromPackage, generateTestFile } from './testGenerator';
3
+ import { generateClass } from './classGenerator.js';
4
+ import { generateInterfaces } from './interfaceGenerator.js';
5
+ import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesFromDir, readValueSetsFromDir } from './packageParser.js';
6
+ import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, buildFhirChildTypeMap } from './sdParser.js';
7
+ import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase } from './utils.js';
8
+ import { generateValidateProfileFunction } from './validatorGenerator.js';
9
+ import { loadExamplesFromPackage, generateTestFile } from './testGenerator.js';
10
10
  import { generateValueSetTypeScript, generateValueSetRegistry } from './valueSetGenerator.js';
11
- import { logger } from '../logger';
12
- import fhirInterfaceNames from './fhirInterfaces.json';
11
+ import { logger } from '../logger.js';
12
+ import fhirInterfaceNames from './fhirInterfaces.json' with { type: 'json' };
13
+ import { spawn } from 'child_process';
13
14
  /** Ensure RandomSupport.ts exists in a generation output directory */
14
15
  function ensureRandomSupportFile(dir) {
15
16
  const filePath = path.join(dir, 'RandomSupport.ts');
@@ -54,6 +55,118 @@ export function skeletonQuantity(): Quantity { return { value: randomInt(1,100),
54
55
  fs.writeFileSync(filePath, content, 'utf-8');
55
56
  }
56
57
  }
58
+ /** Compile TypeScript files to JavaScript with declaration files */
59
+ async function compileTypeScriptToJS(dir) {
60
+ return new Promise((resolve, reject) => {
61
+ const tscCmd = process.platform === 'win32' ? 'tsc.cmd' : 'tsc';
62
+ // Create a temporary tsconfig.json for compilation
63
+ const tsconfig = {
64
+ compilerOptions: {
65
+ target: 'ES2020',
66
+ module: 'ES2020',
67
+ moduleResolution: 'node',
68
+ declaration: true,
69
+ outDir: dir,
70
+ rootDir: dir,
71
+ skipLibCheck: true,
72
+ esModuleInterop: true,
73
+ allowSyntheticDefaultImports: true,
74
+ strict: false,
75
+ resolveJsonModule: true,
76
+ types: ['node']
77
+ },
78
+ include: [`${dir}/**/*.ts`],
79
+ exclude: ['node_modules']
80
+ };
81
+ const tsconfigPath = path.join(dir, 'tsconfig.temp.json');
82
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
83
+ const child = spawn(tscCmd, ['-p', tsconfigPath], {
84
+ stdio: 'inherit',
85
+ shell: true
86
+ });
87
+ child.on('exit', (code) => {
88
+ // Clean up temp tsconfig
89
+ try {
90
+ fs.unlinkSync(tsconfigPath);
91
+ }
92
+ catch { /* ignore */ }
93
+ if (code === 0) {
94
+ // Remove .ts files after successful compilation
95
+ const files = fs.readdirSync(dir, { recursive: true, withFileTypes: true });
96
+ for (const file of files) {
97
+ if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts')) {
98
+ const fullPath = path.join(file.path || dir, file.name);
99
+ fs.unlinkSync(fullPath);
100
+ }
101
+ }
102
+ resolve();
103
+ }
104
+ else {
105
+ reject(new Error(`TypeScript compilation failed with exit code ${code}`));
106
+ }
107
+ });
108
+ child.on('error', (err) => {
109
+ try {
110
+ fs.unlinkSync(tsconfigPath);
111
+ }
112
+ catch { /* ignore */ }
113
+ reject(new Error(`Failed to run tsc: ${err.message}`));
114
+ });
115
+ });
116
+ }
117
+ /** Generate index.ts that exports all generated interfaces and classes */
118
+ async function generateIndexFile(dir) {
119
+ const files = fs.readdirSync(dir, { withFileTypes: true });
120
+ const exports = [];
121
+ const exportedNames = new Set();
122
+ // First pass: collect all files and their exports
123
+ const fileExports = new Map();
124
+ for (const file of files) {
125
+ if (file.isFile() && file.name.endsWith('.ts') && file.name !== 'index.ts' && file.name !== 'RandomSupport.ts') {
126
+ const filePath = path.join(dir, file.name);
127
+ const content = fs.readFileSync(filePath, 'utf-8');
128
+ const names = new Set();
129
+ // Extract export names (interfaces, classes, functions, types)
130
+ const exportMatches = content.matchAll(/export\s+(?:interface|class|type|const|function)\s+(\w+)/g);
131
+ for (const match of exportMatches) {
132
+ names.add(match[1]);
133
+ }
134
+ fileExports.set(file.name, names);
135
+ }
136
+ }
137
+ // Second pass: only export from files that have unique exports or are primary files
138
+ for (const file of files) {
139
+ if (file.isFile() && file.name.endsWith('.ts') && file.name !== 'index.ts' && file.name !== 'RandomSupport.ts') {
140
+ const baseName = file.name.replace('.ts', '');
141
+ const fileNames = fileExports.get(file.name) || new Set();
142
+ // Check if this file's exports would conflict with already exported names
143
+ let hasConflict = false;
144
+ for (const name of fileNames) {
145
+ if (exportedNames.has(name)) {
146
+ hasConflict = true;
147
+ break;
148
+ }
149
+ }
150
+ // Only export if no conflicts, or if it's a Class file (which are primary)
151
+ if (!hasConflict || file.name.endsWith('Class.ts')) {
152
+ exports.push(`export * from './${baseName}.js';`);
153
+ // Mark all names from this file as exported
154
+ for (const name of fileNames) {
155
+ exportedNames.add(name);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ // Also export from valuesets subfolder if it exists
161
+ const valueSetsDir = path.join(dir, 'valuesets');
162
+ if (fs.existsSync(valueSetsDir)) {
163
+ exports.push(`export * from './valuesets/index.js';`);
164
+ }
165
+ // Sort exports alphabetically for consistency
166
+ exports.sort();
167
+ const indexContent = `// Auto-generated index file - exports all generated FHIR profiles\n${exports.join('\n')}\n`;
168
+ fs.writeFileSync(path.join(dir, 'index.ts'), indexContent, 'utf-8');
169
+ }
57
170
  // Reusable processor for a single StructureDefinition
58
171
  async function processStructureDefinition(sd, ctx) {
59
172
  const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, flags, fhirChildTypeMap } = ctx;
@@ -332,15 +445,42 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
332
445
  version: packageVersion,
333
446
  description: `Generated TypeScript interfaces for ${packageName}`,
334
447
  type: 'module',
335
- main: 'index.ts',
336
- types: 'index.ts',
448
+ main: 'index.js',
449
+ types: 'index.d.ts',
337
450
  scripts: {},
338
451
  keywords: ['fhir', 'typescript', 'generated'],
339
- license: 'MIT'
452
+ license: 'MIT',
453
+ // fhirpath is required at runtime by validators; keep as peer so host decides version
454
+ peerDependencies: {
455
+ 'fhirpath': '^4.0.0'
456
+ },
457
+ // Type definitions for FHIR are needed for the .d.ts files
458
+ dependencies: {
459
+ '@types/fhir': '^0.0.41'
460
+ }
340
461
  };
341
462
  const generatedPackageJsonPath = path.join(outputDir, 'package.json');
342
463
  fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
343
464
  console.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
465
+ // Create a tsconfig.json for better IDE support with bundler moduleResolution
466
+ const generatedTsConfig = {
467
+ compilerOptions: {
468
+ moduleResolution: 'bundler',
469
+ module: 'ES2020',
470
+ target: 'ES2020',
471
+ strict: false,
472
+ skipLibCheck: true,
473
+ esModuleInterop: true
474
+ }
475
+ };
476
+ const generatedTsConfigPath = path.join(outputDir, 'tsconfig.json');
477
+ fs.writeFileSync(generatedTsConfigPath, JSON.stringify(generatedTsConfig, null, 2));
478
+ // Generate index.ts that exports all interfaces
479
+ console.log('Generating index.ts exports...');
480
+ await generateIndexFile(outputDir);
481
+ // Compile TypeScript to JavaScript
482
+ console.log('Compiling TypeScript to JavaScript...');
483
+ await compileTypeScriptToJS(outputDir);
344
484
  const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
345
485
  await createPackageFromDir(extractedRoot, finalArchive);
346
486
  return finalArchive;
@@ -1,7 +1,7 @@
1
- import fhirInterfaceNames from "./fhirInterfaces.json";
2
- import { capitalize, sanitizeIdentifier, toPascalCase } from './utils';
1
+ import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
2
+ import { capitalize, sanitizeIdentifier, toPascalCase } from './utils.js';
3
3
  import { generateCodeUnionType, getUniformSystem } from './vsParser.js';
4
- import { logger } from '../logger';
4
+ import { logger } from '../logger.js';
5
5
  export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap) {
6
6
  const debug = (...args) => { if (process.env.DEBUG_FHIR_GEN === 'true')
7
7
  console.log('[gen:interfaces]', ...args); };
@@ -0,0 +1,94 @@
1
+ import { Appointment, Extension } from "fhir/r4";
2
+
3
+ export interface AdmissionCheckInNoCode extends Extension { url: 'http://fhir.pink-protocol.io/StructureDefinition/AdmissionCheckInNoCode' }
4
+
5
+ export interface AdmissionCheckIn extends Extension { url: 'http://fhir.pink-protocol.io/StructureDefinition/AdmissionCheckIn' }
6
+
7
+ export interface AdmissionFormUrl extends Extension { url: 'http://fhir.pink-protocol.io/StructureDefinition/AdmissionFormUrl' }
8
+
9
+ export interface AdmissionFormUuid extends Extension { url: 'http://fhir.pink-protocol.io/StructureDefinition/AdmissionFormUuid' }
10
+
11
+ export interface AdmissionAppointment extends Appointment {
12
+ start: string;
13
+
14
+ extension?: (Extension | AdmissionFormUuid | AdmissionFormUrl | AdmissionCheckIn | AdmissionCheckInNoCode)[];
15
+ }
16
+
17
+
18
+ import fhirpath from "fhirpath";
19
+
20
+ export async function validateAdmissionAppointment(resource: AdmissionAppointment): Promise<{ errors: string[], warnings: string[] }> {
21
+ const errors: string[] = [];
22
+ const warnings: string[] = [];
23
+
24
+ const result0 = await Promise.resolve(fhirpath.evaluate(resource, "start.exists() = end.exists()", { resource }));
25
+ if (!result0.every(Boolean)) {
26
+ errors.push("Constraint violation: Either start and end are specified, or neither");
27
+ }
28
+
29
+ const result1 = await Promise.resolve(fhirpath.evaluate(resource, "(start.exists() and end.exists()) or (status in ('proposed' | 'cancelled' | 'waitlist'))", { resource }));
30
+ if (!result1.every(Boolean)) {
31
+ errors.push("Constraint violation: Only proposed or cancelled appointments can be missing start/end dates");
32
+ }
33
+
34
+ const result2 = await Promise.resolve(fhirpath.evaluate(resource, "Appointment.cancelationReason.exists() implies (Appointment.status='no-show' or Appointment.status='cancelled')", { resource }));
35
+ if (!result2.every(Boolean)) {
36
+ errors.push("Constraint violation: Cancelation reason is only used for appointments that have been cancelled, or no-show");
37
+ }
38
+
39
+ const result3 = await Promise.resolve(fhirpath.evaluate(resource, "contained.contained.empty()", { resource }));
40
+ if (!result3.every(Boolean)) {
41
+ errors.push("Constraint violation: If the resource is contained in another resource, it SHALL NOT contain nested Resources");
42
+ }
43
+
44
+ const result4 = await Promise.resolve(fhirpath.evaluate(resource, "contained.where((('#'+id in (%resource.descendants().reference | %resource.descendants().as(canonical) | %resource.descendants().as(uri) | %resource.descendants().as(url))) or descendants().where(reference = '#').exists() or descendants().where(as(canonical) = '#').exists() or descendants().where(as(canonical) = '#').exists()).not()).trace('unmatched', id).empty()", { resource }));
45
+ if (!result4.every(Boolean)) {
46
+ errors.push("Constraint violation: If the resource is contained in another resource, it SHALL be referred to from elsewhere in the resource or SHALL refer to the containing resource");
47
+ }
48
+
49
+ const result5 = await Promise.resolve(fhirpath.evaluate(resource, "contained.meta.versionId.empty() and contained.meta.lastUpdated.empty()", { resource }));
50
+ if (!result5.every(Boolean)) {
51
+ errors.push("Constraint violation: If a resource is contained in another resource, it SHALL NOT have a meta.versionId or a meta.lastUpdated");
52
+ }
53
+
54
+ const result6 = await Promise.resolve(fhirpath.evaluate(resource, "contained.meta.security.empty()", { resource }));
55
+ if (!result6.every(Boolean)) {
56
+ errors.push("Constraint violation: If a resource is contained in another resource, it SHALL NOT have a security label");
57
+ }
58
+
59
+ const result7 = await Promise.resolve(fhirpath.evaluate(resource, "text.`div`.exists()", { resource }));
60
+ if (!result7.every(Boolean)) {
61
+ warnings.push("Constraint violation: A resource should have narrative for robust management");
62
+ }
63
+
64
+ const result8 = await Promise.resolve(fhirpath.evaluate(resource, "hasValue() or (children().count() > id.count())", { resource }));
65
+ if (!result8.every(Boolean)) {
66
+ errors.push("Constraint violation: All FHIR elements must have a @value or children");
67
+ }
68
+
69
+ const result9 = await Promise.resolve(fhirpath.evaluate(resource, "type.exists() or actor.exists()", { resource }));
70
+ if (!result9.every(Boolean)) {
71
+ errors.push("Constraint violation: Either the type or actor on the participant SHALL be specified");
72
+ }
73
+
74
+ const result10 = await Promise.resolve(fhirpath.evaluate(resource, "status.exists()", { resource }));
75
+ if (!result10.every(Boolean)) {
76
+ errors.push("Constraint violation: status must be present");
77
+ }
78
+
79
+ const result11 = await Promise.resolve(fhirpath.evaluate(resource, "start.exists()", { resource }));
80
+ if (!result11.every(Boolean)) {
81
+ errors.push("Constraint violation: start must be present");
82
+ }
83
+
84
+ const result12 = await Promise.resolve(fhirpath.evaluate(resource, "participant.exists()", { resource }));
85
+ if (!result12.every(Boolean)) {
86
+ errors.push("Constraint violation: participant must be present");
87
+ }
88
+
89
+ const result13 = await Promise.resolve(fhirpath.evaluate(resource, "participant.status.exists()", { resource }));
90
+ if (!result13.every(Boolean)) {
91
+ errors.push("Constraint violation: participant.status must be present");
92
+ }
93
+ return { errors, warnings };
94
+ }