babelfhir-ts 1.5.21 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/out/src/generator/core/extensionSdReader.js +2 -1
- package/out/src/generator/core/utils.js +69 -0
- package/out/src/generator/emitters/class/classGeneratorHelpers.js +10 -2
- package/out/src/generator/emitters/class/sliceElementDefaults.js +10 -1
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +15 -12
- package/out/src/generator/emitters/interface/interfaceFieldUtils.js +6 -3
- package/out/src/generator/emitters/interface/postProcessExtensions.js +8 -2
- package/out/src/generator/emitters/interface/processNestedField.js +4 -6
- package/out/src/generator/emitters/namingsystem/namingSystemGenerator.js +136 -0
- package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -15
- package/out/src/generator/emitters/prefab/prefabRenderer.js +2 -19
- package/out/src/generator/emitters/validator/closedSlicingValidation.js +5 -2
- package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +7 -1
- package/out/src/generator/emitters/validator/sliceBackboneValidation.js +27 -12
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +15 -27
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +5 -1
- package/out/src/generator/emitters/validator/subExtensionScope.js +10 -2
- package/out/src/generator/emitters/validator/validatorExpressions.js +44 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +34 -17
- package/out/src/generator/emitters/validator/validatorGenerator.js +31 -40
- package/out/src/generator/emitters/validator/validatorRuntime.js +13 -2
- package/out/src/generator/emitters/valueset/valueSetGenerator.js +6 -10
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +39 -3
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +36 -8
- package/out/src/generator/generationHelpers.js +59 -7
- package/out/src/generator/index.js +90 -47
- package/out/src/generator/parser/packageParser.js +76 -0
- package/out/src/generator/parser/vsParser.js +4 -2
- package/out/src/generator/sdProcessor.js +30 -21
- package/out/src/main.js +5 -0
- package/package.json +12 -9
- package/parity-matrix.json +6 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and required ValueSet bindings.
|
|
4
4
|
*/
|
|
5
5
|
import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
|
|
6
|
+
import { scopeConstraintExpression } from '../validator/validatorExpressions.js';
|
|
6
7
|
/** Escape double-quote characters in refinement messages */
|
|
7
8
|
function escStr(s) {
|
|
8
9
|
return s.replace(/"/g, '\\"');
|
|
@@ -11,8 +12,11 @@ function escStr(s) {
|
|
|
11
12
|
* Build refinement chains for pattern constraints, required slice cardinalities,
|
|
12
13
|
* and FHIRPath invariants. Returns refinement code strings plus tracked constraint metadata.
|
|
13
14
|
*/
|
|
14
|
-
export function buildRefinements(allFields, rootPrefix, valueSets
|
|
15
|
+
export function buildRefinements(allFields, rootPrefix, valueSets,
|
|
16
|
+
/** See generateZodSchema's `emitInvariants`: opt-in, for the per-parse cost. */
|
|
17
|
+
emitInvariants = false) {
|
|
15
18
|
const refinements = [];
|
|
19
|
+
const invariantEvaluators = [];
|
|
16
20
|
const bindingVsImports = new Map();
|
|
17
21
|
// Build set of direct (root-level) field names for safety checks
|
|
18
22
|
const directFieldNames = new Set();
|
|
@@ -163,10 +167,42 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
|
|
|
163
167
|
unsupportedConstraints.push(meta);
|
|
164
168
|
}
|
|
165
169
|
else {
|
|
166
|
-
// Supported but not yet emitted — future FHIRPath→JS translation point
|
|
167
170
|
supportedConstraints.push(meta);
|
|
168
171
|
}
|
|
169
172
|
}
|
|
173
|
+
// Emit the supported invariants.
|
|
174
|
+
//
|
|
175
|
+
// These were tracked and never emitted, on the premise that FHIRPath
|
|
176
|
+
// evaluation is async while Zod refinements are synchronous. Neither half
|
|
177
|
+
// holds: fhirpath.js returns an array and only returns a Promise when async
|
|
178
|
+
// mode is opted into, which it needs solely for memberOf()/resolve() — and
|
|
179
|
+
// those are already in `unsupportedConstraints` above. So every constraint
|
|
180
|
+
// here evaluates synchronously inside a plain refinement, and `safeParse`
|
|
181
|
+
// keeps working for consumers.
|
|
182
|
+
//
|
|
183
|
+
// The expression goes through the same scoping the validator uses, and the
|
|
184
|
+
// message repeats its wording, so the two agree per profile rather than
|
|
185
|
+
// reporting the same defect under different text.
|
|
186
|
+
supportedConstraints.forEach((meta, i) => {
|
|
187
|
+
if (!emitInvariants)
|
|
188
|
+
return;
|
|
189
|
+
const scoped = scopeConstraintExpression(meta.expression, meta.fieldPath);
|
|
190
|
+
if (!scoped)
|
|
191
|
+
return;
|
|
192
|
+
const human = escStr(meta.human).replace(/[\r\n]+/g, ' ');
|
|
193
|
+
const fn = `_inv${i}`;
|
|
194
|
+
// Compiled once at module load, not per parse. `evaluate` re-parses the
|
|
195
|
+
// expression on every call, which cost ~0.3ms each: 69 invariants made a
|
|
196
|
+
// C4BBOrganization parse 242x slower on an empty resource and 964x on a
|
|
197
|
+
// populated one. `compile` parses once and returns a reusable evaluator.
|
|
198
|
+
//
|
|
199
|
+
// traceFn is silenced deliberately: fhirpath's trace() defaults to writing
|
|
200
|
+
// to the console, so a schema parse would print "TRACE:[…]" out of a
|
|
201
|
+
// published package.
|
|
202
|
+
invariantEvaluators.push(`const ${fn} = fhirpath.compile("${scoped}", fhirpath_model, { preciseMath: true, traceFn: () => {} });`);
|
|
203
|
+
refinements.push(`.refine(d => { try { const _r = ${fn}(d, { resource: d, rootResource: d }); return Array.isArray(_r) ? _r.every(Boolean) : true; } catch { return true; } },`
|
|
204
|
+
+ ` { message: "Constraint violation: ${human}", when: () => true })`);
|
|
205
|
+
});
|
|
170
206
|
// 4. Meta sub-field constraints
|
|
171
207
|
// When a profile constrains meta.lastUpdated, meta.profile, etc. with min>=1,
|
|
172
208
|
// emit refinements that check for their presence. Uses `when: () => true` so
|
|
@@ -253,5 +289,5 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
|
|
|
253
289
|
}
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
|
-
return { refinements, supportedConstraints, unsupportedConstraints, valueSetImports: bindingVsImports };
|
|
292
|
+
return { refinements, supportedConstraints, unsupportedConstraints, valueSetImports: bindingVsImports, invariantEvaluators };
|
|
257
293
|
}
|
|
@@ -53,7 +53,16 @@ function isPrimitive(fhirType) {
|
|
|
53
53
|
* @param baseFields Fields inherited from the base resource
|
|
54
54
|
* @param valueSets Optional parsed ValueSets for enum generation
|
|
55
55
|
*/
|
|
56
|
-
export function generateZodSchema(interfaceName, newFields, baseResource, baseFields = [], valueSets
|
|
56
|
+
export function generateZodSchema(interfaceName, newFields, baseResource, baseFields = [], valueSets,
|
|
57
|
+
/**
|
|
58
|
+
* Emit the FHIRPath invariants as refinements.
|
|
59
|
+
*
|
|
60
|
+
* Off by default: each invariant is a compiled FHIRPath evaluation on every
|
|
61
|
+
* parse (~5.5us, so +0.38ms across C4BBOrganization's 69), which is the right
|
|
62
|
+
* trade for conformance checking and the wrong one for per-request validation
|
|
63
|
+
* in a hot path. The caller decides.
|
|
64
|
+
*/
|
|
65
|
+
emitInvariants = false) {
|
|
57
66
|
const lines = [];
|
|
58
67
|
const imports = new Set();
|
|
59
68
|
const baseTypeImports = new Set();
|
|
@@ -113,14 +122,22 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
|
|
|
113
122
|
// Pre-compute minimum cardinality implied by required slices.
|
|
114
123
|
// If an array field (e.g., extension) has slices with min>=1, the parent array
|
|
115
124
|
// must contain at least that many elements even if its own min is 0.
|
|
125
|
+
//
|
|
126
|
+
// Keyed by the slice's full path. Keying by the last segment made every
|
|
127
|
+
// `*.extension:slice` anywhere in the resource count towards the *root*
|
|
128
|
+
// `extension`, so PASClaimBase emitted `z.array(ExtensionSchema).min(3)` for a
|
|
129
|
+
// profile whose Claim.extension slices are all min=0.
|
|
130
|
+
//
|
|
131
|
+
// An unstated `min` counts as 0, not 1. A differential states only what it
|
|
132
|
+
// changes, so a slice that says nothing about cardinality inherits the base,
|
|
133
|
+
// and a slice can only become required by stating min>=1 itself.
|
|
116
134
|
const sliceMinByParent = new Map();
|
|
117
135
|
for (const f of allFields) {
|
|
118
136
|
if (!f.sliceName)
|
|
119
137
|
continue;
|
|
120
|
-
const sliceMin = typeof f.min === 'number' ? f.min :
|
|
138
|
+
const sliceMin = typeof f.min === 'number' ? f.min : 0;
|
|
121
139
|
if (sliceMin > 0) {
|
|
122
|
-
|
|
123
|
-
sliceMinByParent.set(parentKey, (sliceMinByParent.get(parentKey) || 0) + sliceMin);
|
|
140
|
+
sliceMinByParent.set(f.name, (sliceMinByParent.get(f.name) || 0) + sliceMin);
|
|
124
141
|
}
|
|
125
142
|
}
|
|
126
143
|
for (const field of schemaFields) {
|
|
@@ -181,7 +198,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
|
|
|
181
198
|
if (field.isArray) {
|
|
182
199
|
let minItems = typeof field.min === 'number' && field.min > 0 ? field.min : 0;
|
|
183
200
|
// If required slices exist, the parent array needs at least that many items
|
|
184
|
-
const sliceImpliedMin = sliceMinByParent.get(
|
|
201
|
+
const sliceImpliedMin = sliceMinByParent.get(field.name) || 0;
|
|
185
202
|
if (sliceImpliedMin > minItems)
|
|
186
203
|
minItems = sliceImpliedMin;
|
|
187
204
|
const maxItems = typeof field.max === 'number' && field.max !== Infinity ? field.max : 0;
|
|
@@ -220,7 +237,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
|
|
|
220
237
|
}
|
|
221
238
|
// Make optional or required
|
|
222
239
|
// A field is required if it has min>=1 OR required slices imply a minimum
|
|
223
|
-
const effectiveRequired = !field.isOptional || (sliceMinByParent.get(
|
|
240
|
+
const effectiveRequired = !field.isOptional || (sliceMinByParent.get(field.name) || 0) > 0;
|
|
224
241
|
if (!effectiveRequired) {
|
|
225
242
|
zodType = `${zodType}.optional()`;
|
|
226
243
|
}
|
|
@@ -256,8 +273,8 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
|
|
|
256
273
|
// because DataType random() methods don't produce pattern-aware data.
|
|
257
274
|
const isDataTypeProfile = getZodPackageTypes().has(rootPrefix);
|
|
258
275
|
const refinementResult = isDataTypeProfile
|
|
259
|
-
? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map() }
|
|
260
|
-
: buildRefinements(allFields, rootPrefix, valueSets);
|
|
276
|
+
? { refinements: [], supportedConstraints: [], unsupportedConstraints: [], valueSetImports: new Map(), invariantEvaluators: [] }
|
|
277
|
+
: buildRefinements(allFields, rootPrefix, valueSets, emitInvariants);
|
|
261
278
|
// Merge ValueSet imports from binding refinements
|
|
262
279
|
for (const [name, importPath] of refinementResult.valueSetImports) {
|
|
263
280
|
if (!valueSetImports.has(name)) {
|
|
@@ -277,7 +294,18 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
|
|
|
277
294
|
const headerIndex = lines.indexOf(`import { z } from "zod";`);
|
|
278
295
|
lines.splice(headerIndex + 1, 0, `import { ${usedHelpers.join(', ')} } from "./ValidatorOptions.js";`);
|
|
279
296
|
}
|
|
297
|
+
// Invariant refinements evaluate FHIRPath synchronously, so the engine and the
|
|
298
|
+
// version's model have to be imported alongside the schema.
|
|
299
|
+
if (refinementResult.invariantEvaluators.length > 0) {
|
|
300
|
+
const headerIndex = lines.indexOf(`import { z } from "zod";`);
|
|
301
|
+
lines.splice(headerIndex + 1, 0, `import fhirpath from "fhirpath";`, `import fhirpath_model from "fhirpath/fhir-context/${versionSlug()}/index.js";`);
|
|
302
|
+
}
|
|
280
303
|
lines.push('');
|
|
304
|
+
// Compiled invariant evaluators, hoisted above the schema so each FHIRPath
|
|
305
|
+
// expression is parsed once at import instead of on every parse.
|
|
306
|
+
if (refinementResult.invariantEvaluators.length > 0) {
|
|
307
|
+
lines.push(...refinementResult.invariantEvaluators, '');
|
|
308
|
+
}
|
|
281
309
|
// Emit schema
|
|
282
310
|
const schemaName = `${interfaceName}Schema`;
|
|
283
311
|
lines.push(`export const ${schemaName} =`);
|
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import { expandValueSetsFromTx, addPlaceholdersForExternalBindings } from './parser/vsParser.js';
|
|
8
8
|
import { generateValueSetTypeScript, generateValueSetRegistry, assignValueSetNames } from './emitters/valueset/valueSetGenerator.js';
|
|
9
|
+
import { generateNamingSystemTypeScript, generateNamingSystemRegistry, assignNamingSystemNames } from './emitters/namingsystem/namingSystemGenerator.js';
|
|
9
10
|
import { collectValueSetBindingUrls, clearLocalStructureDefinitions } from './parser/sdParser.js';
|
|
10
11
|
import { ensureDirectoryExists, sanitizeIdentifier, toPascalCase } from './core/utils.js';
|
|
11
12
|
import { getCacheConfig, getFhirPackagesCacheDir } from './core/cacheConfig.js';
|
|
@@ -183,6 +184,12 @@ export async function expandValueSetsWithTx(valueSets, valueSetCodesMap, structu
|
|
|
183
184
|
* Returns whether any files were generated. Handles stale directory cleanup.
|
|
184
185
|
*/
|
|
185
186
|
export function emitValueSetFiles(valueSets, outputDir) {
|
|
187
|
+
const generated = writeValueSetFiles(valueSets, outputDir);
|
|
188
|
+
if (!generated)
|
|
189
|
+
cleanupStaleSubdir(outputDir, 'valuesets');
|
|
190
|
+
return generated;
|
|
191
|
+
}
|
|
192
|
+
function writeValueSetFiles(valueSets, outputDir) {
|
|
186
193
|
if (valueSets.size === 0)
|
|
187
194
|
return false;
|
|
188
195
|
const valueSetOutputDir = path.join(outputDir, 'valuesets');
|
|
@@ -282,17 +289,62 @@ export function enrichValueSetsFromCodeMap(valueSets, valueSetCodesMap, bindingU
|
|
|
282
289
|
logger.log(`ValueSet enrichment: ${enrichedCount} enriched, ${createdCount} created from CodeSystem resolution`);
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Generate TypeScript files for NamingSystems and write them to disk.
|
|
294
|
+
*
|
|
295
|
+
* Mirrors {@link emitValueSetFiles}: an IG's declared identifier systems become
|
|
296
|
+
* typed constants rather than literals retyped by every consumer.
|
|
297
|
+
* Returns whether any files were generated. Handles stale directory cleanup.
|
|
298
|
+
*/
|
|
299
|
+
export function emitNamingSystemFiles(namingSystems, outputDir) {
|
|
300
|
+
const generated = writeNamingSystemFiles(namingSystems, outputDir);
|
|
301
|
+
if (!generated)
|
|
302
|
+
cleanupStaleSubdir(outputDir, 'namingsystems');
|
|
303
|
+
return generated;
|
|
304
|
+
}
|
|
305
|
+
function writeNamingSystemFiles(namingSystems, outputDir) {
|
|
306
|
+
if (namingSystems.length === 0)
|
|
307
|
+
return false;
|
|
308
|
+
const nsOutputDir = path.join(outputDir, 'namingsystems');
|
|
309
|
+
let generatedCount = 0;
|
|
310
|
+
const assigned = assignNamingSystemNames(namingSystems);
|
|
311
|
+
for (const { namingSystem, sanitized } of assigned) {
|
|
312
|
+
try {
|
|
313
|
+
if (generatedCount === 0) {
|
|
314
|
+
ensureDirectoryExists(nsOutputDir);
|
|
315
|
+
}
|
|
316
|
+
const { filename, content } = generateNamingSystemTypeScript(namingSystem, { nameOverride: sanitized });
|
|
317
|
+
fs.writeFileSync(path.join(nsOutputDir, filename), content, 'utf-8');
|
|
318
|
+
generatedCount++;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
log.error(`Failed to generate NamingSystem ${namingSystem.name}:`, err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (generatedCount === 0)
|
|
325
|
+
return false;
|
|
326
|
+
const registryContent = generateNamingSystemRegistry(namingSystems);
|
|
327
|
+
fs.writeFileSync(path.join(nsOutputDir, 'index.ts'), registryContent, 'utf-8');
|
|
328
|
+
logger.log(`Generated ${generatedCount} NamingSystem files in ${nsOutputDir}`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Remove a generated subdirectory left over from an earlier run.
|
|
333
|
+
*
|
|
334
|
+
* A cached extraction can keep a `valuesets/`/`namingsystems/` folder whose
|
|
335
|
+
* source resources are gone; leaving it behind makes the barrel export a module
|
|
336
|
+
* nothing regenerates.
|
|
337
|
+
*/
|
|
338
|
+
function cleanupStaleSubdir(outputDir, subdir) {
|
|
339
|
+
const dir = path.join(outputDir, subdir);
|
|
288
340
|
try {
|
|
289
|
-
if (fs.existsSync(
|
|
290
|
-
fs.rmSync(
|
|
291
|
-
log.debug(`Removed stale
|
|
341
|
+
if (fs.existsSync(dir)) {
|
|
342
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
343
|
+
log.debug(`Removed stale ${subdir} directory: ${dir}`);
|
|
292
344
|
}
|
|
293
345
|
}
|
|
294
346
|
catch (e) {
|
|
295
|
-
log.debug(`Failed to remove stale
|
|
347
|
+
log.debug(`Failed to remove stale ${subdir} directory ${dir}: ${e.message}`);
|
|
296
348
|
}
|
|
297
349
|
}
|
|
298
350
|
/** Initialize logger and clear local StructureDefinitions for a generation run. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded, readSearchParametersFromDir } from './parser/packageParser.js';
|
|
4
|
+
import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded, readSearchParametersFromDir, readNamingSystemsFromDir } from './parser/packageParser.js';
|
|
5
5
|
import { fetchStructureDefinitions, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './parser/sdParser.js';
|
|
6
6
|
import { ensureDirectoryExists, downloadFile } from './core/utils.js';
|
|
7
7
|
import { getFhirPackagesCacheDir } from './core/cacheConfig.js';
|
|
@@ -13,7 +13,7 @@ import { DEFAULT_FHIR_VERSION } from './fhir/types.js';
|
|
|
13
13
|
import { FHIR_VERSIONS } from './fhir/versionRegistry.js';
|
|
14
14
|
import { ensureCorePackage } from './fhir/corePackageResolver.js';
|
|
15
15
|
import { spawn } from 'child_process';
|
|
16
|
-
import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles,
|
|
16
|
+
import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, emitNamingSystemFiles, initGenerationContext, enrichValueSetsFromCodeMap, collectReferencedDependencyProfiles, } from './generationHelpers.js';
|
|
17
17
|
/** Resolve the babelfhir-ts CLI version from its own package.json (used to stamp generated packages). */
|
|
18
18
|
const __generatorDir = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const _generatorPkgPath = [
|
|
@@ -218,7 +218,62 @@ async function compileTypeScriptToJS(dir) {
|
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Output directories that only some generation runs produce.
|
|
223
|
+
*
|
|
224
|
+
* Single source of truth for both the generated `package.json` exports map and
|
|
225
|
+
* the root index barrel. These were three separate lists — two identical
|
|
226
|
+
* `exports` blocks plus a hardcoded check in the barrel writer — so registering
|
|
227
|
+
* an emitter meant remembering all three, and reaching a second entry module
|
|
228
|
+
* meant naming it literally (`./dicomweb/cornerstone`). `modules` covers any
|
|
229
|
+
* module a folder emits, so nothing here names one.
|
|
230
|
+
*/
|
|
231
|
+
const OPTIONAL_OUTPUT_DIRS = [
|
|
232
|
+
{ dir: 'fhir-client', barrel: true },
|
|
233
|
+
// dicomweb writes a second entry module (cornerstone) beside its barrel.
|
|
234
|
+
{ dir: 'dicomweb', barrel: true, modules: true },
|
|
235
|
+
{ dir: 'prefab', barrel: true },
|
|
236
|
+
{ dir: 'valuesets', modules: true, rootReExport: true },
|
|
237
|
+
{ dir: 'namingsystems', modules: true, rootReExport: true },
|
|
238
|
+
];
|
|
239
|
+
/**
|
|
240
|
+
* Add subpath exports for the optional output directories that were created.
|
|
241
|
+
*
|
|
242
|
+
* Shared by generateIntoPackage() and generateIntoPackageDirect(), which
|
|
243
|
+
* previously carried identical copies of this block.
|
|
244
|
+
*/
|
|
245
|
+
function addOptionalSubpathExports(outputDir, generatedExports) {
|
|
246
|
+
for (const { dir, barrel, modules } of OPTIONAL_OUTPUT_DIRS) {
|
|
247
|
+
if (!fs.existsSync(path.join(outputDir, dir)))
|
|
248
|
+
continue;
|
|
249
|
+
if (barrel) {
|
|
250
|
+
generatedExports[`./${dir}`] = { types: `./${dir}/index.d.ts`, import: `./${dir}/index.js` };
|
|
251
|
+
}
|
|
252
|
+
if (modules) {
|
|
253
|
+
generatedExports[`./${dir}/*`] = { types: `./${dir}/*.d.ts`, import: `./${dir}/*.js` };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Put the extraction root's `package.json` back the way it was found.
|
|
259
|
+
*
|
|
260
|
+
* The generated manifest is staged there only so `createPackageFromDir` puts it
|
|
261
|
+
* at the tarball root; it must not outlive the repack when the extraction root
|
|
262
|
+
* is a shared cache entry.
|
|
263
|
+
*/
|
|
264
|
+
export function restoreRootManifest(rootPackageJsonPath, prior) {
|
|
265
|
+
try {
|
|
266
|
+
if (prior) {
|
|
267
|
+
fs.writeFileSync(rootPackageJsonPath, prior);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
fs.rmSync(rootPackageJsonPath, { force: true });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
log.warn(`Could not restore ${rootPackageJsonPath}: ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
222
277
|
async function generateIndexFile(dir) {
|
|
223
278
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
224
279
|
const exports = [];
|
|
@@ -279,10 +334,11 @@ async function generateIndexFile(dir) {
|
|
|
279
334
|
}
|
|
280
335
|
}
|
|
281
336
|
}
|
|
282
|
-
//
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
337
|
+
// Re-export the optional subfolders that opt into it (see OPTIONAL_OUTPUT_DIRS)
|
|
338
|
+
for (const { dir: subdir } of OPTIONAL_OUTPUT_DIRS.filter(d => d.rootReExport)) {
|
|
339
|
+
if (fs.existsSync(path.join(dir, subdir))) {
|
|
340
|
+
exports.push(`export * from './${subdir}/index.js';`);
|
|
341
|
+
}
|
|
286
342
|
}
|
|
287
343
|
// Export shared ValidatorOptions type
|
|
288
344
|
if (fs.existsSync(path.join(dir, 'ValidatorOptions.ts'))) {
|
|
@@ -450,10 +506,8 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
450
506
|
ensureDirectoryExists(outputDir);
|
|
451
507
|
// Generate TypeScript files for ValueSets inside the embedded package
|
|
452
508
|
endPhase = startPhase('generate');
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
cleanupStaleValueSetDir(outputDir);
|
|
456
|
-
}
|
|
509
|
+
emitValueSetFiles(valueSets, outputDir);
|
|
510
|
+
emitNamingSystemFiles(readNamingSystemsFromDir(extractedRoot), outputDir);
|
|
457
511
|
// Build profile registries from BOTH main and dependency SDs so delegation can resolve names
|
|
458
512
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allSDs, fhirInterfaceNames);
|
|
459
513
|
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
@@ -543,20 +597,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
543
597
|
const { finalizePrefabOutput } = await import('./emitters/prefab/prefabEmitter.js');
|
|
544
598
|
finalizePrefabOutput(outputDir, { prefabStylesPath: flags.prefabStyles });
|
|
545
599
|
}
|
|
546
|
-
|
|
547
|
-
if (fs.existsSync(path.join(outputDir, 'fhir-client'))) {
|
|
548
|
-
generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
|
|
549
|
-
}
|
|
550
|
-
if (fs.existsSync(path.join(outputDir, 'dicomweb'))) {
|
|
551
|
-
generatedExports['./dicomweb'] = { types: './dicomweb/index.d.ts', import: './dicomweb/index.js' };
|
|
552
|
-
generatedExports['./dicomweb/cornerstone'] = { types: './dicomweb/cornerstone.d.ts', import: './dicomweb/cornerstone.js' };
|
|
553
|
-
}
|
|
554
|
-
if (fs.existsSync(path.join(outputDir, 'prefab'))) {
|
|
555
|
-
generatedExports['./prefab'] = { types: './prefab/index.d.ts', import: './prefab/index.js' };
|
|
556
|
-
}
|
|
557
|
-
if (fs.existsSync(path.join(outputDir, 'valuesets'))) {
|
|
558
|
-
generatedExports['./valuesets/*'] = { types: './valuesets/*.d.ts', import: './valuesets/*.js' };
|
|
559
|
-
}
|
|
600
|
+
addOptionalSubpathExports(outputDir, generatedExports);
|
|
560
601
|
const generatedPackageJsonPath = path.join(outputDir, 'package.json');
|
|
561
602
|
fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
|
|
562
603
|
logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
|
|
@@ -611,14 +652,30 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
611
652
|
fs.rmdirSync(nodeModulesDir);
|
|
612
653
|
}
|
|
613
654
|
catch { /* not empty or doesn't exist — fine */ }
|
|
614
|
-
// Copy package.json to root of extractedRoot so it's at the tarball root
|
|
655
|
+
// Copy package.json to root of extractedRoot so it's at the tarball root.
|
|
656
|
+
//
|
|
657
|
+
// extractedRoot is the shared FHIR cache entry whenever the package was
|
|
658
|
+
// already cached (see shouldCleanup), so this file is not ours to keep: a
|
|
659
|
+
// package installed flat — no `package/` subdir, as `babelfhir-ts install`
|
|
660
|
+
// produces for a locally built IG — keeps its own manifest right here, and
|
|
661
|
+
// leaving the generated one behind makes the next run read `@types/fhir`
|
|
662
|
+
// and friends as FHIR package specs. Restore the previous state once the
|
|
663
|
+
// archive is written.
|
|
615
664
|
const rootPackageJsonPath = path.join(extractedRoot, 'package.json');
|
|
665
|
+
const priorRootManifest = fs.existsSync(rootPackageJsonPath)
|
|
666
|
+
? fs.readFileSync(rootPackageJsonPath)
|
|
667
|
+
: undefined;
|
|
616
668
|
fs.copyFileSync(generatedPackageJsonPath, rootPackageJsonPath);
|
|
617
669
|
logger.log('Copied package.json to tarball root');
|
|
618
670
|
const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
|
|
619
671
|
endPhase = startPhase('repack');
|
|
620
|
-
|
|
621
|
-
|
|
672
|
+
try {
|
|
673
|
+
await createPackageFromDir(extractedRoot, finalArchive);
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
endPhase(); // repack
|
|
677
|
+
restoreRootManifest(rootPackageJsonPath, priorRootManifest);
|
|
678
|
+
}
|
|
622
679
|
// Write timing summary to output directory
|
|
623
680
|
const timing = getTimingSummary();
|
|
624
681
|
fs.writeFileSync(path.join(outputDir, 'generation-timing.json'), JSON.stringify(timing, null, 2));
|
|
@@ -692,9 +749,8 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
692
749
|
}
|
|
693
750
|
ensureDirectoryExists(outputDir);
|
|
694
751
|
endPhase = startPhase('generate');
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
cleanupStaleValueSetDir(outputDir);
|
|
752
|
+
emitValueSetFiles(valueSets, outputDir);
|
|
753
|
+
emitNamingSystemFiles(readNamingSystemsFromDir(extractedRoot), outputDir);
|
|
698
754
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allSDs, fhirInterfaceNames);
|
|
699
755
|
registerLocalStructureDefinitions(allSDs);
|
|
700
756
|
// PackageManager already loaded and registered dependency SDs above —
|
|
@@ -775,19 +831,7 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
775
831
|
finalizePrefabOutput(outputDir, { prefabStylesPath: flags.prefabStyles });
|
|
776
832
|
}
|
|
777
833
|
// Only add subpath exports if their directories were actually created
|
|
778
|
-
|
|
779
|
-
generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
|
|
780
|
-
}
|
|
781
|
-
if (fs.existsSync(path.join(outputDir, 'dicomweb'))) {
|
|
782
|
-
generatedExports['./dicomweb'] = { types: './dicomweb/index.d.ts', import: './dicomweb/index.js' };
|
|
783
|
-
generatedExports['./dicomweb/cornerstone'] = { types: './dicomweb/cornerstone.d.ts', import: './dicomweb/cornerstone.js' };
|
|
784
|
-
}
|
|
785
|
-
if (fs.existsSync(path.join(outputDir, 'prefab'))) {
|
|
786
|
-
generatedExports['./prefab'] = { types: './prefab/index.d.ts', import: './prefab/index.js' };
|
|
787
|
-
}
|
|
788
|
-
if (fs.existsSync(path.join(outputDir, 'valuesets'))) {
|
|
789
|
-
generatedExports['./valuesets/*'] = { types: './valuesets/*.d.ts', import: './valuesets/*.js' };
|
|
790
|
-
}
|
|
834
|
+
addOptionalSubpathExports(outputDir, generatedExports);
|
|
791
835
|
const generatedPackageJsonPath = path.join(outputDir, 'package.json');
|
|
792
836
|
fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
|
|
793
837
|
logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
|
|
@@ -918,9 +962,8 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
918
962
|
}
|
|
919
963
|
// Generate TypeScript files for ValueSets
|
|
920
964
|
endPhase = startPhase('generate');
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
965
|
+
emitValueSetFiles(valueSets, outputDir);
|
|
966
|
+
emitNamingSystemFiles(readNamingSystemsFromDir(inputDir), outputDir);
|
|
924
967
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName, profileUrlToType } = buildProfileRegistries(allLocalSDs, fhirInterfaceNames);
|
|
925
968
|
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
926
969
|
registerLocalStructureDefinitions(allLocalSDs);
|
|
@@ -498,6 +498,20 @@ export function readValueSetCodesFromDir(extractedRoot, codeSystemMap) {
|
|
|
498
498
|
}
|
|
499
499
|
return map;
|
|
500
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Whether a `dependencies` entry names a FHIR package at a concrete version.
|
|
503
|
+
*
|
|
504
|
+
* A FHIR IG manifest lists unscoped names pinned to exact versions. An npm
|
|
505
|
+
* scope or a semver range means the manifest being read is not an IG's — and
|
|
506
|
+
* the cache path built from it is unwritable (`<cache>/@types/fhir->=0.0.44
|
|
507
|
+
* <0.1.0.tgz`), which surfaced as an unhandled rejection from the download
|
|
508
|
+
* stream and took the whole run down instead of skipping one entry.
|
|
509
|
+
*/
|
|
510
|
+
export function isFhirPackageSpec(depName, depVersion) {
|
|
511
|
+
if (depName.startsWith('@'))
|
|
512
|
+
return false;
|
|
513
|
+
return /^[A-Za-z0-9][A-Za-z0-9.\-+]*$/.test(depVersion);
|
|
514
|
+
}
|
|
501
515
|
/**
|
|
502
516
|
* Recursively ensure all dependency packages are downloaded and extracted.
|
|
503
517
|
* Walks the dependency tree from an extracted package's package.json,
|
|
@@ -508,6 +522,10 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
|
|
|
508
522
|
ensureCacheDir(cacheDir);
|
|
509
523
|
const visited = new Set();
|
|
510
524
|
async function downloadAndExtract(depName, depVersion) {
|
|
525
|
+
if (!isFhirPackageSpec(depName, depVersion)) {
|
|
526
|
+
log.warn(`Ignoring dependency that is not a FHIR package spec: ${depName}@${depVersion}`);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
511
529
|
const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
|
|
512
530
|
// Already cached (@ separator)?
|
|
513
531
|
if (fs.existsSync(depDir) && fs.readdirSync(depDir).length > 0) {
|
|
@@ -824,6 +842,64 @@ export function readValueSetsFromDir(extractedRoot) {
|
|
|
824
842
|
}
|
|
825
843
|
return map;
|
|
826
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Read all NamingSystem resources from an extracted package directory.
|
|
847
|
+
*
|
|
848
|
+
* Entries with no `uniqueId` are dropped: there is nothing to emit for them.
|
|
849
|
+
*/
|
|
850
|
+
export function readNamingSystemsFromDir(extractedRoot) {
|
|
851
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'NamingSystem-');
|
|
852
|
+
const found = [];
|
|
853
|
+
let skippedRetired = 0;
|
|
854
|
+
let skippedNoId = 0;
|
|
855
|
+
for (const file of jsonFiles) {
|
|
856
|
+
try {
|
|
857
|
+
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
858
|
+
if (content.resourceType !== 'NamingSystem')
|
|
859
|
+
continue;
|
|
860
|
+
if (content.status === 'retired') {
|
|
861
|
+
skippedRetired++;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const uniqueIds = (Array.isArray(content.uniqueId) ? content.uniqueId : [])
|
|
865
|
+
.filter((u) => typeof u?.value === 'string' && u.value.length > 0)
|
|
866
|
+
.map((u) => ({
|
|
867
|
+
type: u.type || 'other',
|
|
868
|
+
value: u.value,
|
|
869
|
+
...(u.preferred !== undefined && { preferred: u.preferred }),
|
|
870
|
+
...(u.comment && { comment: u.comment }),
|
|
871
|
+
}));
|
|
872
|
+
// Destructured rather than length-checked: this is what proves the tuple
|
|
873
|
+
// shape to the compiler, so the emitters need no undefined handling.
|
|
874
|
+
const [firstId, ...restIds] = uniqueIds;
|
|
875
|
+
if (!firstId) {
|
|
876
|
+
skippedNoId++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const name = content.name || content.title || content.id;
|
|
880
|
+
if (!name) {
|
|
881
|
+
skippedNoId++;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
found.push({
|
|
885
|
+
name,
|
|
886
|
+
...(content.title && { title: content.title }),
|
|
887
|
+
...(content.description && { description: content.description }),
|
|
888
|
+
...(content.kind && { kind: content.kind }),
|
|
889
|
+
uniqueIds: [firstId, ...restIds],
|
|
890
|
+
});
|
|
891
|
+
log.debug(`Loaded NamingSystem: ${name} (${uniqueIds.length} uniqueId entries)`);
|
|
892
|
+
}
|
|
893
|
+
catch (err) {
|
|
894
|
+
log.debug(`Skipping invalid JSON while reading NamingSystems ${file}: ${err.message}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Say what was dropped: a silently shorter list reads as "the IG declares none".
|
|
898
|
+
if (skippedRetired > 0 || skippedNoId > 0) {
|
|
899
|
+
log.info(`NamingSystems: emitting ${found.length}, skipped ${skippedRetired} retired and ${skippedNoId} without a usable uniqueId`);
|
|
900
|
+
}
|
|
901
|
+
return found;
|
|
902
|
+
}
|
|
827
903
|
/**
|
|
828
904
|
* Read all SearchParameter resources from a package directory.
|
|
829
905
|
* Returns a map of base resource type → array of search parameter definitions.
|
|
@@ -118,7 +118,9 @@ export function getUniformSystem(valueSet) {
|
|
|
118
118
|
if (valueSet.concepts.length === 0) {
|
|
119
119
|
return null;
|
|
120
120
|
}
|
|
121
|
-
const firstSystem = valueSet.concepts[0]
|
|
121
|
+
const firstSystem = valueSet.concepts[0]?.system;
|
|
122
|
+
if (firstSystem === undefined)
|
|
123
|
+
return null;
|
|
122
124
|
const allSameSystem = valueSet.concepts.every(c => c.system === firstSystem);
|
|
123
125
|
return allSameSystem ? firstSystem : null;
|
|
124
126
|
}
|
|
@@ -222,7 +224,7 @@ export function addPlaceholdersForExternalBindings(valueSets, bindingUrls, fhirC
|
|
|
222
224
|
function findValueSetInCache(canonicalUrl, fhirCacheDir) {
|
|
223
225
|
// Derive the expected filename from the canonical URL
|
|
224
226
|
// e.g. "http://hl7.org/fhir/ValueSet/mimetypes" → "ValueSet-mimetypes.json"
|
|
225
|
-
const urlName = canonicalUrl.split('|')[0].split('/').pop();
|
|
227
|
+
const urlName = (canonicalUrl.split('|')[0] ?? canonicalUrl).split('/').pop();
|
|
226
228
|
if (!urlName)
|
|
227
229
|
return undefined;
|
|
228
230
|
const expectedFilename = `ValueSet-${urlName}.json`;
|