@@ -6,7 +6,7 @@ import YAML from 'yaml';
66import semver from 'semver' ;
77import { execSync } from 'child_process' ;
88import { YAMLMap , Collection } from 'yaml/types' ;
9- import { isPlainObject , padEnd , startCase , sortBy , upperFirst } from 'lodash' ;
9+ import { isPlainObject , padEnd , startCase , sortBy , upperFirst , isEqual , uniqWith } from 'lodash' ;
1010import { EOL } from 'os' ;
1111import table from 'text-table' ;
1212import { OptionValues } from 'commander' ;
@@ -34,10 +34,16 @@ const EXT_PKG_TO_FHIR_PKG_MAP: { [key: string]: string } = {
3434 'hl7.fhir.extensions.r5' : 'hl7.fhir.r5.core#5.0.0'
3535} ;
3636
37+ export enum AutomaticDependencyPriority {
38+ Low = 'Low' , // load before configured dependencies / FHIR core (lowest resolution priority)
39+ High = 'High' // load after configured dependencies / FHIR core (highest resolution priority)
40+ }
41+
3742type AutomaticDependency = {
3843 packageId : string ;
3944 version : string ;
4045 fhirVersions ?: FHIRVersionName [ ] ;
46+ priority : AutomaticDependencyPriority ;
4147} ;
4248
4349type FshFhirMapping = {
@@ -54,35 +60,54 @@ export const AUTOMATIC_DEPENDENCIES: AutomaticDependency[] = [
5460 {
5561 packageId : 'hl7.fhir.uv.tools.r4' ,
5662 version : 'latest' ,
57- fhirVersions : [ 'R4' , 'R4B' ]
63+ fhirVersions : [ 'R4' , 'R4B' ] ,
64+ priority : AutomaticDependencyPriority . Low
5865 } ,
5966 {
6067 packageId : 'hl7.fhir.uv.tools.r5' ,
6168 version : 'latest' ,
62- fhirVersions : [ 'R5' , 'R6' ]
69+ fhirVersions : [ 'R5' , 'R6' ] ,
70+ priority : AutomaticDependencyPriority . Low
6371 } ,
6472 {
6573 packageId : 'hl7.terminology.r4' ,
6674 version : 'latest' ,
67- fhirVersions : [ 'R4' , 'R4B' ]
75+ fhirVersions : [ 'R4' , 'R4B' ] ,
76+ priority : AutomaticDependencyPriority . Low
6877 } ,
6978 {
7079 packageId : 'hl7.terminology.r5' ,
7180 version : 'latest' ,
72- fhirVersions : [ 'R5' , 'R6' ]
81+ fhirVersions : [ 'R5' , 'R6' ] ,
82+ priority : AutomaticDependencyPriority . Low
7383 } ,
7484 {
7585 packageId : 'hl7.fhir.uv.extensions.r4' ,
7686 version : 'latest' ,
77- fhirVersions : [ 'R4' , 'R4B' ]
87+ fhirVersions : [ 'R4' , 'R4B' ] ,
88+ priority : AutomaticDependencyPriority . High
7889 } ,
7990 {
8091 packageId : 'hl7.fhir.uv.extensions.r5' ,
8192 version : 'latest' ,
82- fhirVersions : [ 'R5' , 'R6' ]
93+ fhirVersions : [ 'R5' , 'R6' ] ,
94+ priority : AutomaticDependencyPriority . High
8395 }
8496] ;
8597
98+ function configuredDependencyMatchesAutomaticDependency (
99+ cd : ImplementationGuideDependsOn ,
100+ ad : AutomaticDependency
101+ ) {
102+ // hl7.some.package, hl7.some.package.r4, and hl7.some.package.r5 all represent the same content,
103+ // so they are essentially interchangeable and we should allow for any of them in the config.
104+ // See: https://chat.fhir.org/#narrow/stream/179239-tooling/topic/New.20Implicit.20Package/near/325488084
105+ const [ configRootId , packageRootId ] = [ cd . packageId , ad . packageId ] . map ( id =>
106+ / \. r [ 4 - 9 ] $ / . test ( id ) ? id . slice ( 0 , - 3 ) : id
107+ ) ;
108+ return configRootId === packageRootId ;
109+ }
110+
86111export function isSupportedFHIRVersion ( version : string ) : boolean {
87112 // For now, allow current or any 4.x/5.x/6.x version of FHIR except 4.0.0. This is a quick check; not a guarantee. If a user passes
88113 // in an invalid version that passes this test (e.g., 4.99.0), it is still expected to fail when we load dependencies.
@@ -366,23 +391,42 @@ export async function loadExternalDependencies(
366391 }
367392 dependencies . push ( { packageId : fhirVersionInfo . packageId , version : fhirVersionInfo . version } ) ;
368393
369- // Load automatic dependencies first so they have lowest priority in resolution
370- await loadAutomaticDependencies ( fhirVersionInfo . version , dependencies , defs ) ;
394+ // First load automatic dependencies with the lowest priority (before configured dependencies and FHIR core)
395+ await loadAutomaticDependencies (
396+ fhirVersionInfo . version ,
397+ dependencies ,
398+ defs ,
399+ AutomaticDependencyPriority . Low
400+ ) ;
371401
372- // Then load configured dependencies, with FHIR core last so it has highest priority in resolution
402+ // Then load configured dependencies and FHIR core (FHIR core is last so it has higher priority in resolution)
373403 await loadConfiguredDependencies ( dependencies , fhirVersionInfo . version , config . filePath , defs ) ;
404+
405+ // Then load automatic dependencies with highest priority (taking precedence over even FHIR core)
406+ // See: https://chat.fhir.org/#narrow/channel/179239-tooling/topic/New.20Implicit.20Package/near/562477575
407+ await loadAutomaticDependencies (
408+ fhirVersionInfo . version ,
409+ dependencies ,
410+ defs ,
411+ AutomaticDependencyPriority . High
412+ ) ;
374413}
375414
376415export async function loadAutomaticDependencies (
377416 fhirVersion : string ,
378417 configuredDependencies : ImplementationGuideDependsOn [ ] ,
379- defs : FHIRDefinitions
418+ defs : FHIRDefinitions ,
419+ priority : AutomaticDependencyPriority
380420) : Promise < void > {
381421 const fhirVersionName = getFHIRVersionInfo ( fhirVersion ) . name ;
382422
383- if ( fhirVersionName === 'R4' || fhirVersionName === 'R4B' ) {
423+ if (
424+ priority === AutomaticDependencyPriority . Low &&
425+ ( fhirVersionName === 'R4' || fhirVersionName === 'R4B' )
426+ ) {
384427 // There are several R5 resources that are allowed for use in R4 and R4B.
385- // Add them first so they're always available.
428+ // Add them first so they're always available (but are lower priority than
429+ // any other version loaded from an official package).
386430 const R5forR4Map = new Map < string , any > ( ) ;
387431 R5_DEFINITIONS_NEEDED_IN_R4 . forEach ( def => R5forR4Map . set ( def . id , def ) ) ;
388432 const virtualR5forR4Package = new InMemoryVirtualPackage (
@@ -397,46 +441,57 @@ export async function loadAutomaticDependencies(
397441 await defs . loadVirtualPackage ( virtualR5forR4Package ) ;
398442 }
399443
400- // Load dependencies serially so dependency loading order is predictable and repeatable
401- for ( const dep of AUTOMATIC_DEPENDENCIES ) {
402- // Skip dependencies not intended for this version of FHIR
403- if ( dep . fhirVersions && ! dep . fhirVersions . includes ( fhirVersionName ) ) {
404- continue ;
405- }
406- const alreadyConfigured = configuredDependencies . some ( cd => {
407- // hl7.some.package, hl7.some.package.r4, and hl7.some.package.r5 all represent the same content,
408- // so they are essentially interchangeable and we should allow for any of them in the config.
409- // See: https://chat.fhir.org/#narrow/stream/179239-tooling/topic/New.20Implicit.20Package/near/325488084
410- const [ configRootId , packageRootId ] = [ cd . packageId , dep . packageId ] . map ( id =>
411- / \. r [ 4 - 9 ] $ / . test ( id ) ? id . slice ( 0 , - 3 ) : id
412- ) ;
413- return configRootId === packageRootId ;
414- } ) ;
415- if ( ! alreadyConfigured ) {
416- let status : string ;
417- try {
418- // Suppress error logs when loading automatic dependencies because many IGs can succeed without them
444+ // Gather all automatic dependencies matching this priority, substituting matching configured dependencies where applicable
445+ const automaticDependencies = uniqWith (
446+ AUTOMATIC_DEPENDENCIES . filter ( ad => ad . priority === priority )
447+ . map ( autoDep => {
448+ const configuredDeps = configuredDependencies . filter ( configuredDep =>
449+ configuredDependencyMatchesAutomaticDependency ( configuredDep , autoDep )
450+ ) ;
451+ if ( configuredDeps . length ) {
452+ // Prefer configured dependencies over automatic dependencies
453+ return configuredDeps ;
454+ } else if ( autoDep . fhirVersions && ! autoDep . fhirVersions . includes ( fhirVersionName ) ) {
455+ // Skip automatic dependencies not intended for this version of FHIR
456+ return [ ] ;
457+ }
458+ return autoDep ;
459+ } )
460+ . flat ( ) ,
461+ isEqual
462+ ) ;
463+ // Load automatic dependencies serially so dependency loading order is predictable and repeatable
464+ for ( const dep of automaticDependencies ) {
465+ const isUserConfigured = ! AUTOMATIC_DEPENDENCIES . some (
466+ autoDep => autoDep . packageId === dep . packageId && autoDep . version === dep . version
467+ ) ;
468+ let status : string ;
469+ try {
470+ // Suppress error logs when loading non-configured automatic dependencies because many IGs can succeed without them
471+ if ( ! isUserConfigured ) {
419472 defs . setFHIRPackageLoaderLogInterceptor ( ( level : string ) => {
420473 return level !== 'error' ;
421474 } ) ;
422- status = await defs . loadPackage ( dep . packageId , dep . version ) ;
423- } catch ( e ) {
424- // This shouldn't happen, but just in case
425- status = 'FAILED' ;
426- if ( e . stack ) {
427- logger . debug ( e . stack ) ;
428- }
429- } finally {
430- // Unset the log interceptor so it behaves normally after this
475+ }
476+ status = await defs . loadPackage ( dep . packageId , dep . version ) ;
477+ } catch ( e ) {
478+ // This shouldn't happen, but just in case
479+ status = 'FAILED' ;
480+ if ( e . stack ) {
481+ logger . debug ( e . stack ) ;
482+ }
483+ } finally {
484+ // Unset the log interceptor for non-configured automatic dependencies so it behaves normally after this
485+ if ( ! isUserConfigured ) {
431486 defs . setFHIRPackageLoaderLogInterceptor ( ) ;
432487 }
433- if ( status !== 'LOADED' ) {
434- let message = `Failed to load automatically-provided ${ dep . packageId } #${ dep . version } ` ;
435- if ( process . env . FPL_REGISTRY ) {
436- message += ` from custom FHIR package registry ${ process . env . FPL_REGISTRY } .` ;
437- }
438- logger . warn ( message ) ;
488+ }
489+ if ( status !== 'LOADED' && ! isUserConfigured ) {
490+ let message = `Failed to load automatically-provided ${ dep . packageId } #${ dep . version } ` ;
491+ if ( process . env . FPL_REGISTRY ) {
492+ message += ` from custom FHIR package registry ${ process . env . FPL_REGISTRY } .` ;
439493 }
494+ logger . warn ( message ) ;
440495 }
441496 }
442497}
@@ -477,6 +532,11 @@ async function loadConfiguredDependencies(
477532 `Loading supplemental version of FHIR to support extensions from ${ dep . packageId } `
478533 ) ;
479534 await defs . loadSupplementalFHIRPackage ( EXT_PKG_TO_FHIR_PKG_MAP [ dep . packageId ] ) ;
535+ } else if (
536+ AUTOMATIC_DEPENDENCIES . some ( ad => configuredDependencyMatchesAutomaticDependency ( dep , ad ) )
537+ ) {
538+ // skip configured dependencies that override automatic dependencies; they will be loaded at the end
539+ continue ;
480540 } else {
481541 await defs . loadPackage ( dep . packageId , dep . version ) . catch ( e => {
482542 logger . error ( `Failed to load ${ dep . packageId } #${ dep . version } : ${ e . message } ` ) ;
0 commit comments