babelfhir-ts 1.3.2 → 1.3.4
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 +4 -3
- package/out/src/generator/emitters/class/randomUtilitiesTemplate.js +26 -2
- package/out/src/generator/emitters/zod/zodStubInstaller.js +2 -2
- package/out/src/generator/fhir/corePackageResolver.js +56 -54
- package/out/src/generator/fhir/corePackageResolver.ts +51 -51
- package/out/src/generator/index.js +49 -5
- package/out/src/generator/parser/packageManager.js +29 -2
- package/out/src/generator/parser/packageParser.js +71 -8
- package/out/src/generator/parser/sdFetcher.js +6 -7
- package/out/src/generator/parser/sdParser.js +9 -0
- package/out/src/generator/sdProcessorHelpers.js +23 -5
- package/out/src/generator/timing.js +43 -0
- package/out/src/main.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
- **Random data builders** for testing and development (when class generation is enabled)
|
|
48
48
|
- **Zero manual mapping**—consume any FHIR package or Implementation Guide directly from registries
|
|
49
49
|
- **Fast and lightweight**—minimal runtime deps; only `fhirpath` is required for validators
|
|
50
|
-
- **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) with profile-specific methods (e.g., `.usCorePatient()`, `.pASClaim()`) on top of base
|
|
50
|
+
- **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [`client-r4b`](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [`client-r5`](https://www.npmjs.com/package/@babelfhir-ts/client-r5) with profile-specific methods (e.g., `.usCorePatient()`, `.pASClaim()`) on top of base resource accessors
|
|
51
51
|
- **Install any FHIR profile as a node module**—use `babelfhir-ts install` to add Implementation Guides directly to your project
|
|
52
52
|
|
|
53
53
|
<!-- PARITY-BADGES:START - Do not remove or modify this section -->
|
|
@@ -260,7 +260,7 @@ Full documentation is available at **[max-health-inc.github.io/BabelFHIR-TS/docs
|
|
|
260
260
|
- [Generated Code Guide](https://max-health-inc.github.io/BabelFHIR-TS/docs/generated-code) — understanding the output, module resolution, FHIR type imports
|
|
261
261
|
- [FHIR Client](https://max-health-inc.github.io/BabelFHIR-TS/docs/fhir-client) — type-safe server interactions
|
|
262
262
|
- [Validation](https://max-health-inc.github.io/BabelFHIR-TS/docs/validation) — what validators check, CI pipelines, known Firely SDK issues
|
|
263
|
-
- [Limitations](https://max-health-inc.github.io/BabelFHIR-TS/docs/limitations) —
|
|
263
|
+
- [Limitations](https://max-health-inc.github.io/BabelFHIR-TS/docs/limitations) — edge cases, random() caveats
|
|
264
264
|
- [Contributing](https://max-health-inc.github.io/BabelFHIR-TS/docs/contributing) — dev setup, scripts, project structure
|
|
265
265
|
|
|
266
266
|
## License
|
|
@@ -283,7 +283,8 @@ For security issues, please see [SECURITY.md](SECURITY.md) for our security poli
|
|
|
283
283
|
## Links
|
|
284
284
|
|
|
285
285
|
- [npm package — babelfhir-ts](https://www.npmjs.com/package/babelfhir-ts)
|
|
286
|
-
- [npm package — @babelfhir-ts/client-r4](https://www.npmjs.com/package/@babelfhir-ts/client-r4)
|
|
286
|
+
- [npm package — @babelfhir-ts/client-r4](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [client-r4b](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [client-r5](https://www.npmjs.com/package/@babelfhir-ts/client-r5)
|
|
287
|
+
- [npm package — @babelfhir-ts/zod](https://www.npmjs.com/package/@babelfhir-ts/zod)
|
|
287
288
|
- [GitHub repository](https://github.com/Max-Health-Inc/BabelFHIR-TS)
|
|
288
289
|
- [Validation report](https://max-health-inc.github.io/BabelFHIR-TS/)
|
|
289
290
|
- [Issue tracker](https://github.com/Max-Health-Inc/BabelFHIR-TS/issues)
|
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
|
|
2
|
-
import { fhirModuleSpecifier } from '../../fhir/versionContext.js';
|
|
2
|
+
import { ctx, fhirModuleSpecifier } from '../../fhir/versionContext.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the SampledData skeleton object literal from the active version's
|
|
5
|
+
* dataTypes map. R4 has `period`, R5 replaced it with `interval` +
|
|
6
|
+
* `intervalUnit` — but instead of hard-coding the difference we simply
|
|
7
|
+
* inspect which fields exist for the current FHIR version.
|
|
8
|
+
*/
|
|
9
|
+
function buildSampledDataFields() {
|
|
10
|
+
const sdFields = ctx().dataTypes['SampledData'] ?? {};
|
|
11
|
+
const parts = [];
|
|
12
|
+
// origin is always a Quantity — use a complete skeleton
|
|
13
|
+
parts.push("origin: { value: 0, unit: 'unit', system: '" + TS.UCUM + "', code: '1' }");
|
|
14
|
+
// Version-varying numeric / string fields
|
|
15
|
+
if ('period' in sdFields)
|
|
16
|
+
parts.push('period: 1');
|
|
17
|
+
if ('interval' in sdFields)
|
|
18
|
+
parts.push('interval: 1');
|
|
19
|
+
if ('intervalUnit' in sdFields)
|
|
20
|
+
parts.push("intervalUnit: 'ms'");
|
|
21
|
+
// Always-present fields
|
|
22
|
+
parts.push('dimensions: 1');
|
|
23
|
+
parts.push("data: 'E'");
|
|
24
|
+
return parts.join(', ');
|
|
25
|
+
}
|
|
3
26
|
export function generateRandomUtilities() {
|
|
4
27
|
const fhirModule = fhirModuleSpecifier();
|
|
28
|
+
const sampledDataFields = buildSampledDataFields();
|
|
5
29
|
return `// Auto-generated helper for random resource generation with optional deterministic seeding
|
|
6
30
|
// Uses official FHIR TypeScript definitions
|
|
7
31
|
import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity, Money, Narrative, Dosage, Ratio, Range, Annotation, Attachment, Age, Duration, Expression, RelatedArtifact, DataRequirement, ParameterDefinition, TriggerDefinition, ContactDetail, UsageContext, Signature, SampledData, Timing } from '${fhirModule}';
|
|
@@ -96,7 +120,7 @@ export function skeletonTriggerDefinition(): TriggerDefinition { return { type:
|
|
|
96
120
|
export function skeletonContactDetail(): ContactDetail { return { name: 'Contact' }; }
|
|
97
121
|
export function skeletonUsageContext(): UsageContext { return { code: { system: 'http://terminology.hl7.org/CodeSystem/usage-context-type', code: 'focus' }, valueCodeableConcept: skeletonCodeableConcept() }; }
|
|
98
122
|
export function skeletonSignature(): Signature { return { type: [{ system: 'urn:iso-astm:E1762-95:2013', code: '1.2.840.10065.1.12.1.1' }], when: randomDate(), who: skeletonReference('Practitioner') }; }
|
|
99
|
-
export function skeletonSampledData(): SampledData { return {
|
|
123
|
+
export function skeletonSampledData(): SampledData { return { ${sampledDataFields} }; }
|
|
100
124
|
export function skeletonTiming(): Timing { return { repeat: { frequency: 1, period: 1, periodUnit: 'd' } }; }
|
|
101
125
|
|
|
102
126
|
/**
|
|
@@ -39,8 +39,8 @@ export function installBaseZodTypes(outputDir) {
|
|
|
39
39
|
if (fs.existsSync(srcPkgJson)) {
|
|
40
40
|
fs.copyFileSync(srcPkgJson, path.join(pkgDir, 'package.json'));
|
|
41
41
|
}
|
|
42
|
-
// Copy all runtime dist files into
|
|
43
|
-
const distSlugDir = path.join(pkgDir, slug);
|
|
42
|
+
// Copy all runtime dist files into dist/<slug>/ to match package.json exports
|
|
43
|
+
const distSlugDir = path.join(pkgDir, 'dist', slug);
|
|
44
44
|
fs.mkdirSync(distSlugDir, { recursive: true });
|
|
45
45
|
for (const file of fs.readdirSync(runtimeDist)) {
|
|
46
46
|
fs.copyFileSync(path.join(runtimeDist, file), path.join(distSlugDir, file));
|
|
@@ -121,70 +121,72 @@ export function deriveVersionData(corePackageDir) {
|
|
|
121
121
|
const resourceFields = {};
|
|
122
122
|
const files = fs.readdirSync(sdDir)
|
|
123
123
|
.filter(f => f.startsWith('StructureDefinition-') && f.endsWith('.json'));
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// Memory-efficient approach: parse SDs one file at a time.
|
|
125
|
+
// Pass 1 processes base specialisations and records which constraint SDs
|
|
126
|
+
// need full processing in Pass 2 (only complex-type constraints whose
|
|
127
|
+
// base type is a known specialisation — typically <10 out of ~828).
|
|
128
|
+
const complexTypeSpecNames = new Set();
|
|
129
|
+
const constraintSDFiles = []; // filenames needing Pass 2
|
|
126
130
|
for (const file of files) {
|
|
127
131
|
try {
|
|
128
132
|
const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
|
|
129
133
|
const sd = JSON.parse(raw);
|
|
130
134
|
if (sd.resourceType !== 'StructureDefinition')
|
|
131
135
|
continue;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
const kind = sd.kind;
|
|
137
|
+
const derivation = sd.derivation;
|
|
138
|
+
const typeName = sd.type;
|
|
139
|
+
// Constraint SDs are deferred to Pass 2
|
|
140
|
+
if (derivation === 'constraint' && kind === 'complex-type') {
|
|
141
|
+
constraintSDFiles.push(file);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!typeName)
|
|
145
|
+
continue;
|
|
146
|
+
if (derivation && derivation !== 'specialization')
|
|
147
|
+
continue;
|
|
148
|
+
if (kind === 'logical')
|
|
149
|
+
continue;
|
|
150
|
+
if (kind === 'resource') {
|
|
151
|
+
resourceNames.push(typeName);
|
|
152
|
+
interfaceNames.push(typeName);
|
|
153
|
+
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
154
|
+
collectDataTypeFields(sd, typeName, resourceFields);
|
|
155
|
+
}
|
|
156
|
+
else if (kind === 'primitive-type') {
|
|
157
|
+
primitiveTypeNames.push(typeName);
|
|
158
|
+
}
|
|
159
|
+
else if (kind === 'complex-type') {
|
|
160
|
+
complexTypeSpecNames.add(typeName);
|
|
161
|
+
interfaceNames.push(typeName);
|
|
162
|
+
collectDataTypeFields(sd, typeName, dataTypes);
|
|
163
|
+
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
164
|
+
}
|
|
165
|
+
// sd is now GC-eligible — no reference kept
|
|
136
166
|
}
|
|
137
167
|
catch { /* skip */ }
|
|
138
168
|
}
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
// Pass 1: base specialisation SDs (derivation = 'specialization' or absent)
|
|
142
|
-
for (const { kind, derivation, typeName, sd } of parsedSDs) {
|
|
143
|
-
if (!typeName)
|
|
144
|
-
continue;
|
|
145
|
-
// Abstract bases (Element, Resource) have no derivation field
|
|
146
|
-
if (derivation && derivation !== 'specialization')
|
|
147
|
-
continue;
|
|
148
|
-
// Skip logical types (Definition, Event, FiveWs, Request)
|
|
149
|
-
if (kind === 'logical')
|
|
150
|
-
continue;
|
|
151
|
-
if (kind === 'resource') {
|
|
152
|
-
resourceNames.push(typeName);
|
|
153
|
-
// Resources are also valid interface names (used by isFhirType)
|
|
154
|
-
interfaceNames.push(typeName);
|
|
155
|
-
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
156
|
-
// Collect resource direct-child fields for cardinality tables
|
|
157
|
-
collectDataTypeFields(sd, typeName, resourceFields);
|
|
158
|
-
}
|
|
159
|
-
else if (kind === 'primitive-type') {
|
|
160
|
-
primitiveTypeNames.push(typeName);
|
|
161
|
-
}
|
|
162
|
-
else if (kind === 'complex-type') {
|
|
163
|
-
complexTypeSpecNames.add(typeName);
|
|
164
|
-
interfaceNames.push(typeName);
|
|
165
|
-
collectDataTypeFields(sd, typeName, dataTypes);
|
|
166
|
-
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Pass 2: core profiles (constraint) on data types shipped in the core package
|
|
170
|
-
// Examples: MoneyQuantity, SimpleQuantity — both are constraint on Quantity
|
|
171
|
-
// Only include SDs whose base type is a known complex-type specialisation,
|
|
172
|
-
// excluding Extension (hundreds of extension constraint SDs) and
|
|
173
|
-
// infrastructure types that aren't used as field type codes.
|
|
169
|
+
// Pass 2: re-read only the constraint complex-type SDs whose base type is a
|
|
170
|
+
// known specialisation (e.g. MoneyQuantity constraining Quantity).
|
|
174
171
|
const CONSTRAINT_EXCLUDE_TYPES = new Set(['Extension', 'ElementDefinition']);
|
|
175
|
-
for (const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
172
|
+
for (const file of constraintSDFiles) {
|
|
173
|
+
try {
|
|
174
|
+
const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
|
|
175
|
+
const sd = JSON.parse(raw);
|
|
176
|
+
const typeName = sd.type;
|
|
177
|
+
const sdName = sd.name || sd.id;
|
|
178
|
+
if (!typeName || !sdName)
|
|
179
|
+
continue;
|
|
180
|
+
if (sdName === typeName)
|
|
181
|
+
continue;
|
|
182
|
+
if (!complexTypeSpecNames.has(typeName))
|
|
183
|
+
continue;
|
|
184
|
+
if (CONSTRAINT_EXCLUDE_TYPES.has(typeName))
|
|
185
|
+
continue;
|
|
186
|
+
interfaceNames.push(sdName);
|
|
187
|
+
collectDataTypeFields(sd, sdName, dataTypes);
|
|
188
|
+
}
|
|
189
|
+
catch { /* skip */ }
|
|
188
190
|
}
|
|
189
191
|
// Deterministic ordering
|
|
190
192
|
resourceNames.sort();
|
|
@@ -163,67 +163,67 @@ export function deriveVersionData(corePackageDir: string): DerivedVersionData {
|
|
|
163
163
|
const files = fs.readdirSync(sdDir)
|
|
164
164
|
.filter(f => f.startsWith('StructureDefinition-') && f.endsWith('.json'));
|
|
165
165
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
// Memory-efficient approach: parse SDs one file at a time.
|
|
167
|
+
// Pass 1 processes base specialisations and records which constraint SDs
|
|
168
|
+
// need full processing in Pass 2 (only complex-type constraints whose
|
|
169
|
+
// base type is a known specialisation — typically <10 out of ~828).
|
|
170
|
+
const complexTypeSpecNames = new Set<string>();
|
|
171
|
+
const constraintSDFiles: string[] = []; // filenames needing Pass 2
|
|
171
172
|
|
|
172
173
|
for (const file of files) {
|
|
173
174
|
try {
|
|
174
175
|
const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
|
|
175
176
|
const sd = JSON.parse(raw);
|
|
176
177
|
if (sd.resourceType !== 'StructureDefinition') continue;
|
|
177
|
-
parsedSDs.push({
|
|
178
|
-
kind: sd.kind, derivation: sd.derivation,
|
|
179
|
-
typeName: sd.type, sdName: sd.name || sd.id, sd,
|
|
180
|
-
});
|
|
181
|
-
} catch { /* skip */ }
|
|
182
|
-
}
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
179
|
+
const kind = sd.kind;
|
|
180
|
+
const derivation = sd.derivation;
|
|
181
|
+
const typeName = sd.type;
|
|
182
|
+
|
|
183
|
+
// Constraint SDs are deferred to Pass 2
|
|
184
|
+
if (derivation === 'constraint' && kind === 'complex-type') {
|
|
185
|
+
constraintSDFiles.push(file);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!typeName) continue;
|
|
190
|
+
if (derivation && derivation !== 'specialization') continue;
|
|
191
|
+
if (kind === 'logical') continue;
|
|
192
|
+
|
|
193
|
+
if (kind === 'resource') {
|
|
194
|
+
resourceNames.push(typeName);
|
|
195
|
+
interfaceNames.push(typeName);
|
|
196
|
+
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
197
|
+
collectDataTypeFields(sd, typeName, resourceFields);
|
|
198
|
+
} else if (kind === 'primitive-type') {
|
|
199
|
+
primitiveTypeNames.push(typeName);
|
|
200
|
+
} else if (kind === 'complex-type') {
|
|
201
|
+
complexTypeSpecNames.add(typeName);
|
|
202
|
+
interfaceNames.push(typeName);
|
|
203
|
+
collectDataTypeFields(sd, typeName, dataTypes);
|
|
204
|
+
collectBackboneInterfaces(sd, typeName, interfaceNames);
|
|
205
|
+
}
|
|
206
|
+
// sd is now GC-eligible — no reference kept
|
|
207
|
+
} catch { /* skip */ }
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
// Pass 2:
|
|
213
|
-
//
|
|
214
|
-
// Only include SDs whose base type is a known complex-type specialisation,
|
|
215
|
-
// excluding Extension (hundreds of extension constraint SDs) and
|
|
216
|
-
// infrastructure types that aren't used as field type codes.
|
|
210
|
+
// Pass 2: re-read only the constraint complex-type SDs whose base type is a
|
|
211
|
+
// known specialisation (e.g. MoneyQuantity constraining Quantity).
|
|
217
212
|
const CONSTRAINT_EXCLUDE_TYPES = new Set(['Extension', 'ElementDefinition']);
|
|
218
|
-
for (const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
213
|
+
for (const file of constraintSDFiles) {
|
|
214
|
+
try {
|
|
215
|
+
const raw = fs.readFileSync(path.join(sdDir, file), 'utf-8');
|
|
216
|
+
const sd = JSON.parse(raw);
|
|
217
|
+
const typeName = sd.type;
|
|
218
|
+
const sdName = sd.name || sd.id;
|
|
219
|
+
if (!typeName || !sdName) continue;
|
|
220
|
+
if (sdName === typeName) continue;
|
|
221
|
+
if (!complexTypeSpecNames.has(typeName)) continue;
|
|
222
|
+
if (CONSTRAINT_EXCLUDE_TYPES.has(typeName)) continue;
|
|
223
|
+
|
|
224
|
+
interfaceNames.push(sdName);
|
|
225
|
+
collectDataTypeFields(sd, sdName, dataTypes);
|
|
226
|
+
} catch { /* skip */ }
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Deterministic ordering
|
|
@@ -11,6 +11,8 @@ import { initVersionContext, ctx, versionSlug } from './fhir/versionContext.js';
|
|
|
11
11
|
import { DEFAULT_FHIR_VERSION } from './fhir/types.js';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
13
|
import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, cleanupStaleValueSetDir, initGenerationContext, enrichValueSetsFromCodeMap, } from './generationHelpers.js';
|
|
14
|
+
import { resetTimings, startPhase, getTimingSummary, formatTimingSummary } from './timing.js';
|
|
15
|
+
export { getTimingSummary, formatTimingSummary } from './timing.js';
|
|
14
16
|
const log = logger.withTag('generator');
|
|
15
17
|
// Convert version-context dataTypes to the Map format expected by code
|
|
16
18
|
function buildFhirChildTypeMapFromJson() {
|
|
@@ -284,13 +286,17 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
284
286
|
*/
|
|
285
287
|
export async function generateIntoPackage(packageArchivePath, outArchivePath, flags) {
|
|
286
288
|
initGenerationContext(flags, path.dirname(outArchivePath || packageArchivePath));
|
|
289
|
+
resetTimings();
|
|
287
290
|
if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
|
|
288
291
|
throw new Error('Only .tgz or .zip supported.');
|
|
292
|
+
let endPhase = startPhase('extract');
|
|
289
293
|
const extractResult = await extractPackage(packageArchivePath);
|
|
290
294
|
const extractedRoot = extractResult.path;
|
|
295
|
+
endPhase();
|
|
291
296
|
const shouldCleanup = !extractResult.isCached; // Only cleanup temp directories, not cache
|
|
292
297
|
try {
|
|
293
298
|
// Auto-detect FHIR version from package when not explicitly set
|
|
299
|
+
endPhase = startPhase('init');
|
|
294
300
|
if (!flags?.fhirVersion) {
|
|
295
301
|
const detected = detectFhirVersion(extractedRoot);
|
|
296
302
|
await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
|
|
@@ -299,8 +305,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
299
305
|
await initVersionContext(flags.fhirVersion);
|
|
300
306
|
}
|
|
301
307
|
const fhirInterfaceNames = ctx().interfaceNames;
|
|
308
|
+
endPhase(); // init
|
|
302
309
|
// Load dependencies from package.json BEFORE reading StructureDefinitions
|
|
303
310
|
// This ensures base profiles (like coverage-de-basis) are available for resolution
|
|
311
|
+
endPhase = startPhase('resolve');
|
|
304
312
|
const { getPackageManager } = await import('./parser/packageManager.js');
|
|
305
313
|
const packageManager = getPackageManager();
|
|
306
314
|
packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
|
|
@@ -313,6 +321,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
313
321
|
let valueSets = readValueSetsFromDir(extractedRoot);
|
|
314
322
|
logger.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
|
|
315
323
|
logger.log(`Loaded ${Object.keys(valueSetCodesMap).length} ValueSet code mappings (including dependencies)`);
|
|
324
|
+
endPhase(); // resolve
|
|
316
325
|
// Enrich ValueSets with codes resolved from CodeSystems (system-only compose.include)
|
|
317
326
|
const bindingUrls = collectValueSetBindingUrls(structureDefinitions);
|
|
318
327
|
enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingUrls);
|
|
@@ -328,6 +337,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
328
337
|
}
|
|
329
338
|
ensureDirectoryExists(outputDir);
|
|
330
339
|
// Generate TypeScript files for ValueSets inside the embedded package
|
|
340
|
+
endPhase = startPhase('generate');
|
|
331
341
|
const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
|
|
332
342
|
if (!didGenerateValueSets) {
|
|
333
343
|
cleanupStaleValueSetDir(outputDir);
|
|
@@ -335,12 +345,12 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
335
345
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
|
|
336
346
|
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
337
347
|
registerLocalStructureDefinitions(structureDefinitions);
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
registerLocalStructureDefinitions(depSDs);
|
|
348
|
+
// PackageManager already loaded and registered dependency SDs above —
|
|
349
|
+
// no need to re-parse them via readStructureDefinitionsFromDependencies.
|
|
341
350
|
for (const sd of structureDefinitions) {
|
|
342
351
|
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
|
|
343
352
|
}
|
|
353
|
+
endPhase(); // generate
|
|
344
354
|
// Create package.json in generated folder
|
|
345
355
|
const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
|
|
346
356
|
let packageName = path.basename(packageArchivePath, path.extname(packageArchivePath));
|
|
@@ -441,8 +451,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
441
451
|
fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
|
|
442
452
|
fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
|
|
443
453
|
// Compile TypeScript to JavaScript
|
|
454
|
+
endPhase = startPhase('compile');
|
|
444
455
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
445
456
|
await compileTypeScriptToJS(outputDir);
|
|
457
|
+
endPhase(); // compile
|
|
446
458
|
// Remove fhirpath stub — the real package is a peer dependency
|
|
447
459
|
removeFhirpathStub(outputDir);
|
|
448
460
|
// Remove @types/node stub — only needed for compilation
|
|
@@ -481,7 +493,13 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
481
493
|
fs.copyFileSync(generatedPackageJsonPath, rootPackageJsonPath);
|
|
482
494
|
logger.log('Copied package.json to tarball root');
|
|
483
495
|
const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
|
|
496
|
+
endPhase = startPhase('repack');
|
|
484
497
|
await createPackageFromDir(extractedRoot, finalArchive);
|
|
498
|
+
endPhase(); // repack
|
|
499
|
+
// Write timing summary to output directory
|
|
500
|
+
const timing = getTimingSummary();
|
|
501
|
+
fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
|
|
502
|
+
logger.log(formatTimingSummary(timing));
|
|
485
503
|
return finalArchive;
|
|
486
504
|
}
|
|
487
505
|
finally {
|
|
@@ -503,12 +521,16 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
503
521
|
*/
|
|
504
522
|
export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
505
523
|
initGenerationContext(flags, path.dirname(packageArchivePath));
|
|
524
|
+
resetTimings();
|
|
506
525
|
if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
|
|
507
526
|
throw new Error('Only .tgz or .zip supported.');
|
|
527
|
+
let endPhase = startPhase('extract');
|
|
508
528
|
const extractResult = await extractPackage(packageArchivePath);
|
|
509
529
|
const extractedRoot = extractResult.path;
|
|
530
|
+
endPhase();
|
|
510
531
|
const shouldCleanup = !extractResult.isCached;
|
|
511
532
|
// Auto-detect FHIR version from package when not explicitly set
|
|
533
|
+
endPhase = startPhase('init');
|
|
512
534
|
if (!flags?.fhirVersion) {
|
|
513
535
|
const detected = detectFhirVersion(extractedRoot);
|
|
514
536
|
await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
|
|
@@ -517,6 +539,8 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
517
539
|
await initVersionContext(flags.fhirVersion);
|
|
518
540
|
}
|
|
519
541
|
const fhirInterfaceNames = ctx().interfaceNames;
|
|
542
|
+
endPhase(); // init
|
|
543
|
+
endPhase = startPhase('resolve');
|
|
520
544
|
const { getPackageManager } = await import('./parser/packageManager.js');
|
|
521
545
|
const packageManager = getPackageManager();
|
|
522
546
|
packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
|
|
@@ -527,6 +551,7 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
527
551
|
let valueSets = readValueSetsFromDir(extractedRoot);
|
|
528
552
|
logger.log(`Package contains ${structureDefinitions.length} StructureDefinitions.`);
|
|
529
553
|
logger.log(`Loaded ${Object.keys(valueSetCodesMap).length} ValueSet code mappings (including dependencies)`);
|
|
554
|
+
endPhase(); // resolve
|
|
530
555
|
const bindingUrls = collectValueSetBindingUrls(structureDefinitions);
|
|
531
556
|
enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingUrls);
|
|
532
557
|
if (flags?.txServer) {
|
|
@@ -539,16 +564,18 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
539
564
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
540
565
|
}
|
|
541
566
|
ensureDirectoryExists(outputDir);
|
|
567
|
+
endPhase = startPhase('generate');
|
|
542
568
|
const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
|
|
543
569
|
if (!didGenerateValueSets)
|
|
544
570
|
cleanupStaleValueSetDir(outputDir);
|
|
545
571
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
|
|
546
572
|
registerLocalStructureDefinitions(structureDefinitions);
|
|
547
|
-
|
|
548
|
-
|
|
573
|
+
// PackageManager already loaded and registered dependency SDs above —
|
|
574
|
+
// no need to re-parse them via readStructureDefinitionsFromDependencies.
|
|
549
575
|
for (const sd of structureDefinitions) {
|
|
550
576
|
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
|
|
551
577
|
}
|
|
578
|
+
endPhase(); // generate
|
|
552
579
|
// Create package.json in generated folder
|
|
553
580
|
const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
|
|
554
581
|
let packageName = path.basename(packageArchivePath, path.extname(packageArchivePath));
|
|
@@ -633,8 +660,10 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
633
660
|
fs.mkdirSync(nodeTypesDir, { recursive: true });
|
|
634
661
|
fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
|
|
635
662
|
fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
|
|
663
|
+
endPhase = startPhase('compile');
|
|
636
664
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
637
665
|
await compileTypeScriptToJS(outputDir);
|
|
666
|
+
endPhase(); // compile
|
|
638
667
|
removeFhirpathStub(outputDir);
|
|
639
668
|
try {
|
|
640
669
|
fs.rmSync(path.join(outputDir, 'node_modules', '@types'), { recursive: true, force: true });
|
|
@@ -660,6 +689,10 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
660
689
|
fs.rmdirSync(nodeModulesDir);
|
|
661
690
|
}
|
|
662
691
|
catch { /* not empty or doesn't exist — fine */ }
|
|
692
|
+
// Write timing summary to output directory
|
|
693
|
+
const timing = getTimingSummary();
|
|
694
|
+
fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
|
|
695
|
+
logger.log(formatTimingSummary(timing));
|
|
663
696
|
return {
|
|
664
697
|
generatedDir: outputDir,
|
|
665
698
|
cleanup: () => {
|
|
@@ -681,7 +714,9 @@ function deriveOutputArchiveName(input) {
|
|
|
681
714
|
*/
|
|
682
715
|
export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
683
716
|
initGenerationContext(flags, outputDir);
|
|
717
|
+
resetTimings();
|
|
684
718
|
// Auto-detect FHIR version from directory contents when not explicitly set
|
|
719
|
+
let endPhase = startPhase('init');
|
|
685
720
|
if (!flags?.fhirVersion) {
|
|
686
721
|
const detected = detectFhirVersion(inputDir);
|
|
687
722
|
await initVersionContext(detected ?? DEFAULT_FHIR_VERSION);
|
|
@@ -690,11 +725,13 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
690
725
|
await initVersionContext(flags.fhirVersion);
|
|
691
726
|
}
|
|
692
727
|
const fhirInterfaceNames = ctx().interfaceNames;
|
|
728
|
+
endPhase(); // init
|
|
693
729
|
if (!fs.existsSync(inputDir))
|
|
694
730
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
695
731
|
if (!fs.existsSync(outputDir))
|
|
696
732
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
697
733
|
// Load ValueSets from the directory
|
|
734
|
+
endPhase = startPhase('resolve');
|
|
698
735
|
const valueSets = readValueSetsFromDir(inputDir);
|
|
699
736
|
// Ensure dependency packages are downloaded before loading ValueSets
|
|
700
737
|
await ensureDependenciesDownloaded(inputDir);
|
|
@@ -710,6 +747,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
710
747
|
log.warn(`No archives or StructureDefinition JSON in ${inputDir}`);
|
|
711
748
|
return;
|
|
712
749
|
}
|
|
750
|
+
endPhase(); // resolve
|
|
713
751
|
// Collect StructureDefinitions from JSON files, then build profile registries
|
|
714
752
|
const localStructureDefinitions = [];
|
|
715
753
|
for (const jf of jsonFiles) {
|
|
@@ -733,6 +771,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
733
771
|
}
|
|
734
772
|
}
|
|
735
773
|
// Generate TypeScript files for ValueSets
|
|
774
|
+
endPhase = startPhase('generate');
|
|
736
775
|
if (!emitValueSetFiles(valueSets, outputDir)) {
|
|
737
776
|
cleanupStaleValueSetDir(outputDir);
|
|
738
777
|
}
|
|
@@ -786,6 +825,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
786
825
|
const { generateDicomweb } = await import('./emitters/dicomweb/dicomwebGenerator.js');
|
|
787
826
|
generateDicomweb({ outputDir });
|
|
788
827
|
}
|
|
828
|
+
endPhase(); // generate
|
|
829
|
+
// Write timing summary to output directory
|
|
830
|
+
const timing = getTimingSummary();
|
|
831
|
+
fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
|
|
832
|
+
logger.log(formatTimingSummary(timing));
|
|
789
833
|
}
|
|
790
834
|
export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
|
|
791
835
|
// Initialize FHIR version context
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { registerLocalStructureDefinitions } from './sdParser.js';
|
|
7
|
+
import { isLargeHl7Package } from './packageParser.js';
|
|
7
8
|
import { logger } from '../../logger.js';
|
|
8
9
|
import { getFhirPackagesCacheDir, ensureCacheDir } from '../core/cacheConfig.js';
|
|
9
10
|
export class PackageManager {
|
|
@@ -166,9 +167,14 @@ export class PackageManager {
|
|
|
166
167
|
try {
|
|
167
168
|
const files = fs.readdirSync(sdDir);
|
|
168
169
|
for (const file of files) {
|
|
170
|
+
// Only read StructureDefinition-*.json files (FHIR naming convention).
|
|
171
|
+
// Fall back to all .json if no prefix-matched files found (e.g. KBV packages).
|
|
169
172
|
if (!file.endsWith('.json') || file === 'package.json') {
|
|
170
173
|
continue;
|
|
171
174
|
}
|
|
175
|
+
if (!file.startsWith('StructureDefinition-')) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
172
178
|
try {
|
|
173
179
|
const filePath = path.join(sdDir, file);
|
|
174
180
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -182,6 +188,25 @@ export class PackageManager {
|
|
|
182
188
|
continue;
|
|
183
189
|
}
|
|
184
190
|
}
|
|
191
|
+
// Fallback: if no prefix-matched files, scan all JSON (non-standard naming)
|
|
192
|
+
if (sds.length === 0) {
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
if (!file.endsWith('.json') || file === 'package.json' || file.startsWith('StructureDefinition-')) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const filePath = path.join(sdDir, file);
|
|
199
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
200
|
+
const resource = JSON.parse(content);
|
|
201
|
+
if (resource.resourceType === 'StructureDefinition') {
|
|
202
|
+
sds.push(resource);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
185
210
|
}
|
|
186
211
|
catch (e) {
|
|
187
212
|
logger.log(`Failed to read StructureDefinitions from ${packagePath}: ${e.message}`);
|
|
@@ -243,8 +268,10 @@ export class PackageManager {
|
|
|
243
268
|
const packageJson = JSON.parse(content);
|
|
244
269
|
if (packageJson.dependencies && typeof packageJson.dependencies === 'object') {
|
|
245
270
|
for (const [depName, depVersion] of Object.entries(packageJson.dependencies)) {
|
|
246
|
-
// Skip
|
|
247
|
-
|
|
271
|
+
// Skip large HL7 base packages (core, extensions, terminology) —
|
|
272
|
+
// they contain thousands of files and are handled via deriveVersionData
|
|
273
|
+
// or on-demand fetching.
|
|
274
|
+
if (isLargeHl7Package(depName)) {
|
|
248
275
|
continue;
|
|
249
276
|
}
|
|
250
277
|
const depRef = `${depName}@${depVersion}`;
|
|
@@ -95,7 +95,7 @@ export function resolveWellKnownValueSetCodes(valueSetCodesMap) {
|
|
|
95
95
|
function resolveValueSetCompose(visitedDirs, merged) {
|
|
96
96
|
let resolved = 0;
|
|
97
97
|
for (const dir of visitedDirs) {
|
|
98
|
-
const jsonFiles =
|
|
98
|
+
const jsonFiles = findJsonFilesByPrefix(dir, 'ValueSet-');
|
|
99
99
|
for (const file of jsonFiles) {
|
|
100
100
|
try {
|
|
101
101
|
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
@@ -197,6 +197,37 @@ function findAllJsonFiles(dir) {
|
|
|
197
197
|
}
|
|
198
198
|
return results;
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Find JSON files matching a filename prefix (e.g., "StructureDefinition-").
|
|
202
|
+
* FHIR packages name files by resourceType, so this avoids parsing thousands
|
|
203
|
+
* of irrelevant JSON files and dramatically reduces memory usage for large
|
|
204
|
+
* packages like hl7.fhir.r5.core (~2,970 files but only ~307 are SDs).
|
|
205
|
+
*
|
|
206
|
+
* Falls back to returning ALL JSON files when no prefix-matched files are found,
|
|
207
|
+
* because some packages (e.g. KBV) use non-standard naming conventions.
|
|
208
|
+
*/
|
|
209
|
+
function findJsonFilesByPrefix(dir, prefix) {
|
|
210
|
+
const prefixed = findJsonFilesMatching(dir, prefix);
|
|
211
|
+
if (prefixed.length > 0)
|
|
212
|
+
return prefixed;
|
|
213
|
+
// Non-standard naming — fall back to all JSON files
|
|
214
|
+
log.debug(`No files matching prefix "${prefix}" in ${dir}, falling back to all JSON files`);
|
|
215
|
+
return findAllJsonFiles(dir);
|
|
216
|
+
}
|
|
217
|
+
function findJsonFilesMatching(dir, prefix) {
|
|
218
|
+
let results = [];
|
|
219
|
+
const list = fs.readdirSync(dir, { withFileTypes: true });
|
|
220
|
+
for (const entry of list) {
|
|
221
|
+
const fullPath = path.join(dir, entry.name);
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
results = results.concat(findJsonFilesMatching(fullPath, prefix));
|
|
224
|
+
}
|
|
225
|
+
else if (entry.isFile() && entry.name.endsWith(".json") && entry.name.startsWith(prefix)) {
|
|
226
|
+
results.push(fullPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
200
231
|
/** Map FHIR version strings to slugs. */
|
|
201
232
|
const FHIR_VERSION_TO_SLUG = {
|
|
202
233
|
'4.0.0': 'r4', '4.0.1': 'r4',
|
|
@@ -209,8 +240,9 @@ const FHIR_VERSION_TO_SLUG = {
|
|
|
209
240
|
* Returns the detected slug, or undefined if detection fails.
|
|
210
241
|
*/
|
|
211
242
|
export function detectFhirVersion(extractedRoot) {
|
|
212
|
-
|
|
213
|
-
|
|
243
|
+
// Try ImplementationGuide files first (usually just 1)
|
|
244
|
+
const igFiles = findJsonFilesByPrefix(extractedRoot, 'ImplementationGuide-');
|
|
245
|
+
for (const file of igFiles) {
|
|
214
246
|
try {
|
|
215
247
|
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
216
248
|
if (content.resourceType === 'ImplementationGuide') {
|
|
@@ -229,7 +261,8 @@ export function detectFhirVersion(extractedRoot) {
|
|
|
229
261
|
}
|
|
230
262
|
}
|
|
231
263
|
// Fallback: check fhirVersion on first StructureDefinition found
|
|
232
|
-
|
|
264
|
+
const sdFiles = findJsonFilesByPrefix(extractedRoot, 'StructureDefinition-');
|
|
265
|
+
for (const file of sdFiles) {
|
|
233
266
|
try {
|
|
234
267
|
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
235
268
|
if (content.resourceType === 'StructureDefinition' && content.fhirVersion) {
|
|
@@ -293,7 +326,7 @@ export async function extractPackage(packagePath) {
|
|
|
293
326
|
* Reads all StructureDefinition resources from an already extracted package directory.
|
|
294
327
|
*/
|
|
295
328
|
export function readStructureDefinitionsFromDir(extractedRoot) {
|
|
296
|
-
const jsonFiles =
|
|
329
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'StructureDefinition-');
|
|
297
330
|
const structureDefinitions = [];
|
|
298
331
|
for (const file of jsonFiles) {
|
|
299
332
|
try {
|
|
@@ -324,7 +357,7 @@ export async function createPackageFromDir(sourceDir, outFile) {
|
|
|
324
357
|
* compose.include entries that reference a whole system without listing concepts.
|
|
325
358
|
*/
|
|
326
359
|
export function readCodeSystemConceptsFromDir(extractedRoot) {
|
|
327
|
-
const jsonFiles =
|
|
360
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'CodeSystem-');
|
|
328
361
|
const map = {};
|
|
329
362
|
for (const file of jsonFiles) {
|
|
330
363
|
try {
|
|
@@ -367,7 +400,7 @@ function flattenCodeSystemConcepts(concepts) {
|
|
|
367
400
|
* Each code entry has shape { code, system? } drawn from compose.include or expansion contains.
|
|
368
401
|
*/
|
|
369
402
|
export function readValueSetCodesFromDir(extractedRoot, codeSystemMap) {
|
|
370
|
-
const jsonFiles =
|
|
403
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'ValueSet-');
|
|
371
404
|
const map = {};
|
|
372
405
|
for (const file of jsonFiles) {
|
|
373
406
|
try {
|
|
@@ -529,6 +562,28 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
|
|
|
529
562
|
}
|
|
530
563
|
await walkDeps(extractedRoot);
|
|
531
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Returns true for large HL7 base packages that should be skipped when walking
|
|
567
|
+
* dependency trees eagerly. These packages contain thousands of JSON files and
|
|
568
|
+
* loading them all into memory causes OOM for R5 workloads.
|
|
569
|
+
*
|
|
570
|
+
* Core SDs are derived via `deriveVersionData()` / bundled base definitions.
|
|
571
|
+
* Extensions and terminology resources are fetched on-demand as needed.
|
|
572
|
+
*/
|
|
573
|
+
export function isLargeHl7Package(depName) {
|
|
574
|
+
// hl7.terminology.r5, hl7.terminology.r4, etc. — large CodeSystem/ValueSet packages
|
|
575
|
+
if (depName.startsWith('hl7.terminology'))
|
|
576
|
+
return true;
|
|
577
|
+
if (!depName.startsWith('hl7.fhir.'))
|
|
578
|
+
return false;
|
|
579
|
+
// hl7.fhir.r5.core, hl7.fhir.r4.core, etc.
|
|
580
|
+
if (depName.includes('.core'))
|
|
581
|
+
return true;
|
|
582
|
+
// hl7.fhir.uv.extensions.r5, hl7.fhir.uv.extensions.r4, etc.
|
|
583
|
+
if (depName.includes('extensions'))
|
|
584
|
+
return true;
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
532
587
|
/**
|
|
533
588
|
* Reads StructureDefinitions from all dependency packages (recursively).
|
|
534
589
|
* Returns SDs from dependencies only — the main package SDs are loaded separately.
|
|
@@ -560,6 +615,10 @@ export function readStructureDefinitionsFromDependencies(extractedRoot) {
|
|
|
560
615
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
561
616
|
const deps = pkgJson.dependencies || {};
|
|
562
617
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
618
|
+
// Skip large HL7 base packages — core SDs are available via deriveVersionData /
|
|
619
|
+
// bundled base SDs, and extensions & terminology are fetched on-demand.
|
|
620
|
+
if (isLargeHl7Package(depName))
|
|
621
|
+
continue;
|
|
563
622
|
for (const sep of ['@', '#']) {
|
|
564
623
|
const depDir = path.join(cacheDir, `${depName}${sep}${depVersion}`);
|
|
565
624
|
if (fs.existsSync(depDir)) {
|
|
@@ -608,6 +667,8 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
|
|
|
608
667
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
609
668
|
const deps = pkgJson.dependencies || {};
|
|
610
669
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
670
|
+
if (isLargeHl7Package(depName))
|
|
671
|
+
continue;
|
|
611
672
|
const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
|
|
612
673
|
if (fs.existsSync(depDir))
|
|
613
674
|
collectCodeSystems(depDir);
|
|
@@ -654,6 +715,8 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
|
|
|
654
715
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
655
716
|
const deps = pkgJson.dependencies || {};
|
|
656
717
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
718
|
+
if (isLargeHl7Package(depName))
|
|
719
|
+
continue;
|
|
657
720
|
// Look for dependency in cache: name@version format
|
|
658
721
|
const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
|
|
659
722
|
if (fs.existsSync(depDir)) {
|
|
@@ -683,7 +746,7 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
|
|
|
683
746
|
* This provides richer metadata than readValueSetCodesFromDir, including type generation capabilities.
|
|
684
747
|
*/
|
|
685
748
|
export function readValueSetsFromDir(extractedRoot) {
|
|
686
|
-
const jsonFiles =
|
|
749
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'ValueSet-');
|
|
687
750
|
const map = new Map();
|
|
688
751
|
for (const file of jsonFiles) {
|
|
689
752
|
try {
|
|
@@ -101,13 +101,12 @@ export async function fetchStructureDefinition(url, fallbackUrl) {
|
|
|
101
101
|
url = url.replace(/^https?:\/\/hl7\.org\/fhir\/StructureDefinition\//, `http://hl7.org/fhir/${targetVersion}/StructureDefinition/`);
|
|
102
102
|
logger.debug(`[fetchSD] Adjusted versionless FHIR URL to ${targetVersion}: ${originalUrl} -> ${url}`);
|
|
103
103
|
}
|
|
104
|
-
// Special case: Base
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return fetchStructureDefinition(resourceProfileUrl, resourceProfileUrl);
|
|
104
|
+
// Special case: FHIR "Base" is the absolute root type with no parent.
|
|
105
|
+
// It has no meaningful fields to inherit. Return null to stop recursion.
|
|
106
|
+
// Note: Must match exactly /Base (not /BaseResource, /BaseSomething etc.)
|
|
107
|
+
if (/\/StructureDefinition\/Base$/.test(url) && url.includes('hl7.org/fhir')) {
|
|
108
|
+
logger.debug(`[fetchSD] Base type detected (FHIR root) — returning null to stop recursion`);
|
|
109
|
+
return null;
|
|
111
110
|
}
|
|
112
111
|
// First, check if we have this SD registered locally (from input directory)
|
|
113
112
|
if (localStructureDefinitions.has(url)) {
|
|
@@ -9,6 +9,8 @@ const log = logger.withTag('sdParser');
|
|
|
9
9
|
// Re-export fetcher functions for backward compatibility
|
|
10
10
|
export { fetchStructureDefinition, fetchStructureDefinitions, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './sdFetcher.js';
|
|
11
11
|
const processedBaseDefinitions = new Map(); // Track processed base resources
|
|
12
|
+
// Guard against circular base-definition chains (e.g., Base → Base in R5)
|
|
13
|
+
const _inFlightBaseUrls = new Set();
|
|
12
14
|
/** Deep-clone an array of Field objects to prevent shared mutable state.
|
|
13
15
|
* Clones binding, patternConstraint, nestedCodingSlices (with their sub-objects),
|
|
14
16
|
* and requiredSlices so that mutations on one copy don't affect the other. */
|
|
@@ -84,8 +86,14 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
84
86
|
// modifying nestedCodingSlices) from bleeding between sibling profiles.
|
|
85
87
|
baseFields = deepCloneFields(processedBaseDefinitions.get(baseDefinitionUrl) || []);
|
|
86
88
|
}
|
|
89
|
+
else if (_inFlightBaseUrls.has(baseDefinitionUrl)) {
|
|
90
|
+
// Circular base-definition chain detected (e.g., Base → Base in R5).
|
|
91
|
+
// Treat as root type — no inherited fields.
|
|
92
|
+
log.log(`Circular baseDefinition detected for ${baseDefinitionUrl}, treating as root type.`);
|
|
93
|
+
}
|
|
87
94
|
else {
|
|
88
95
|
log.log(`Fetching base resource for StructureDefinition ${structureDefinition.id || "unknown"}: ${baseDefinitionUrl}`);
|
|
96
|
+
_inFlightBaseUrls.add(baseDefinitionUrl);
|
|
89
97
|
// Only use fhirServerUrl if it's a valid HTTP URL AND it's not a canonical StructureDefinition URL
|
|
90
98
|
// (fhirServerUrl should be a server base like "https://server.com", not a full StructureDefinition URL)
|
|
91
99
|
const isValidHttpUrl = fhirServerUrl && (fhirServerUrl.startsWith('http://') || fhirServerUrl.startsWith('https://'));
|
|
@@ -138,6 +146,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
138
146
|
impact: 'Generated code may have wrong types; validators cannot fully evaluate this profile.',
|
|
139
147
|
});
|
|
140
148
|
}
|
|
149
|
+
_inFlightBaseUrls.delete(baseDefinitionUrl);
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
const elements = structureDefinition.snapshot?.element || structureDefinition.differential?.element || [];
|
|
@@ -6,6 +6,7 @@ import fs from 'fs';
|
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
7
|
import { generateRandomSupportContent } from './emitters/class/randomSupportGenerator.js';
|
|
8
8
|
import { generateValidatorOptionsContent } from './emitters/validator/validatorGenerator.js';
|
|
9
|
+
import { ctx } from './fhir/versionContext.js';
|
|
9
10
|
// ── Fetch failure tracking ─────────────────────────────────────────
|
|
10
11
|
let failedFetchCount = 0;
|
|
11
12
|
const failedFetchProfiles = [];
|
|
@@ -35,8 +36,13 @@ export function getFetchFailureWarning() {
|
|
|
35
36
|
`Note: The .cache folder has been preserved to speed up the next generation attempt.\n`;
|
|
36
37
|
}
|
|
37
38
|
// ── FHIR complex type child map ────────────────────────────────────
|
|
38
|
-
/**
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Static fallback for complex type child fields.
|
|
41
|
+
* Used when ctx().dataTypes is not yet populated (early init) or for types
|
|
42
|
+
* not present in the derived data. The canonical source of truth is
|
|
43
|
+
* ctx().dataTypes which is derived from the real FHIR core SDs.
|
|
44
|
+
*/
|
|
45
|
+
const COMPLEX_TYPE_CHILDREN_FALLBACK = {
|
|
40
46
|
Quantity: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
|
|
41
47
|
Money: { value: 'decimal', currency: 'code' },
|
|
42
48
|
Identifier: { use: 'code', system: 'uri', value: 'string' },
|
|
@@ -53,11 +59,23 @@ const COMPLEX_TYPE_CHILDREN = {
|
|
|
53
59
|
Count: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
|
|
54
60
|
Distance: { value: 'decimal', comparator: 'code', unit: 'string', system: 'uri', code: 'code' },
|
|
55
61
|
Timing: { event: 'dateTime' },
|
|
56
|
-
SampledData: { period: 'decimal', dimensions: 'positiveInt', data: 'string' },
|
|
57
62
|
};
|
|
58
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the type of a child field within a known FHIR complex type.
|
|
65
|
+
* Prefers the version-derived ctx().dataTypes (which handles cross-version
|
|
66
|
+
* differences like SampledData automatically), falling back to the static map.
|
|
67
|
+
*/
|
|
59
68
|
export function resolveComplexTypeChildType(parentType, childName) {
|
|
60
|
-
|
|
69
|
+
// Primary: version-derived data (works for any FHIR version)
|
|
70
|
+
try {
|
|
71
|
+
const derived = ctx().dataTypes[parentType];
|
|
72
|
+
if (derived?.[childName])
|
|
73
|
+
return derived[childName].type;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// ctx() not yet initialised — fall through to static map
|
|
77
|
+
}
|
|
78
|
+
return COMPLEX_TYPE_CHILDREN_FALLBACK[parentType]?.[childName];
|
|
61
79
|
}
|
|
62
80
|
// ── Support file generators ────────────────────────────────────────
|
|
63
81
|
/** Ensure RandomSupport.ts exists in a generation output directory */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight generation-phase timer.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* resetTimings();
|
|
6
|
+
* const end = startPhase('fetch');
|
|
7
|
+
* await doWork();
|
|
8
|
+
* end();
|
|
9
|
+
* // … more phases …
|
|
10
|
+
* const summary = getTimingSummary(); // { phases: { fetch: 1234, … }, totalMs: 5678 }
|
|
11
|
+
*/
|
|
12
|
+
let _phases = {};
|
|
13
|
+
let _wallStart = 0;
|
|
14
|
+
/** Clear all recorded timings and start the wall clock. */
|
|
15
|
+
export function resetTimings() {
|
|
16
|
+
_phases = {};
|
|
17
|
+
_wallStart = performance.now();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Begin timing a named phase. Returns a callback to call when the phase ends.
|
|
21
|
+
* If the same phase name is used multiple times the durations are summed.
|
|
22
|
+
*/
|
|
23
|
+
export function startPhase(name) {
|
|
24
|
+
const t0 = performance.now();
|
|
25
|
+
return () => {
|
|
26
|
+
const elapsed = performance.now() - t0;
|
|
27
|
+
_phases[name] = (_phases[name] ?? 0) + elapsed;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Get the timing summary (all phases + wall-clock total). */
|
|
31
|
+
export function getTimingSummary() {
|
|
32
|
+
return {
|
|
33
|
+
phases: { ...(_phases) },
|
|
34
|
+
totalMs: Math.round(performance.now() - _wallStart),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Format the summary as a human-readable string for console output. */
|
|
38
|
+
export function formatTimingSummary(summary) {
|
|
39
|
+
const fmt = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
40
|
+
const lines = Object.entries(summary.phases).map(([name, ms]) => ` ${name}: ${fmt(ms)}`);
|
|
41
|
+
lines.push(` total: ${fmt(summary.totalMs)}`);
|
|
42
|
+
return `⏱ Generation timing\n${lines.join('\n')}`;
|
|
43
|
+
}
|
package/out/src/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { generateIntoPackage, generateForDirectory, generateFromJsonFile, generateFromProfileUrl, resetFetchFailureTracking, getFetchFailureWarning } from "./generator/index.js";
|
|
2
|
+
import { generateIntoPackage, generateForDirectory, generateFromJsonFile, generateFromProfileUrl, resetFetchFailureTracking, getFetchFailureWarning, getTimingSummary, formatTimingSummary } from "./generator/index.js";
|
|
3
3
|
import { generateClient } from "./generator/emitters/client/clientGenerator.js";
|
|
4
4
|
import { extractPackage } from "./generator/parser/packageParser.js";
|
|
5
5
|
import { getCacheConfig, getFhirPackagesCacheDir, clearAllCaches, ensureCacheDir, setCacheConfig } from "./generator/core/cacheConfig.js";
|
|
@@ -113,6 +113,10 @@ function completeGeneration(flags, outputDir) {
|
|
|
113
113
|
const summary = formatReportSummary(report);
|
|
114
114
|
if (summary.trim())
|
|
115
115
|
console.log(summary);
|
|
116
|
+
// Print generation timing summary
|
|
117
|
+
const timing = getTimingSummary();
|
|
118
|
+
if (timing.totalMs > 0)
|
|
119
|
+
console.log(formatTimingSummary(timing));
|
|
116
120
|
const hasWarnings = showGenerationWarnings();
|
|
117
121
|
if (!hasWarnings && flags.noCache) {
|
|
118
122
|
cleanupCache();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/src/main.js",
|