@open-audio-stack/core
Version:
Open-source audio plugin management software
216 lines (215 loc) • 8.65 kB
JavaScript
import * as semver from 'semver';
import { z } from 'zod';
import { Architecture } from '../types/Architecture.js';
import { FileFormat } from '../types/FileFormat.js';
import { FileType } from '../types/FileType.js';
import { License } from '../types/License.js';
import { PluginType } from '../types/PluginType.js';
import { PresetType } from '../types/PresetType.js';
import { ProjectType } from '../types/ProjectType.js';
import { SystemType } from '../types/SystemType.js';
import { pathGetExt } from './utils.js';
import yaml from 'js-yaml';
export function packageCompatibleFiles(pkg, arch, sys, excludedFormats) {
return pkg.files.filter((file) => {
const archMatches = file.architectures.filter(architecture => {
return arch.includes(architecture);
});
const sysMatches = file.systems.filter(system => {
return sys.includes(system.type);
});
const formatAllowed = excludedFormats && excludedFormats.includes(pathGetExt(file.url)) ? false : true;
return archMatches.length && sysMatches.length && formatAllowed;
});
}
export function packageErrors(pkgVersion) {
return PackageVersionValidator.safeParse(pkgVersion).error?.issues || [];
}
export function packageFileMap(pkgVersion) {
return pkgVersion.files.reduce((result, file) => {
file.systems.forEach(system => {
if (!result[system.type]) {
result[system.type] = [];
}
result[system.type].push(file);
});
return result;
}, {});
}
export function packageVersionLatest(pkg) {
return Array.from(Object.keys(pkg.versions)).sort(semver.rcompare)[0] || '0.0.0';
}
// This is a first version using zod library for validation.
// If it works well, consider updating all types to infer from Zod objects.
// This will remove duplication of code between types and validators.
export const PackageSystemValidator = z.object({
max: z.number().min(0).max(99).optional(),
min: z.number().min(0).max(99).optional(),
type: z.nativeEnum(SystemType),
});
export const PackageFileValidator = z.object({
architectures: z.nativeEnum(Architecture).array().min(1).max(Object.keys(Architecture).length),
sha256: z.string().length(64),
size: z.number().min(8).max(9999999999),
systems: PackageSystemValidator.array().min(1).max(Object.keys(SystemType).length),
type: z.nativeEnum(FileType),
url: z.string().min(8).max(256).startsWith('https://'),
});
export const PackageTypeObj = { ...PluginType, ...PresetType, ...ProjectType };
export const PackageVersionValidator = z.object({
audio: z.optional(z.string().min(8).max(256).startsWith('https://')),
author: z.string().min(1).max(256),
changes: z.string().min(1).max(256),
date: z.string().datetime(),
description: z.string().min(1).max(256),
donate: z.optional(z.string().min(8).max(256).startsWith('https://')),
files: z.array(PackageFileValidator).min(1).max(256),
image: z.string().min(8).max(256).startsWith('https://'),
license: z.nativeEnum(License),
name: z.string().min(1).max(256),
tags: z.string().min(1).max(256).array().min(1).max(8),
type: z.nativeEnum(PackageTypeObj),
url: z.string().min(8).max(256).startsWith('https://'),
});
export const SemverValidator = z
.string()
.regex(/^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:-[\w.-]+)?(?:\+[\w.-]+)?$/, 'Invalid semantic version (expected format: x.y.z or vX.Y.Z)');
// TODO refactor all this using a proper validation library.
export function packageRecommendations(pkgVersion) {
const recs = [];
packageRecommendationsUrl(pkgVersion, recs, 'audio', 'github');
packageRecommendationsUrl(pkgVersion, recs, 'image', 'github');
packageRecommendationsUrl(pkgVersion, recs, 'url', 'github');
packageRecommendationsUrl(pkgVersion, recs, 'donate');
// Image/audio previews
if (pkgVersion.image && pkgVersion.image.endsWith('png')) {
recs.push({
field: 'image',
rec: 'should use the jpg format',
});
}
if (pkgVersion.audio && pkgVersion.audio.endsWith('wav')) {
recs.push({
field: 'audio',
rec: 'should use the flac format',
});
}
// Files
if (pkgVersion.files) {
const supportedArchitectures = {};
const supportedSystems = {};
const supportedFileFormats = {};
pkgVersion.files.forEach(file => {
file.architectures.forEach(architecture => {
supportedArchitectures[architecture] = true;
});
file.systems.forEach(system => {
supportedSystems[system.type] = true;
});
const ext = pathGetExt(file.url);
supportedFileFormats[ext] = true;
packageRecommendationsUrl(file, recs, 'url', 'github');
// Formats which do not support headless installation.
if (ext === FileFormat.AppImage)
recs.push({ field: 'url', rec: 'requires manual installation steps, consider .deb and .rpm instead' });
if (ext === FileFormat.AppleDiskImage)
recs.push({ field: 'url', rec: 'requires mounting step, consider .pkg instead' });
if (!Object.values(FileFormat).includes(ext))
recs.push({ field: 'url', rec: 'not a supported format' });
// Validate format is a supported installer format
const installerFormats = [
FileFormat.AppleDiskImage,
FileFormat.ApplePackage,
FileFormat.DebianPackage,
FileFormat.ExecutableInstaller,
FileFormat.RedHatPackage,
FileFormat.WindowsInstaller,
];
if (file.type === FileType.Installer && !installerFormats.includes(ext))
recs.push({ field: 'type', rec: 'should match url field' });
});
// Architectures
if (!supportedArchitectures.arm64)
recs.push({ field: 'architectures', rec: 'should support arm64' });
if (!supportedArchitectures.x64)
recs.push({ field: 'architectures', rec: 'should support x64' });
// Systems
if (!supportedSystems.linux)
recs.push({ field: 'systems', rec: 'should support Linux' });
if (!supportedSystems.mac)
recs.push({ field: 'systems', rec: 'should support Mac' });
if (!supportedSystems.win)
recs.push({ field: 'systems', rec: 'should support Windows' });
}
else {
recs.push({
field: 'files',
rec: 'is missing',
});
}
// Tags
if (pkgVersion.tags) {
const pluginTags = pkgVersion.tags.map(tag => tag.trim().toLowerCase());
if (pluginTags.length < 2)
recs.push({ field: 'tags', rec: 'should have more items' });
}
else {
recs.push({
field: 'tags',
rec: 'is missing',
});
}
// Licence
if (pkgVersion.license) {
if (!Object.values(License).includes(pkgVersion.license)) {
recs.push({ field: 'license', rec: 'should be from the supported list' });
}
else if (pkgVersion.license === License.Other) {
recs.push({ field: 'license', rec: 'should be more specific' });
}
}
else {
recs.push({
field: 'license',
rec: 'is missing',
});
}
return recs;
}
export function packageRecommendationsUrl(obj, recs, field, domain) {
// @ts-expect-error indexing a field with multiple package types.
const val = obj[field];
if (typeof val !== 'string')
return;
if (!val.startsWith('https://')) {
recs.push({
field,
rec: 'should use https url',
});
}
if (domain && !val.includes(`${domain}.com`) && !val.includes(`${domain}.io`)) {
recs.push({
field,
rec: 'should point to GitHub',
});
}
}
export function packageJsToYaml(pkgVersion) {
return yaml.dump(pkgVersion, { lineWidth: -1 });
}
export function packageYamlToJs(pkgYaml) {
return yaml.load(pkgYaml);
}
export function packageIsVerified(slug, pkgVersion) {
const org = slug.split('/')[0];
let verified = true;
pkgVersion.files.forEach(file => {
const url = file.url.toLowerCase();
const root = url.startsWith('https://github.com/') ? 'https://github.com/' + org + '/' : `https://${org}.`;
if (!url.startsWith(root)) {
verified = false;
return;
}
});
return verified;
}