Files
metrics/source/plugins/licenses/index.mjs

161 lines
7.1 KiB
JavaScript

//Setup
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false, extras = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled) || (!extras) || (!q.licenses))
return null
//Load inputs
let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q})
//Initialization
const {user:{repository}} = await graphql(queries.licenses.repository({owner:data.repo.owner.login, name:data.repo.name, account}))
const result = {ratio, legal, default:repository.licenseInfo, licensed:{available:false}, text:{}, list:[], used:{}, dependencies:[], known:0, unknown:0}
const {used, text} = result
//Register existing licenses properties
const licenses = Object.fromEntries((await graphql(queries.licenses())).licenses.map(license => [license.key, license]))
for (const license of Object.values(licenses)) {
//dprint-ignore
[...license.limitations, ...license.conditions, ...license.permissions].flat().map(({key, label}) => text[key] = label)
}
colors(licenses)
//Check if licensed exists
if (await imports.which("licensed")) {
//Setup for licensed
console.debug(`metrics/compute/${login}/plugins > licenses > searching dependencies licenses using licensed`)
const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`)
//Create temporary directory
console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true})
await imports.fs.mkdir(path, {recursive:true})
//Clone repository
console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`)
const git = imports.git(path)
await git.clone(repository.url, path)
//Run setup
if (setup) {
console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`)
await imports.run(setup, {cwd:path}, {prefixed:false})
}
//Create configuration file if needed
if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) {
console.debug(`metrics/compute/${login}/plugins > licenses > building .licensed.yml configuration file`)
await imports.fs.writeFile(
imports.paths.join(path, ".licensed.yml"),
[
"cache_path: .licensed",
].join("\n"),
)
}
else
console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`)
//Spawn licensed process
console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`)
JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd:path})).apps
.map(({sources}) => sources?.flatMap(source => source.dependencies?.map(({dependency, license}) => {
used[license] = (used[license] ?? 0) + 1
result.dependencies.push(dependency)
result.known += (license in licenses)
result.unknown += !(license in licenses)
})
)
)
//Cleaning
console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true})
}
else
console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`)
//List licenses properties
console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`)
const base = {permissions:new Set(), limitations:new Set(), conditions:new Set()}
const combined = {permissions:new Set(), limitations:new Set(), conditions:new Set()}
const detected = Object.entries(used).map(([key, _value]) => ({key}))
for (const properties of Object.keys(base)) {
//Base license
if (repository.licenseInfo)
licenses[repository.licenseInfo.key]?.[properties]?.map(({key}) => base[properties].add(key))
//Combined licenses
for (const {key} of detected)
licenses[key]?.[properties]?.map(({key}) => combined[properties].add(key))
}
//Merge limitations and conditions
for (const properties of ["limitations", "conditions"])
result[properties] = [[...base[properties]].map(key => ({key, text:text[key], inherited:false})), [...combined[properties]].filter(key => !base[properties].has(key)).map(key => ({key, text:text[key], inherited:true}))].flat()
//Remove base permissions conflicting with inherited limitations
result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text:text[key]}))
//Count used licenses
console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`)
const total = Object.values(used).reduce((a, b) => a + b, 0)
//Format used licenses and compute positions
const list = Object.entries(used).map(([key, count]) => ({name:licenses[key]?.spdxId ?? `${key.charAt(0).toLocaleUpperCase()}${key.substring(1)}`, key, count, value:count / total, x:0, color:licenses[key]?.color ?? "#6e7681", order:licenses[key]?.order ?? -1})).sort((
a,
b,
) => a.order === b.order ? b.count - a.count : b.order - a.order)
for (let i = 0; i < list.length; i++)
list[i].x = (list[i - 1]?.x ?? 0) + (list[i - 1]?.value ?? 0)
//Save ratios
result.list = list
//Results
return result
}
//Handle errors
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}
/**Licenses colorizer (based on category) */
function colors(licenses) {
for (const [license, value] of Object.entries(licenses)) {
const [permissions, conditions] = [value.permissions, value.conditions].map(properties => properties.map(({key}) => key))
switch (true) {
//Other licenses
case (license === "other"): {
value.color = "#8b949e"
value.order = 0
break
}
//Strongly protective licenses and network protective
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license")) && (conditions.includes("network-use-disclose"))): {
value.color = "#388bfd"
value.order = 1
break
}
//Strongly protective licenses
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license"))): {
value.color = "#79c0ff"
value.order = 2
break
}
//Weakly protective licenses
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license--library"))): {
value.color = "#7ee787"
value.order = 3
break
}
//Permissive license
case ((permissions.includes("private-use")) && (permissions.includes("commercial-use")) && (permissions.includes("modifications")) && (permissions.includes("distribution"))): {
value.color = "#56d364"
value.order = 4
break
}
//Unknown
default: {
value.color = "#6e7681"
value.order = -1
}
}
}
}