babelfhir-ts 1.2.0 → 1.2.1

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.
@@ -5,7 +5,7 @@ import { versionSlug } from '../../fhir/versionContext.js';
5
5
  import { buildRequiredConstraints, buildMaxCardinalityConstraints, buildForbiddenFieldConstraintEntries, buildPatternValidations, } from './validatorConstraintBuilders.js';
6
6
  import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, } from './validatorFieldBuilders.js';
7
7
  import { buildBindingValidations } from './validatorBindingBuilder.js';
8
- import { generateBundleRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
8
+ import { generateBundleRefValidation, generateContainedRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
9
9
  const log = logger.withTag('validator');
10
10
  /**
11
11
  * Returns the content of ValidatorOptions.ts — the shared runtime options type
@@ -146,6 +146,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
146
146
  });
147
147
  const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
148
148
  const bundleRefValidation = generateBundleRefValidation(baseResourceType);
149
+ const containedRefValidation = generateContainedRefValidation(baseResourceType);
149
150
  const extensionStructuralValidation = generateExtensionStructuralValidation();
150
151
  // ── Deduplicate & filter constraints ────────────────────────────────────
151
152
  const uniqueConstraints = Array.from(new Map(constraintsWithContext.map((item) => [item.constraint.expression + "|" + item.fieldPath, item])).values());
@@ -229,7 +230,7 @@ export async function validate${interfaceName}(resource: ${interfaceName}, optio
229
230
  const errors: string[] = [];
230
231
  const warnings: string[] = [];
231
232
  void options;
232
- ${extensionStructuralValidation}
233
+ ${extensionStructuralValidation}${containedRefValidation}
233
234
  return { errors, warnings };
234
235
  }`,
235
236
  valueSetImports
@@ -265,7 +266,7 @@ ${extensionStructuralValidation}
265
266
  const errors: string[] = [];
266
267
  const warnings: string[] = [];
267
268
  ${fhirpathOptionsBlock}
268
- ${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${bundleRefValidation}
269
+ ${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${containedRefValidation}${bundleRefValidation}
269
270
  return { errors, warnings };
270
271
  }`,
271
272
  valueSetImports
@@ -31,7 +31,7 @@ export function generateBundleRefValidation(baseResourceType) {
31
31
  const ref = rec.reference as string;
32
32
  if (ref.startsWith('urn:uuid:') || ref.startsWith('urn:oid:')) {
33
33
  if (!_fullUrls.has(ref)) {
34
- errors.push('Bundled or contained reference not found within the bundle/resource ' + ref);
34
+ errors.push('Bundle reference not found: ' + ref);
35
35
  }
36
36
  } else if (/^[A-Za-z]+\\//.test(ref)) {
37
37
  if (!_resIds.has(ref)) {
@@ -40,7 +40,7 @@ export function generateBundleRefValidation(baseResourceType) {
40
40
  if (_fu.endsWith('/' + ref) || _fu.endsWith(ref)) { _found = true; break; }
41
41
  }
42
42
  if (!_found) {
43
- errors.push('Bundled or contained reference not found within the bundle/resource ' + ref);
43
+ errors.push('Bundle reference not found: ' + ref);
44
44
  }
45
45
  }
46
46
  }
@@ -56,6 +56,46 @@ export function generateBundleRefValidation(baseResourceType) {
56
56
  }
57
57
  `;
58
58
  }
59
+ /**
60
+ * Generate standalone contained reference resolution validation code.
61
+ * For non-Bundle resources, verifies that #fragment references resolve to contained[] entries.
62
+ * Bundle validation is handled separately by generateBundleRefValidation.
63
+ */
64
+ export function generateContainedRefValidation(baseResourceType) {
65
+ // Bundle resources have their own reference resolution check; skip them here
66
+ if (baseResourceType === 'Bundle')
67
+ return '';
68
+ return `
69
+ // Standalone contained reference resolution: verify #id references resolve to contained[] entries
70
+ {
71
+ const _res = resource as unknown as Record<string, unknown>;
72
+ const _containedIds = new Set<string>();
73
+ if (Array.isArray(_res.contained)) {
74
+ for (const _c of _res.contained as Array<Record<string, unknown>>) {
75
+ if (_c && typeof _c.id === 'string') _containedIds.add(_c.id);
76
+ }
77
+ }
78
+ const _checkContainedRef = (obj: unknown): void => {
79
+ if (!obj || typeof obj !== 'object') return;
80
+ if (Array.isArray(obj)) { obj.forEach(_checkContainedRef); return; }
81
+ const _rec = obj as Record<string, unknown>;
82
+ if (typeof _rec.reference === 'string') {
83
+ const _ref = _rec.reference as string;
84
+ if (_ref.startsWith('#')) {
85
+ const _id = _ref.substring(1);
86
+ if (_id && !_containedIds.has(_id)) {
87
+ errors.push('Contained reference not found: ' + _ref);
88
+ }
89
+ }
90
+ }
91
+ for (const [_k, _v] of Object.entries(_rec)) {
92
+ if (_k !== 'contained') _checkContainedRef(_v);
93
+ }
94
+ };
95
+ _checkContainedRef(_res);
96
+ }
97
+ `;
98
+ }
59
99
  /**
60
100
  * Generate extension structural validation code.
61
101
  * Checks extension.url required, ext-1 constraint, empty objects.
@@ -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, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded } from './parser/packageParser.js';
4
+ import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded } 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';
@@ -235,6 +235,9 @@ export async function generate(fhirSource, outputDir, flags) {
235
235
  structureDefinitions = readStructureDefinitionsFromDir(extracted);
236
236
  // Ensure dependency packages are downloaded before loading ValueSets
237
237
  await ensureDependenciesDownloaded(extracted);
238
+ // Register SDs from dependency packages so external profile resolution can find them
239
+ const depSDs = readStructureDefinitionsFromDependencies(extracted);
240
+ registerLocalStructureDefinitions(depSDs);
238
241
  // Load ValueSets from package AND its dependencies (for binding resolution)
239
242
  valueSetCodesMap = readValueSetCodesWithDependencies(extracted);
240
243
  valueSets = readValueSetsFromDir(extracted);
@@ -326,6 +329,9 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
326
329
  const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
327
330
  // Register all local StructureDefinitions for resolution before HTTP fetches
328
331
  registerLocalStructureDefinitions(structureDefinitions);
332
+ // Register SDs from dependency packages so external profile resolution can find them
333
+ const depSDs = readStructureDefinitionsFromDependencies(extractedRoot);
334
+ registerLocalStructureDefinitions(depSDs);
329
335
  for (const sd of structureDefinitions) {
330
336
  await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
331
337
  }
@@ -390,11 +396,21 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
390
396
  // Install fhirpath type stub so tsc can resolve validator imports
391
397
  const { installFhirpathStub, removeFhirpathStub } = await import('./emitters/validator/fhirpathStubInstaller.js');
392
398
  installFhirpathStub(outputDir);
399
+ // Install minimal @types/node stub so tsc can resolve `import { createRequire } from 'module'`
400
+ const nodeTypesDir = path.join(outputDir, 'node_modules', '@types', 'node');
401
+ fs.mkdirSync(nodeTypesDir, { recursive: true });
402
+ fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
403
+ fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
393
404
  // Compile TypeScript to JavaScript
394
405
  logger.log('Compiling TypeScript to JavaScript...');
395
406
  await compileTypeScriptToJS(outputDir);
396
407
  // Remove fhirpath stub — the real package is a peer dependency
397
408
  removeFhirpathStub(outputDir);
409
+ // Remove @types/node stub — only needed for compilation
410
+ try {
411
+ fs.rmSync(path.join(outputDir, 'node_modules', '@types'), { recursive: true, force: true });
412
+ }
413
+ catch { /* ignore */ }
398
414
  // Remove base client type stubs — they were only needed for tsc to resolve
399
415
  // @babelfhir-ts/client-<version> imports during compilation. The real package is
400
416
  // installed by the consumer via npm.
@@ -467,6 +483,9 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
467
483
  const valueSets = readValueSetsFromDir(inputDir);
468
484
  // Ensure dependency packages are downloaded before loading ValueSets
469
485
  await ensureDependenciesDownloaded(inputDir);
486
+ // Register SDs from dependency packages so external profile resolution can find them
487
+ const depSDs = readStructureDefinitionsFromDependencies(inputDir);
488
+ registerLocalStructureDefinitions(depSDs);
470
489
  const valueSetCodesMap = readValueSetCodesWithDependencies(inputDir);
471
490
  logger.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
472
491
  const entries = fs.readdirSync(inputDir);
@@ -521,6 +521,52 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
521
521
  }
