diff --git a/specification.md b/specification.md index 78913f5..3931fc3 100644 --- a/specification.md +++ b/specification.md @@ -119,10 +119,11 @@ $ manager config get List of registries which will be searched for packages. This allows brands/companies to each own a package list, but registries are combined for the user consuming the list. -| Field | Type | Description | Example | -| :---- | :----- | :------------ | :--------------------------------------------------------------- | -| name | string | Registry name | `"Open Audio Registry"` | -| url | string | Registry url | `"https://open-audio-stack.github.io/open-audio-stack-registry"` | +| Field | Type | Description | Example | +| :------ | :----- | :------------------------------------------------------------------ | :--------------------------------------------------------------- | +| name | string | Registry name | `"Open Audio Registry"` | +| url | string | Registry url | `"https://open-audio-stack.github.io/open-audio-stack-registry"` | +| version | string | Optional - see [Registry versioning](#registry-versioning-optional) | `"v1"` | #### Get registries diff --git a/src/classes/Manager.ts b/src/classes/Manager.ts index 7780fda..1cd41b2 100644 --- a/src/classes/Manager.ts +++ b/src/classes/Manager.ts @@ -6,6 +6,7 @@ import { ManagerReport, PackageVersion } from '../types/Package.js'; import { RegistryInterface, RegistryPackages, RegistryType } from '../types/Registry.js'; import { Base } from './Base.js'; import { packageCompatibleFiles } from '../helpers/package.js'; +import { registryUrl } from '../helpers/registry.js'; import { Architecture, SystemType } from '../index-browser.js'; export class Manager extends Base { @@ -137,7 +138,7 @@ export class Manager extends Base { for (const index in registries) { let json: RegistryInterface; try { - json = await apiJson(registries[index].url); + json = await apiJson(registryUrl(registries[index])); } catch (err) { // One unreachable/misconfigured registry shouldn't stop the others from being synced - // record the failure and move on, matching the spec's goal of combining packages from diff --git a/src/helpers/registry.ts b/src/helpers/registry.ts index a8a436d..bcc4986 100644 --- a/src/helpers/registry.ts +++ b/src/helpers/registry.ts @@ -1,3 +1,4 @@ +import { ConfigRegistry } from '../types/Config.js'; import { RegistryInterface } from '../types/Registry.js'; export function registryDefaults(): RegistryInterface { @@ -10,3 +11,10 @@ export function registryDefaults(): RegistryInterface { version: '1.0.0', }; } + +// See specification.md "Registry versioning (optional)" - appends the version segment to the +// registry root when one is configured. Unversioned registries (the common case) are untouched, +// resolving to whatever the registry serves as its latest version. +export function registryUrl(registry: ConfigRegistry): string { + return registry.version ? `${registry.url.replace(/\/$/, '')}/${registry.version}` : registry.url; +} diff --git a/src/types/Config.ts b/src/types/Config.ts index f327339..ee37488 100644 --- a/src/types/Config.ts +++ b/src/types/Config.ts @@ -22,4 +22,9 @@ export interface ConfigInterface { export interface ConfigRegistry { name: string; url: string; + // Optional versioned endpoint (see specification.md "Registry versioning (optional)") - when + // set, appended as a path segment onto `url` when requesting resources, so a registry can + // introduce breaking changes on a new version without affecting managers still pointed at the + // root (which always resolves to the latest version). + version?: string; } diff --git a/tests/classes/Manager.test.ts b/tests/classes/Manager.test.ts index 2d9b8d7..0e8af38 100644 --- a/tests/classes/Manager.test.ts +++ b/tests/classes/Manager.test.ts @@ -242,6 +242,24 @@ test('Manager sync isolates a malformed package version instead of throwing', as apiJsonSpy.mockRestore(); }); +test('Manager sync appends a configured registry version to the request url', async () => { + const apiJsonSpy = vi.spyOn(apiHelpers, 'apiJson').mockResolvedValue({ + name: 'Mock Registry', + url: 'https://example.invalid/registry', + version: '1.0.0', + [RegistryType.Plugins]: {}, + }); + + const manager = new Manager(RegistryType.Plugins, { + registries: [{ name: 'Mock Registry', url: 'https://example.invalid/registry', version: 'v1' }], + }); + await manager.sync(); + + expect(apiJsonSpy).toHaveBeenCalledWith('https://example.invalid/registry/v1'); + + apiJsonSpy.mockRestore(); +}); + test('Manager sync with existing package', async () => { const manager = new Manager(RegistryType.Plugins); const pkg = new Package(PLUGIN_PACKAGE.slug); diff --git a/tests/helpers/registry.test.ts b/tests/helpers/registry.test.ts new file mode 100644 index 0000000..41092f7 --- /dev/null +++ b/tests/helpers/registry.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from 'vitest'; +import { registryDefaults, registryUrl } from '../../src/helpers/registry'; +import { RegistryInterface } from '../../src/types/Registry'; + +const REGISTRY: RegistryInterface = { + name: 'Open Audio Registry', + plugins: {}, + presets: {}, + projects: {}, + url: 'https://open-audio-stack.github.io/open-audio-stack-registry', + version: '1.0.0', +}; + +test('Get default value', () => { + expect(registryDefaults()).toEqual(REGISTRY); +}); + +test('Registry url without a version is unchanged', () => { + expect(registryUrl({ name: 'Open Audio Registry', url: 'https://example.com/registry' })).toEqual( + 'https://example.com/registry', + ); +}); + +test('Registry url with a version appends the version segment', () => { + expect(registryUrl({ name: 'Open Audio Registry', url: 'https://example.com/registry', version: 'v1' })).toEqual( + 'https://example.com/registry/v1', + ); +}); + +test('Registry url with a version strips a trailing slash from the root first', () => { + expect(registryUrl({ name: 'Open Audio Registry', url: 'https://example.com/registry/', version: 'v1' })).toEqual( + 'https://example.com/registry/v1', + ); +});