mirror of https://github.com/renovatebot/renovate
refactor(composer): Use schema for parsing (#21520)
parent
546a52cbe6
commit
664dc808e7
lib/modules/manager/composer
|
@ -1,5 +1,6 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { quote } from 'shlex';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
SYSTEM_INSUFFICIENT_DISK_SPACE,
|
||||
TEMPORARY_ERROR,
|
||||
|
@ -18,10 +19,12 @@ import {
|
|||
import { getRepoStatus } from '../../../util/git';
|
||||
import * as hostRules from '../../../util/host-rules';
|
||||
import { regEx } from '../../../util/regex';
|
||||
import { Json } from '../../../util/schema-utils';
|
||||
import { GitTagsDatasource } from '../../datasource/git-tags';
|
||||
import { PackagistDatasource } from '../../datasource/packagist';
|
||||
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
|
||||
import type { AuthJson, ComposerLock } from './types';
|
||||
import { Lockfile, PackageFile } from './schema';
|
||||
import type { AuthJson } from './types';
|
||||
import {
|
||||
extractConstraints,
|
||||
findGithubToken,
|
||||
|
@ -105,10 +108,19 @@ export async function updateArtifacts({
|
|||
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
|
||||
logger.debug(`composer.updateArtifacts(${packageFileName})`);
|
||||
|
||||
const file = Json.pipe(PackageFile).parse(newPackageFileContent);
|
||||
|
||||
const lockFileName = packageFileName.replace(regEx(/\.json$/), '.lock');
|
||||
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
|
||||
if (!existingLockFileContent) {
|
||||
logger.debug('No composer.lock found');
|
||||
const lockfile = await z
|
||||
.string()
|
||||
.transform((f) => readLocalFile(f, 'utf8'))
|
||||
.pipe(Json)
|
||||
.pipe(Lockfile)
|
||||
.nullable()
|
||||
.catch(null)
|
||||
.parseAsync(lockFileName);
|
||||
if (!lockfile) {
|
||||
logger.debug('Composer: unable to read lockfile');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -118,12 +130,8 @@ export async function updateArtifacts({
|
|||
try {
|
||||
await writeLocalFile(packageFileName, newPackageFileContent);
|
||||
|
||||
const existingLockFile: ComposerLock = JSON.parse(existingLockFileContent);
|
||||
const constraints = {
|
||||
...extractConstraints(
|
||||
JSON.parse(newPackageFileContent),
|
||||
existingLockFile
|
||||
),
|
||||
...extractConstraints(file, lockfile),
|
||||
...config.constraints,
|
||||
};
|
||||
|
||||
|
@ -150,7 +158,7 @@ export async function updateArtifacts({
|
|||
const commands: string[] = [];
|
||||
|
||||
// Determine whether install is required before update
|
||||
if (requireComposerDependencyInstallation(existingLockFile)) {
|
||||
if (requireComposerDependencyInstallation(lockfile)) {
|
||||
const preCmd = 'composer';
|
||||
const preArgs =
|
||||
'install' + getComposerArguments(config, composerToolConstraint);
|
||||
|
|
|
@ -279,7 +279,7 @@ describe('modules/manager/composer/extract', () => {
|
|||
});
|
||||
|
||||
it('extracts dependencies with lock file', async () => {
|
||||
fs.readLocalFile.mockResolvedValue('some content');
|
||||
fs.readLocalFile.mockResolvedValue('{}');
|
||||
const res = await extractPackageFile(requirements1, packageFile);
|
||||
expect(res).toMatchSnapshot();
|
||||
expect(res?.deps).toHaveLength(33);
|
||||
|
|
|
@ -1,215 +1,15 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { logger } from '../../../logger';
|
||||
import { readLocalFile } from '../../../util/fs';
|
||||
import { regEx } from '../../../util/regex';
|
||||
import { GitTagsDatasource } from '../../datasource/git-tags';
|
||||
import { GithubTagsDatasource } from '../../datasource/github-tags';
|
||||
import { PackagistDatasource } from '../../datasource/packagist';
|
||||
import { api as semverComposer } from '../../versioning/composer';
|
||||
import type { PackageDependency, PackageFileContent } from '../types';
|
||||
import type {
|
||||
ComposerConfig,
|
||||
ComposerLock,
|
||||
ComposerManagerData,
|
||||
ComposerRepositories,
|
||||
Repo,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies
|
||||
* to use a base URL containing packages.json. Composer still works in this scenario by determining
|
||||
* whether to add / remove packages.json from the URL.
|
||||
*
|
||||
* See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815
|
||||
*/
|
||||
function transformRegUrl(url: string): string {
|
||||
return url.replace(regEx(/(\/packages\.json)$/), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the repositories field from a composer.json
|
||||
*
|
||||
* Entries with type vcs or git will be added to repositories,
|
||||
* other entries will be added to registryUrls
|
||||
*/
|
||||
function parseRepositories(
|
||||
repoJson: ComposerRepositories,
|
||||
repositories: Record<string, Repo>,
|
||||
registryUrls: string[]
|
||||
): void {
|
||||
try {
|
||||
let packagist = true;
|
||||
Object.entries(repoJson).forEach(([key, repo]) => {
|
||||
if (is.object(repo)) {
|
||||
const name = is.array(repoJson) ? repo.name : key;
|
||||
|
||||
switch (repo.type) {
|
||||
case 'vcs':
|
||||
case 'git':
|
||||
case 'path':
|
||||
repositories[name!] = repo;
|
||||
break;
|
||||
case 'composer':
|
||||
registryUrls.push(transformRegUrl(repo.url));
|
||||
break;
|
||||
case 'package':
|
||||
logger.debug(
|
||||
{ url: repo.url },
|
||||
'type package is not supported yet'
|
||||
);
|
||||
}
|
||||
if (repo.packagist === false || repo['packagist.org'] === false) {
|
||||
packagist = false;
|
||||
}
|
||||
} // istanbul ignore else: invalid repo
|
||||
else if (['packagist', 'packagist.org'].includes(key) && repo === false) {
|
||||
packagist = false;
|
||||
}
|
||||
});
|
||||
if (packagist) {
|
||||
registryUrls.push('https://packagist.org');
|
||||
} else {
|
||||
logger.debug('Disabling packagist.org');
|
||||
}
|
||||
} catch (e) /* istanbul ignore next */ {
|
||||
logger.debug(
|
||||
{ repositories: repoJson },
|
||||
'Error parsing composer.json repositories config'
|
||||
);
|
||||
}
|
||||
}
|
||||
import type { PackageFileContent } from '../types';
|
||||
import { ComposerExtract } from './schema';
|
||||
|
||||
export async function extractPackageFile(
|
||||
content: string,
|
||||
fileName: string
|
||||
): Promise<PackageFileContent | null> {
|
||||
logger.trace(`composer.extractPackageFile(${fileName})`);
|
||||
let composerJson: ComposerConfig;
|
||||
try {
|
||||
composerJson = JSON.parse(content);
|
||||
} catch (err) {
|
||||
logger.debug(`Invalid JSON in ${fileName}`);
|
||||
const res = await ComposerExtract.safeParseAsync({ content, fileName });
|
||||
if (!res.success) {
|
||||
logger.debug({ fileName, err: res.error }, 'Composer: extract failed');
|
||||
return null;
|
||||
}
|
||||
const repositories: Record<string, Repo> = {};
|
||||
const registryUrls: string[] = [];
|
||||
const res: PackageFileContent = { deps: [] };
|
||||
|
||||
// handle lockfile
|
||||
const lockfilePath = fileName.replace(regEx(/\.json$/), '.lock');
|
||||
const lockContents = await readLocalFile(lockfilePath, 'utf8');
|
||||
let lockParsed: ComposerLock | undefined;
|
||||
if (lockContents) {
|
||||
logger.debug(`Found composer lock file ${fileName}`);
|
||||
res.lockFiles = [lockfilePath];
|
||||
try {
|
||||
lockParsed = JSON.parse(lockContents) as ComposerLock;
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
logger.warn({ err }, 'Error processing composer.lock');
|
||||
}
|
||||
}
|
||||
|
||||
// handle composer.json repositories
|
||||
if (composerJson.repositories) {
|
||||
parseRepositories(composerJson.repositories, repositories, registryUrls);
|
||||
}
|
||||
|
||||
const deps: PackageDependency[] = [];
|
||||
const depTypes: ('require' | 'require-dev')[] = ['require', 'require-dev'];
|
||||
for (const depType of depTypes) {
|
||||
if (composerJson[depType]) {
|
||||
try {
|
||||
for (const [depName, version] of Object.entries(
|
||||
composerJson[depType]!
|
||||
)) {
|
||||
const currentValue = version.trim();
|
||||
if (depName === 'php') {
|
||||
deps.push({
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
datasource: GithubTagsDatasource.id,
|
||||
packageName: 'php/php-src',
|
||||
extractVersion: '^php-(?<version>.*)$',
|
||||
});
|
||||
} else {
|
||||
// Default datasource and packageName
|
||||
let datasource = PackagistDatasource.id;
|
||||
let packageName = depName;
|
||||
|
||||
// Check custom repositories by type
|
||||
if (repositories[depName]) {
|
||||
switch (repositories[depName].type) {
|
||||
case 'vcs':
|
||||
case 'git':
|
||||
datasource = GitTagsDatasource.id;
|
||||
packageName = repositories[depName].url;
|
||||
break;
|
||||
case 'path':
|
||||
deps.push({
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
skipReason: 'path-dependency',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const dep: PackageDependency = {
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
datasource,
|
||||
};
|
||||
if (depName !== packageName) {
|
||||
dep.packageName = packageName;
|
||||
}
|
||||
if (!depName.includes('/')) {
|
||||
dep.skipReason = 'unsupported';
|
||||
}
|
||||
if (lockParsed) {
|
||||
const lockField =
|
||||
depType === 'require'
|
||||
? 'packages'
|
||||
: /* istanbul ignore next */ 'packages-dev';
|
||||
const lockedDep = lockParsed[lockField]?.find(
|
||||
(item) => item.name === dep.depName
|
||||
);
|
||||
if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
|
||||
dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), '');
|
||||
}
|
||||
}
|
||||
if (
|
||||
!dep.skipReason &&
|
||||
(!repositories[depName] ||
|
||||
repositories[depName].type === 'composer') &&
|
||||
registryUrls.length !== 0
|
||||
) {
|
||||
dep.registryUrls = registryUrls;
|
||||
}
|
||||
deps.push(dep);
|
||||
}
|
||||
}
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
logger.debug({ fileName, depType, err }, 'Error parsing composer.json');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!deps.length) {
|
||||
return null;
|
||||
}
|
||||
res.deps = deps;
|
||||
if (is.string(composerJson.type)) {
|
||||
const managerData: ComposerManagerData = {
|
||||
composerJsonType: composerJson.type,
|
||||
};
|
||||
res.managerData = managerData;
|
||||
}
|
||||
|
||||
if (composerJson.require?.php) {
|
||||
res.extractedConstraints = { php: composerJson.require.php };
|
||||
}
|
||||
|
||||
return res;
|
||||
return res.data;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import { Repos, ReposArray, ReposRecord } from './schema';
|
||||
|
||||
describe('modules/manager/composer/schema', () => {
|
||||
describe('ReposRecord', () => {
|
||||
it('parses default values', () => {
|
||||
expect(ReposRecord.parse({})).toEqual([]);
|
||||
});
|
||||
|
||||
it('parses repositories', () => {
|
||||
expect(
|
||||
ReposRecord.parse({
|
||||
wpackagist: { type: 'composer', url: 'https://wpackagist.org' },
|
||||
someGit: { type: 'vcs', url: 'https://some-vcs.com' },
|
||||
somePath: { type: 'path', url: '/some/path' },
|
||||
packagist: false,
|
||||
'packagist.org': false,
|
||||
foo: 'bar',
|
||||
})
|
||||
).toEqual([
|
||||
{ type: 'composer', url: 'https://wpackagist.org' },
|
||||
{ name: 'someGit', type: 'git', url: 'https://some-vcs.com' },
|
||||
{ name: 'somePath', type: 'path', url: '/some/path' },
|
||||
{ type: 'disable-packagist' },
|
||||
{ type: 'disable-packagist' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReposArray', () => {
|
||||
it('parses default values', () => {
|
||||
expect(ReposArray.parse([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('parses repositories', () => {
|
||||
expect(
|
||||
ReposArray.parse([
|
||||
{
|
||||
type: 'composer',
|
||||
url: 'https://wpackagist.org',
|
||||
},
|
||||
{ name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' },
|
||||
{ name: 'somePath', type: 'path', url: '/some/path' },
|
||||
{ packagist: false },
|
||||
{ 'packagist.org': false },
|
||||
{ foo: 'bar' },
|
||||
])
|
||||
).toEqual([
|
||||
{ type: 'composer', url: 'https://wpackagist.org' },
|
||||
{ name: 'someGit', type: 'git', url: 'https://some-vcs.com' },
|
||||
{ name: 'somePath', type: 'path', url: '/some/path' },
|
||||
{ type: 'disable-packagist' },
|
||||
{ type: 'disable-packagist' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repos', () => {
|
||||
it('parses default values', () => {
|
||||
expect(Repos.parse(null)).toEqual({
|
||||
pathRepos: {},
|
||||
gitRepos: {},
|
||||
registryUrls: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses repositories', () => {
|
||||
expect(
|
||||
Repos.parse([
|
||||
{
|
||||
name: 'wpackagist',
|
||||
type: 'composer',
|
||||
url: 'https://wpackagist.org',
|
||||
},
|
||||
{ name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' },
|
||||
{ name: 'somePath', type: 'path', url: '/some/path' },
|
||||
])
|
||||
).toEqual({
|
||||
pathRepos: {
|
||||
somePath: { name: 'somePath', type: 'path', url: '/some/path' },
|
||||
},
|
||||
registryUrls: ['https://wpackagist.org', 'https://packagist.org'],
|
||||
gitRepos: {
|
||||
someGit: {
|
||||
name: 'someGit',
|
||||
type: 'git',
|
||||
url: 'https://some-vcs.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`parses repositories with packagist disabled`, () => {
|
||||
expect(
|
||||
Repos.parse({
|
||||
wpackagist: { type: 'composer', url: 'https://wpackagist.org' },
|
||||
someGit: { type: 'vcs', url: 'https://some-vcs.com' },
|
||||
somePath: { type: 'path', url: '/some/path' },
|
||||
packagist: false,
|
||||
})
|
||||
).toEqual({
|
||||
pathRepos: {
|
||||
somePath: { name: 'somePath', type: 'path', url: '/some/path' },
|
||||
},
|
||||
registryUrls: ['https://wpackagist.org'],
|
||||
gitRepos: {
|
||||
someGit: {
|
||||
name: 'someGit',
|
||||
type: 'git',
|
||||
url: 'https://some-vcs.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,318 @@
|
|||
import { z } from 'zod';
|
||||
import { logger } from '../../../logger';
|
||||
import { readLocalFile } from '../../../util/fs';
|
||||
import { regEx } from '../../../util/regex';
|
||||
import { Json, LooseArray, LooseRecord } from '../../../util/schema-utils';
|
||||
import { GitTagsDatasource } from '../../datasource/git-tags';
|
||||
import { GithubTagsDatasource } from '../../datasource/github-tags';
|
||||
import { PackagistDatasource } from '../../datasource/packagist';
|
||||
import { api as semverComposer } from '../../versioning/composer';
|
||||
import type { PackageDependency, PackageFileContent } from '../types';
|
||||
import type { ComposerManagerData } from './types';
|
||||
|
||||
export const ComposerRepo = z.object({
|
||||
type: z.literal('composer'),
|
||||
/**
|
||||
* The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies
|
||||
* to use a base URL containing packages.json. Composer still works in this scenario by determining
|
||||
* whether to add / remove packages.json from the URL.
|
||||
*
|
||||
* See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815
|
||||
*/
|
||||
url: z.string().transform((url) => url.replace(/\/packages\.json$/, '')),
|
||||
});
|
||||
export type ComposerRepo = z.infer<typeof ComposerRepo>;
|
||||
|
||||
export const GitRepo = z.object({
|
||||
type: z.enum(['vcs', 'git']).transform(() => 'git' as const),
|
||||
url: z.string(),
|
||||
});
|
||||
export type GitRepo = z.infer<typeof GitRepo>;
|
||||
|
||||
export const PathRepo = z.object({
|
||||
type: z.literal('path'),
|
||||
url: z.string(),
|
||||
});
|
||||
export type PathRepo = z.infer<typeof PathRepo>;
|
||||
|
||||
export const Repo = z.discriminatedUnion('type', [
|
||||
ComposerRepo,
|
||||
GitRepo,
|
||||
PathRepo,
|
||||
]);
|
||||
export type Repo = z.infer<typeof ComposerRepo>;
|
||||
|
||||
export const NamedRepo = z.discriminatedUnion('type', [
|
||||
ComposerRepo,
|
||||
GitRepo.extend({ name: z.string() }),
|
||||
PathRepo.extend({ name: z.string() }),
|
||||
]);
|
||||
export type NamedRepo = z.infer<typeof NamedRepo>;
|
||||
|
||||
const DisablePackagist = z.object({ type: z.literal('disable-packagist') });
|
||||
export type DisablePackagist = z.infer<typeof DisablePackagist>;
|
||||
|
||||
export const ReposRecord = LooseRecord(z.union([Repo, z.literal(false)]), {
|
||||
onError: ({ error: err }) => {
|
||||
logger.warn({ err }, 'Composer: error parsing repositories object');
|
||||
},
|
||||
}).transform((repos) => {
|
||||
const result: (NamedRepo | DisablePackagist)[] = [];
|
||||
for (const [name, repo] of Object.entries(repos)) {
|
||||
if (repo === false) {
|
||||
if (name === 'packagist' || name === 'packagist.org') {
|
||||
result.push({ type: 'disable-packagist' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repo.type === 'path' || repo.type === 'git') {
|
||||
result.push({ name, ...repo });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repo.type === 'composer') {
|
||||
result.push(repo);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
export type ReposRecord = z.infer<typeof ReposRecord>;
|
||||
|
||||
export const ReposArray = LooseArray(
|
||||
z.union([
|
||||
NamedRepo,
|
||||
z
|
||||
.union([
|
||||
z.object({ packagist: z.literal(false) }),
|
||||
z.object({ 'packagist.org': z.literal(false) }),
|
||||
])
|
||||
.transform((): DisablePackagist => ({ type: 'disable-packagist' })),
|
||||
]),
|
||||
{
|
||||
onError: ({ error: err }) => {
|
||||
logger.warn({ err }, 'Composer: error parsing repositories array');
|
||||
},
|
||||
}
|
||||
).transform((repos) => repos.filter((x): x is NamedRepo => x !== null));
|
||||
export type ReposArray = z.infer<typeof ReposArray>;
|
||||
|
||||
export const Repos = z
|
||||
.union([ReposRecord, ReposArray])
|
||||
.default([]) // Prevents warnings for packages without repositories field
|
||||
.catch(({ error: err }) => {
|
||||
logger.warn({ err }, 'Composer: repositories parsing error');
|
||||
return [];
|
||||
})
|
||||
.transform((repos) => {
|
||||
let packagist = true;
|
||||
const repoUrls: string[] = [];
|
||||
const gitRepos: Record<string, GitRepo> = {};
|
||||
const pathRepos: Record<string, PathRepo> = {};
|
||||
|
||||
for (const repo of repos) {
|
||||
if (repo.type === 'composer') {
|
||||
repoUrls.push(repo.url);
|
||||
} else if (repo.type === 'git') {
|
||||
gitRepos[repo.name] = repo;
|
||||
} else if (repo.type === 'path') {
|
||||
pathRepos[repo.name] = repo;
|
||||
} else if (repo.type === 'disable-packagist') {
|
||||
packagist = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (packagist && repoUrls.length) {
|
||||
repoUrls.push('https://packagist.org');
|
||||
}
|
||||
const registryUrls = repoUrls.length ? repoUrls : null;
|
||||
|
||||
return { registryUrls, gitRepos, pathRepos };
|
||||
});
|
||||
export type Repos = z.infer<typeof Repos>;
|
||||
|
||||
const RequireDefs = LooseRecord(z.string().transform((x) => x.trim())).catch(
|
||||
{}
|
||||
);
|
||||
|
||||
export const PackageFile = z
|
||||
.object({
|
||||
type: z.string().optional(),
|
||||
config: z
|
||||
.object({
|
||||
platform: z.object({
|
||||
php: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable()
|
||||
.catch(null),
|
||||
repositories: Repos,
|
||||
require: RequireDefs,
|
||||
'require-dev': RequireDefs,
|
||||
})
|
||||
.transform(
|
||||
({
|
||||
type: composerJsonType,
|
||||
config,
|
||||
repositories,
|
||||
require,
|
||||
'require-dev': requireDev,
|
||||
}) => ({
|
||||
composerJsonType,
|
||||
config,
|
||||
repositories,
|
||||
require,
|
||||
requireDev,
|
||||
})
|
||||
);
|
||||
export type PackageFile = z.infer<typeof PackageFile>;
|
||||
|
||||
const LockedPackage = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
});
|
||||
type LockedPackage = z.infer<typeof LockedPackage>;
|
||||
|
||||
export const Lockfile = z
|
||||
.object({
|
||||
'plugin-api-version': z.string().optional(),
|
||||
packages: LooseArray(LockedPackage).catch([]),
|
||||
'packages-dev': LooseArray(LockedPackage).catch([]),
|
||||
})
|
||||
.transform(
|
||||
({
|
||||
'plugin-api-version': pluginApiVersion,
|
||||
packages,
|
||||
'packages-dev': packagesDev,
|
||||
}) => ({ pluginApiVersion, packages, packagesDev })
|
||||
);
|
||||
export type Lockfile = z.infer<typeof Lockfile>;
|
||||
|
||||
export const ComposerExtract = z
|
||||
.object({
|
||||
content: z.string(),
|
||||
fileName: z.string(),
|
||||
})
|
||||
.transform(({ content, fileName }) => {
|
||||
const lockfileName = fileName.replace(/\.json$/, '.lock');
|
||||
return {
|
||||
file: content,
|
||||
lockfileName,
|
||||
lockfile: lockfileName,
|
||||
};
|
||||
})
|
||||
.pipe(
|
||||
z.object({
|
||||
file: Json.pipe(PackageFile),
|
||||
lockfileName: z.string(),
|
||||
lockfile: z
|
||||
.string()
|
||||
.transform((lockfileName) => readLocalFile(lockfileName, 'utf8'))
|
||||
.pipe(Json)
|
||||
.pipe(Lockfile)
|
||||
.nullable()
|
||||
.catch(({ error: err }) => {
|
||||
logger.warn({ err }, 'Composer: lockfile parsing error');
|
||||
return null;
|
||||
}),
|
||||
})
|
||||
)
|
||||
.transform(({ file, lockfile, lockfileName }) => {
|
||||
const { composerJsonType, require, requireDev } = file;
|
||||
const { registryUrls, gitRepos, pathRepos } = file.repositories;
|
||||
|
||||
const deps: PackageDependency[] = [];
|
||||
|
||||
const profiles = [
|
||||
{
|
||||
depType: 'require',
|
||||
req: require,
|
||||
locked: lockfile?.packages ?? [],
|
||||
},
|
||||
{
|
||||
depType: 'require-dev',
|
||||
req: requireDev,
|
||||
locked: lockfile?.packagesDev ?? [],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { depType, req, locked } of profiles) {
|
||||
for (const [depName, currentValue] of Object.entries(req)) {
|
||||
if (depName === 'php') {
|
||||
deps.push({
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
datasource: GithubTagsDatasource.id,
|
||||
packageName: 'php/php-src',
|
||||
extractVersion: '^php-(?<version>.*)$',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pathRepos[depName]) {
|
||||
deps.push({
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
skipReason: 'path-dependency',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const dep: PackageDependency = {
|
||||
depType,
|
||||
depName,
|
||||
currentValue,
|
||||
};
|
||||
|
||||
if (!depName.includes('/')) {
|
||||
dep.skipReason = 'unsupported';
|
||||
}
|
||||
|
||||
const lockedDep = locked.find((item) => item.name === depName);
|
||||
if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
|
||||
dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), '');
|
||||
}
|
||||
|
||||
const gitRepo = gitRepos[depName];
|
||||
if (gitRepo) {
|
||||
dep.datasource = GitTagsDatasource.id;
|
||||
dep.packageName = gitRepo.url;
|
||||
deps.push(dep);
|
||||
continue;
|
||||
}
|
||||
|
||||
dep.datasource = PackagistDatasource.id;
|
||||
|
||||
if (registryUrls) {
|
||||
dep.registryUrls = registryUrls;
|
||||
}
|
||||
|
||||
deps.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (!deps.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res: PackageFileContent<ComposerManagerData> = { deps };
|
||||
|
||||
if (composerJsonType) {
|
||||
res.managerData = { composerJsonType };
|
||||
}
|
||||
|
||||
if (require.php) {
|
||||
res.extractedConstraints = { php: require.php };
|
||||
}
|
||||
|
||||
if (lockfile) {
|
||||
res.lockFiles = [lockfileName];
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
export type ComposerExtract = z.infer<typeof ComposerExtract>;
|
|
@ -1,49 +1,3 @@
|
|||
// istanbul ignore file: types only
|
||||
export interface Repo {
|
||||
name?: string;
|
||||
type: 'composer' | 'git' | 'package' | 'path' | 'vcs';
|
||||
packagist?: boolean;
|
||||
'packagist.org'?: boolean;
|
||||
url: string;
|
||||
}
|
||||
export type ComposerRepositories = Record<string, Repo | boolean> | Repo[];
|
||||
|
||||
export interface ComposerConfig {
|
||||
type?: string;
|
||||
/**
|
||||
* Setting a fixed PHP version (e.g. {"php": "7.0.3"}) will let you fake the
|
||||
* platform version so that you can emulate a production env or define your
|
||||
* target platform in the config.
|
||||
* See https://getcomposer.org/doc/06-config.md#platform
|
||||
*/
|
||||
config?: {
|
||||
platform?: {
|
||||
php?: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* A repositories field can be an array of Repo objects or an object of repoName: Repo
|
||||
* Also it can be a boolean (usually false) to disable packagist.
|
||||
* (Yes this can be confusing, as it is also not properly documented in the composer docs)
|
||||
* See https://getcomposer.org/doc/05-repositories.md#disabling-packagist-org
|
||||
*/
|
||||
repositories?: ComposerRepositories;
|
||||
|
||||
require?: Record<string, string>;
|
||||
'require-dev'?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ComposerLockPackage {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ComposerLock {
|
||||
'plugin-api-version'?: string;
|
||||
packages?: ComposerLockPackage[];
|
||||
'packages-dev'?: ComposerLockPackage[];
|
||||
}
|
||||
|
||||
export interface ComposerManagerData {
|
||||
composerJsonType?: string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { logger } from '../../../logger';
|
||||
import { Json } from '../../../util/schema-utils';
|
||||
import { api as composer } from '../../versioning/composer';
|
||||
import type { UpdateLockedConfig, UpdateLockedResult } from '../types';
|
||||
import type { ComposerLock } from './types';
|
||||
import { Lockfile } from './schema';
|
||||
|
||||
export function updateLockedDependency(
|
||||
config: UpdateLockedConfig
|
||||
|
@ -12,12 +13,11 @@ export function updateLockedDependency(
|
|||
`composer.updateLockedDependency: ${depName}@${currentVersion} -> ${newVersion} [${lockFile}]`
|
||||
);
|
||||
try {
|
||||
const locked = JSON.parse(lockFileContent!) as ComposerLock;
|
||||
const lockfile = Json.pipe(Lockfile).parse(lockFileContent);
|
||||
if (
|
||||
locked.packages?.find(
|
||||
(entry) =>
|
||||
entry.name === depName &&
|
||||
composer.equals(entry.version || '', newVersion)
|
||||
lockfile?.packages.find(
|
||||
({ name, version }) =>
|
||||
name === depName && composer.equals(version, newVersion)
|
||||
)
|
||||
) {
|
||||
return { status: 'already-updated' };
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GlobalConfig } from '../../../config/global';
|
||||
import * as hostRules from '../../../util/host-rules';
|
||||
import { GitTagsDatasource } from '../../datasource/git-tags';
|
||||
import { Lockfile, PackageFile } from './schema';
|
||||
import {
|
||||
extractConstraints,
|
||||
findGithubToken,
|
||||
|
@ -21,114 +22,121 @@ describe('modules/manager/composer/utils', () => {
|
|||
|
||||
describe('extractConstraints', () => {
|
||||
it('returns from require', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{ require: { php: '>=5.3.2', 'composer/composer': '1.1.0' } },
|
||||
{}
|
||||
)
|
||||
).toEqual({ php: '>=5.3.2', composer: '1.1.0' });
|
||||
const file = PackageFile.parse({
|
||||
require: { php: '>=5.3.2', 'composer/composer': '1.1.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
php: '>=5.3.2',
|
||||
composer: '1.1.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns platform php version', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{
|
||||
config: { platform: { php: '7.4.27' } },
|
||||
require: { php: '~7.4 || ~8.0' },
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.*', php: '<=7.4.27' });
|
||||
const file = PackageFile.parse({
|
||||
config: { platform: { php: '7.4.27' } },
|
||||
require: { php: '~7.4 || ~8.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '1.*',
|
||||
php: '<=7.4.27',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns platform 0 minor php version', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{
|
||||
config: { platform: { php: '7.0.5' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.*', php: '<=7.0.5' });
|
||||
const file = PackageFile.parse({
|
||||
config: { platform: { php: '7.0.5' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '1.*',
|
||||
php: '<=7.0.5',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns platform 0 patch php version', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{
|
||||
config: { platform: { php: '7.4.0' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.*', php: '<=7.4.0' });
|
||||
const file = PackageFile.parse({
|
||||
config: { platform: { php: '7.4.0' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '1.*',
|
||||
php: '<=7.4.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns platform lowest minor php version', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{
|
||||
config: { platform: { php: '7' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.*', php: '<=7.0.0' });
|
||||
const file = PackageFile.parse({
|
||||
config: { platform: { php: '7' } },
|
||||
require: { php: '^7.0 || ~8.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '1.*',
|
||||
php: '<=7.0.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns platform lowest patch php version', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{
|
||||
config: { platform: { php: '7.4' } },
|
||||
require: { php: '~7.4 || ~8.0' },
|
||||
},
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.*', php: '<=7.4.0' });
|
||||
const file = PackageFile.parse({
|
||||
config: { platform: { php: '7.4' } },
|
||||
require: { php: '~7.4 || ~8.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '1.*',
|
||||
php: '<=7.4.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns from require-dev', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{ 'require-dev': { 'composer/composer': '1.1.0' } },
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '1.1.0' });
|
||||
const file = PackageFile.parse({
|
||||
'require-dev': { 'composer/composer': '1.1.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.1.0' });
|
||||
});
|
||||
|
||||
it('returns from composer platform require', () => {
|
||||
expect(
|
||||
extractConstraints({ require: { php: '^8.1', composer: '2.2.0' } }, {})
|
||||
).toEqual({ php: '^8.1', composer: '2.2.0' });
|
||||
const file = PackageFile.parse({
|
||||
require: { php: '^8.1', composer: '2.2.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
php: '^8.1',
|
||||
composer: '2.2.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns from composer platform require-dev', () => {
|
||||
expect(
|
||||
extractConstraints({ 'require-dev': { composer: '^2.2' } }, {})
|
||||
).toEqual({ composer: '^2.2' });
|
||||
const file = PackageFile.parse({ 'require-dev': { composer: '^2.2' } });
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({ composer: '^2.2' });
|
||||
});
|
||||
|
||||
it('returns from composer-runtime-api', () => {
|
||||
expect(
|
||||
extractConstraints(
|
||||
{ require: { 'composer-runtime-api': '^1.1.0' } },
|
||||
{}
|
||||
)
|
||||
).toEqual({ composer: '^1.1' });
|
||||
const file = PackageFile.parse({
|
||||
require: { 'composer-runtime-api': '^1.1.0' },
|
||||
});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({ composer: '^1.1' });
|
||||
});
|
||||
|
||||
it('returns from plugin-api-version', () => {
|
||||
expect(extractConstraints({}, { 'plugin-api-version': '1.1.0' })).toEqual(
|
||||
{
|
||||
composer: '^1.1',
|
||||
}
|
||||
);
|
||||
const file = PackageFile.parse({});
|
||||
const lockfile = Lockfile.parse({ 'plugin-api-version': '1.1.0' });
|
||||
expect(extractConstraints(file, lockfile)).toEqual({
|
||||
composer: '^1.1',
|
||||
});
|
||||
});
|
||||
|
||||
it('fallback to 1.*', () => {
|
||||
expect(extractConstraints({}, {})).toEqual({ composer: '1.*' });
|
||||
const file = PackageFile.parse({});
|
||||
const lockfile = Lockfile.parse({});
|
||||
expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.*' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -276,27 +284,24 @@ describe('modules/manager/composer/utils', () => {
|
|||
|
||||
describe('requireComposerDependencyInstallation', () => {
|
||||
it('returns true when symfony/flex has been installed', () => {
|
||||
expect(
|
||||
requireComposerDependencyInstallation({
|
||||
packages: [{ name: 'symfony/flex', version: '1.17.1' }],
|
||||
})
|
||||
).toBeTrue();
|
||||
const lockfile = Lockfile.parse({
|
||||
packages: [{ name: 'symfony/flex', version: '1.17.1' }],
|
||||
});
|
||||
expect(requireComposerDependencyInstallation(lockfile)).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns true when symfony/flex has been installed as dev dependency', () => {
|
||||
expect(
|
||||
requireComposerDependencyInstallation({
|
||||
'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }],
|
||||
})
|
||||
).toBeTrue();
|
||||
const lockfile = Lockfile.parse({
|
||||
'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }],
|
||||
});
|
||||
expect(requireComposerDependencyInstallation(lockfile)).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false when symfony/flex has not been installed', () => {
|
||||
expect(
|
||||
requireComposerDependencyInstallation({
|
||||
packages: [{ name: 'symfony/console', version: '5.4.0' }],
|
||||
})
|
||||
).toBeFalse();
|
||||
const lockfile = Lockfile.parse({
|
||||
packages: [{ name: 'symfony/console', version: '5.4.0' }],
|
||||
});
|
||||
expect(requireComposerDependencyInstallation(lockfile)).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { HostRuleSearchResult } from '../../../types';
|
|||
import type { ToolConstraint } from '../../../util/exec/types';
|
||||
import { api, id as composerVersioningId } from '../../versioning/composer';
|
||||
import type { UpdateArtifactsConfig } from '../types';
|
||||
import type { ComposerConfig, ComposerLock } from './types';
|
||||
import type { Lockfile, PackageFile } from './schema';
|
||||
|
||||
export { composerVersioningId };
|
||||
|
||||
|
@ -59,53 +59,55 @@ export function getPhpConstraint(
|
|||
return null;
|
||||
}
|
||||
|
||||
export function requireComposerDependencyInstallation(
|
||||
lock: ComposerLock
|
||||
): boolean {
|
||||
export function requireComposerDependencyInstallation({
|
||||
packages,
|
||||
packagesDev,
|
||||
}: Lockfile): boolean {
|
||||
return (
|
||||
lock.packages?.some((p) => depRequireInstall.has(p.name)) === true ||
|
||||
lock['packages-dev']?.some((p) => depRequireInstall.has(p.name)) === true
|
||||
packages.some((p) => depRequireInstall.has(p.name)) === true ||
|
||||
packagesDev.some((p) => depRequireInstall.has(p.name)) === true
|
||||
);
|
||||
}
|
||||
|
||||
export function extractConstraints(
|
||||
composerJson: ComposerConfig,
|
||||
lockParsed: ComposerLock
|
||||
{ config, require, requireDev }: PackageFile,
|
||||
{ pluginApiVersion }: Lockfile
|
||||
): Record<string, string> {
|
||||
const res: Record<string, string> = { composer: '1.*' };
|
||||
|
||||
// extract php
|
||||
if (composerJson.config?.platform?.php) {
|
||||
const major = api.getMajor(composerJson.config.platform.php);
|
||||
const minor = api.getMinor(composerJson.config.platform.php) ?? 0;
|
||||
const patch = api.getPatch(composerJson.config.platform.php) ?? 0;
|
||||
const phpVersion = config?.platform.php;
|
||||
if (phpVersion) {
|
||||
const major = api.getMajor(phpVersion);
|
||||
const minor = api.getMinor(phpVersion) ?? 0;
|
||||
const patch = api.getPatch(phpVersion) ?? 0;
|
||||
res.php = `<=${major}.${minor}.${patch}`;
|
||||
} else if (composerJson.require?.php) {
|
||||
res.php = composerJson.require.php;
|
||||
} else if (require.php) {
|
||||
res.php = require.php;
|
||||
}
|
||||
|
||||
// extract direct composer dependency
|
||||
if (composerJson.require?.['composer/composer']) {
|
||||
res.composer = composerJson.require?.['composer/composer'];
|
||||
} else if (composerJson['require-dev']?.['composer/composer']) {
|
||||
res.composer = composerJson['require-dev']?.['composer/composer'];
|
||||
if (require['composer/composer']) {
|
||||
res.composer = require['composer/composer'];
|
||||
} else if (requireDev['composer/composer']) {
|
||||
res.composer = requireDev['composer/composer'];
|
||||
}
|
||||
// composer platform package
|
||||
else if (composerJson.require?.['composer']) {
|
||||
res.composer = composerJson.require?.['composer'];
|
||||
} else if (composerJson['require-dev']?.['composer']) {
|
||||
res.composer = composerJson['require-dev']?.['composer'];
|
||||
else if (require['composer']) {
|
||||
res.composer = require['composer'];
|
||||
} else if (requireDev['composer']) {
|
||||
res.composer = requireDev['composer'];
|
||||
}
|
||||
// check last used composer version
|
||||
else if (lockParsed?.['plugin-api-version']) {
|
||||
const major = api.getMajor(lockParsed?.['plugin-api-version']);
|
||||
const minor = api.getMinor(lockParsed?.['plugin-api-version']);
|
||||
else if (pluginApiVersion) {
|
||||
const major = api.getMajor(pluginApiVersion);
|
||||
const minor = api.getMinor(pluginApiVersion);
|
||||
res.composer = `^${major}.${minor}`;
|
||||
}
|
||||
// check composer api dependency
|
||||
else if (composerJson.require?.['composer-runtime-api']) {
|
||||
const major = api.getMajor(composerJson.require?.['composer-runtime-api']);
|
||||
const minor = api.getMinor(composerJson.require?.['composer-runtime-api']);
|
||||
else if (require['composer-runtime-api']) {
|
||||
const major = api.getMajor(require['composer-runtime-api']);
|
||||
const minor = api.getMinor(require['composer-runtime-api']);
|
||||
res.composer = `^${major}.${minor}`;
|
||||
}
|
||||
return res;
|
||||
|
|
Loading…
Reference in New Issue