diff --git a/lib/modules/datasource/packagist/schema.spec.ts b/lib/modules/datasource/packagist/schema.spec.ts index 2bae8ee23f0056..c673fa7281fd0e 100644 --- a/lib/modules/datasource/packagist/schema.spec.ts +++ b/lib/modules/datasource/packagist/schema.spec.ts @@ -2,13 +2,74 @@ import type { ReleaseResult } from '../types'; import { ComposerRelease, ComposerReleases, + MinifiedArray, parsePackagesResponse, parsePackagesResponses, } from './schema'; describe('modules/datasource/packagist/schema', () => { + describe('MinifiedArray', () => { + it('parses MinifiedArray', () => { + expect(MinifiedArray.parse([])).toEqual([]); + + // Source: https://github.com/composer/metadata-minifier/blob/1.0.0/tests/MetadataMinifierTest.php + expect( + MinifiedArray.parse([ + { + name: 'foo/bar', + version: '2.0.0', + version_normalized: '2.0.0.0', + type: 'library', + scripts: { + foo: 'bar', + }, + license: ['MIT'], + }, + { + version: '1.2.0', + version_normalized: '1.2.0.0', + license: ['GPL'], + homepage: 'https://example.org', + scripts: '__unset', + }, + { + version: '1.0.0', + version_normalized: '1.0.0.0', + homepage: '__unset', + }, + ]) + ).toEqual([ + { + name: 'foo/bar', + version: '2.0.0', + version_normalized: '2.0.0.0', + type: 'library', + scripts: { + foo: 'bar', + }, + license: ['MIT'], + }, + { + name: 'foo/bar', + version: '1.2.0', + version_normalized: '1.2.0.0', + type: 'library', + license: ['GPL'], + homepage: 'https://example.org', + }, + { + name: 'foo/bar', + version: '1.0.0', + version_normalized: '1.0.0.0', + type: 'library', + license: ['GPL'], + }, + ]); + }); + }); + describe('ComposerRelease', () => { - it('rejects', () => { + it('rejects ComposerRelease', () => { expect(() => ComposerRelease.parse(null)).toThrow(); expect(() => ComposerRelease.parse(undefined)).toThrow(); expect(() => ComposerRelease.parse('')).toThrow(); @@ -17,7 +78,7 @@ describe('modules/datasource/packagist/schema', () => { expect(() => ComposerRelease.parse({ version: null })).toThrow(); }); - it('parses', () => { + it('parses ComposerRelease', () => { expect(ComposerRelease.parse({ version: '' })).toEqual({ version: '' }); expect(ComposerRelease.parse({ version: 'dev-main' })).toEqual({ version: 'dev-main', @@ -50,14 +111,14 @@ describe('modules/datasource/packagist/schema', () => { }); describe('ComposerReleases', () => { - it('rejects', () => { + it('rejects ComposerReleases', () => { expect(() => ComposerReleases.parse(null)).toThrow(); expect(() => ComposerReleases.parse(undefined)).toThrow(); expect(() => ComposerReleases.parse('')).toThrow(); expect(() => ComposerReleases.parse({})).toThrow(); }); - it('parses', () => { + it('parses ComposerReleases', () => { expect(ComposerReleases.parse([])).toEqual([]); expect(ComposerReleases.parse([null])).toEqual([]); expect(ComposerReleases.parse([1, 2, 3])).toEqual([]); @@ -69,7 +130,7 @@ describe('modules/datasource/packagist/schema', () => { }); describe('parsePackageResponse', () => { - it('parses', () => { + it('parses package response', () => { expect(parsePackagesResponse('foo/bar', null)).toEqual([]); expect(parsePackagesResponse('foo/bar', {})).toEqual([]); expect(parsePackagesResponse('foo/bar', { packages: '123' })).toEqual([]); @@ -86,7 +147,7 @@ describe('modules/datasource/packagist/schema', () => { }); describe('parsePackagesResponses', () => { - it('parses', () => { + it('parses array of responses', () => { expect(parsePackagesResponses('foo/bar', [null])).toBeNull(); expect(parsePackagesResponses('foo/bar', [{}])).toBeNull(); expect( diff --git a/lib/modules/datasource/packagist/schema.ts b/lib/modules/datasource/packagist/schema.ts index 881b0012579964..3a998becb62f8a 100644 --- a/lib/modules/datasource/packagist/schema.ts +++ b/lib/modules/datasource/packagist/schema.ts @@ -1,7 +1,44 @@ +import is from '@sindresorhus/is'; import { z } from 'zod'; import { logger } from '../../../logger'; import type { Release, ReleaseResult } from '../types'; +export const MinifiedArray = z.array(z.record(z.unknown())).transform((xs) => { + // Ported from: https://github.com/composer/metadata-minifier/blob/main/src/MetadataMinifier.php#L17 + if (xs.length === 0) { + return xs; + } + + const prevVals: Record = {}; + for (const x of xs) { + for (const key of Object.keys(x)) { + prevVals[key] ??= undefined; + } + + for (const key of Object.keys(prevVals)) { + const val = x[key]; + if (val === '__unset') { + delete x[key]; + prevVals[key] = undefined; + continue; + } + + if (!is.undefined(val)) { + prevVals[key] = val; + continue; + } + + if (!is.undefined(prevVals[key])) { + x[key] = prevVals[key]; + continue; + } + } + } + + return xs; +}); +export type MinifiedArray = z.infer; + export const ComposerRelease = z .object({ version: z.string(), @@ -37,7 +74,8 @@ export function parsePackagesResponse( ): ComposerReleases { try { const { packages } = ComposerPackagesResponse.parse(packagesResponse); - const releases = ComposerReleases.parse(packages[packageName]); + const array = MinifiedArray.parse(packages[packageName]); + const releases = ComposerReleases.parse(array); return releases; } catch (err) { logger.debug(