diff --git a/src/load.ts b/src/load.ts index ed53b22..e514ebc 100644 --- a/src/load.ts +++ b/src/load.ts @@ -11,6 +11,21 @@ import { getCustomRegistry } from './utils/customRegistry'; import { AxiosResponse } from 'axios'; import { LatestVersionUnavailableError } from './errors/LatestVersionUnavailableError'; +async function getDistUrl(registry: string, packageName: string, version: string): Promise { + const cleanedRegistry = registry.replace(/\/$/, ''); + // 1 get the manifest information about the package from the registry + const res = await axiosGet(`${cleanedRegistry}/${packageName}`); + // 2 find the NPM tarball location + const npmLocation = res.data?.versions?.[version]?.dist?.tarball; + + // 3 if found, use it, otherwise fallback to the FHIR spec location + if (npmLocation) { + return npmLocation; + } else { + return `${cleanedRegistry}/${packageName}/${version}`; + } +} + /** * Loads multiple dependencies from a directory (the user FHIR cache or a specified directory) or from online * @param {string[]} fhirPackages - An array of FHIR packages to download and load definitions from (format: packageId#version) @@ -220,7 +235,7 @@ export async function mergeDependency( } else if (!loadedPackage) { const customRegistry = getCustomRegistry(log); if (customRegistry) { - packageUrl = `${customRegistry.replace(/\/$/, '')}/${packageName}/${version}`; + packageUrl = await getDistUrl(customRegistry, packageName, version); } else { packageUrl = `https://packages.fhir.org/${packageName}/${version}`; } diff --git a/test/load.test.ts b/test/load.test.ts index 1949d7c..7cdd227 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -474,7 +474,7 @@ describe('#mergeDependency()', () => { // This function encapsulates that testing logic. It's coupled more tightly to // the actual implementation than I'd prefer, but... at least it's in one place. const expectDownloadSequence = ( - sources: string | string[], + sources: string | string[] | { source: string; omitResponseType?: boolean }[], destination: string | null, isCurrent = false, isCurrentFound = true @@ -485,14 +485,33 @@ describe('#mergeDependency()', () => { if (isCurrent) { const mockCalls: any[] = [['https://build.fhir.org/ig/qas.json']]; if (isCurrentFound) { - mockCalls.push( - [sources[0].replace(/package\.tgz$/, 'package.manifest.json')], - [sources[0], { responseType: 'arraybuffer' }] - ); + if (typeof sources[0] === 'string') { + mockCalls.push( + [sources[0].replace(/package\.tgz$/, 'package.manifest.json')], + [sources[0], { responseType: 'arraybuffer' }] + ); + } else { + mockCalls.push([sources[0].source.replace(/package\.tgz$/, 'package.manifest.json')]); + if (sources[0].omitResponseType !== true) { + mockCalls.push([sources[0].source, { responseType: 'arraybuffer' }]); + } + } } expect(axiosSpy.mock.calls).toEqual(mockCalls); } else { - expect(axiosSpy.mock.calls).toEqual(sources.map(s => [s, { responseType: 'arraybuffer' }])); + expect(axiosSpy.mock.calls).toEqual( + sources.map(s => { + if (typeof s === 'string') { + return [s, { responseType: 'arraybuffer' }]; + } else { + if (s.omitResponseType === true) { + return [s.source]; + } else { + return [s.source, { responseType: 'arraybuffer' }]; + } + } + }) + ); } if (destination != null) { const tempTarFile = writeSpy.mock.calls[0][0]; @@ -591,6 +610,28 @@ describe('#mergeDependency()', () => { date: '20200413230227' } }; + } else if (uri === 'https://packages.fhir.org/hl7.fhir.r4.core') { + return { + data: { + _id: 'hl7.fhir.r4.core', + name: 'hl7.fhir.r4.core', + 'dist-tags': { latest: '4.0.1' }, + versions: { + '4.0.1': { + name: 'hl7.fhir.r4.core', + version: '4.0.1', + description: + 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', + dist: { + shasum: '0e4b8d99f7918587557682c8b47df605424547a5', + tarball: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' + }, + fhirVersion: 'R4', + url: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' + } + } + } + }; } else if ( uri === 'https://packages.fhir.org/sushi-test/0.2.0' || uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz' || @@ -736,13 +777,31 @@ describe('#mergeDependency()', () => { ); }); - it('should try to load a package from a custom registry', async () => { + it('should try to load a package from a custom registry that is like NPM', async () => { + // packages.fhir.org supports NPM clients + process.env.FPL_REGISTRY = 'https://packages.fhir.org'; + await expect(mergeDependency('hl7.fhir.r4.core', '4.0.1', defs, 'foo', log)).rejects.toThrow( + 'The package hl7.fhir.r4.core#4.0.1 could not be loaded locally or from the custom FHIR package registry https://packages.fhir.org.' + ); // the package is never actually added to the cache, since tar is mocked + expectDownloadSequence( + [ + { source: 'https://packages.fhir.org/hl7.fhir.r4.core', omitResponseType: true }, + { source: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' } + ], + path.join('foo', 'hl7.fhir.r4.core#4.0.1') + ); + }); + + it('should try to load a package from a custom registry that is not like NPM', async () => { process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; await expect(mergeDependency('good-thing', '0.3.6', defs, 'foo', log)).rejects.toThrow( 'The package good-thing#0.3.6 could not be loaded locally or from the custom FHIR package registry https://custom-registry.example.org' ); // the package is never actually added to the cache, since tar is mocked expectDownloadSequence( - 'https://custom-registry.example.org/good-thing/0.3.6', + [ + { source: 'https://custom-registry.example.org/good-thing', omitResponseType: true }, + { source: 'https://custom-registry.example.org/good-thing/0.3.6' } + ], path.join('foo', 'good-thing#0.3.6') ); }); @@ -753,7 +812,10 @@ describe('#mergeDependency()', () => { 'The package good-thing#0.3.6 could not be loaded locally or from the custom FHIR package registry https://custom-registry.example.org/' ); // the package is never actually added to the cache, since tar is mocked expectDownloadSequence( - 'https://custom-registry.example.org/good-thing/0.3.6', + [ + { source: 'https://custom-registry.example.org/good-thing', omitResponseType: true }, + { source: 'https://custom-registry.example.org/good-thing/0.3.6' } + ], path.join('foo', 'good-thing#0.3.6') ); });