522
522
  await walkDeps(extractedRoot);
523
523
  }
524
+ /**
525
+ * Reads StructureDefinitions from all dependency packages (recursively).
526
+ * Returns SDs from dependencies only — the main package SDs are loaded separately.
527
+ * This allows external profile resolution to find extension SDs from dependency packages
528
+ * (e.g., hl7.fhir.uv.extensions) without HTTP fetching.
529
+ */
530
+ export function readStructureDefinitionsFromDependencies(extractedRoot) {
531
+ const cacheDir = getFhirPackagesCacheDir();
532
+ const visited = new Set();
533
+ const allSDs = [];
534
+ function walkDeps(pkgDir, isRoot) {
535
+ const pkgDirNorm = path.normalize(pkgDir);
536
+ if (visited.has(pkgDirNorm))
537
+ return;
538
+ visited.add(pkgDirNorm);
539
+ // Read SDs from this package (skip the root — those are loaded separately)
540
+ if (!isRoot) {
541
+ const sds = readStructureDefinitionsFromDir(pkgDir);
542
+ allSDs.push(...sds);
543
+ }
544
+ // Find package.json and recurse into dependencies
545
+ const packageJsonPath = path.join(pkgDir, 'package', 'package.json');
546
+ const altPath = path.join(pkgDir, 'package.json');
547
+ const pkgJsonPath = fs.existsSync(packageJsonPath) ? packageJsonPath
548
+ : fs.existsSync(altPath) ? altPath : null;
549
+ if (!pkgJsonPath)
550
+ return;
551
+ try {
552
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
553
+ const deps = pkgJson.dependencies || {};
554
+ for (const [depName, depVersion] of Object.entries(deps)) {
555
+ for (const sep of ['@', '#']) {
556
+ const depDir = path.join(cacheDir, `${depName}${sep}${depVersion}`);
557
+ if (fs.existsSync(depDir)) {
558
+ walkDeps(depDir, false);
559
+ break;
560
+ }
561
+ }
562
+ }
563
+ }
564
+ catch { /* skip */ }
565
+ }
566
+ walkDeps(extractedRoot, true);
567
+ log.info(`Loaded ${allSDs.length} StructureDefinitions from dependency packages`);
568
+ return allSDs;
569
+ }
524
570
  /**
525
571
  * Reads ValueSet codes from a package and all its dependencies (recursively).
526
572
  * Looks for dependency packages in the FHIR package cache directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",