refactor(app/web): new features (#1124) [skip ci]

This commit is contained in:
Simon Lecoq
2022-07-06 04:37:39 +02:00
committed by GitHub
parent 7379fb21a8
commit 130c74b266
80 changed files with 1304 additions and 1103 deletions

View File

@@ -24,7 +24,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Initialization
const pending = []
const {queries} = conf
const extras = {css: (conf.settings.extras?.css ?? conf.settings.extras?.default) ? q["extras.css"] ?? "" : "", js: (conf.settings.extras?.js ?? conf.settings.extras?.default) ? q["extras.js"] ?? "" : ""}
const extras = {css: imports.metadata.plugins.core.extras("extras_css", {...conf.settings, error:false}) ? q["extras.css"] ?? "" : "", js: imports.metadata.plugins.core.extras("extras_js", {...conf.settings, error:false}) ? q["extras.js"] ?? "" : ""}
const data = {q, animated: true, large: false, base: {}, config: {}, errors: [], plugins: {}, computed: {}, extras, postscripts: []}
const imports = {
plugins: Plugins,
@@ -184,7 +184,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("svg")))
rendered = await imports.svg.optimize.svg(rendered, q, experimental)
//Verify svg
if (verify) {
if ((verify)&&(imports.metadata.plugins.core.extras("verify", {...conf.settings, error:false}))) {
console.debug(`metrics/compute/${login} > verify SVG`)
let libxmljs = null
try {
@@ -281,9 +281,9 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
console.debug(`metrics/compute/${login} > insights > generating data`)
const result = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})
const json = JSON.stringify(result)
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
await page.waitForSelector(".container .user", {timeout: 10 * 60 * 1000})
//Rendering
@@ -297,7 +297,7 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
</head>
<body>
${await page.evaluate(() => document.querySelector("main").outerHTML)}
${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
${(await Promise.all([".css/style.vars.css", ".css/style.css", "insights/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
</body>
</html>`
await browser.close()

View File

@@ -111,7 +111,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
if (account !== "bypass") {
const context = q.repo ? "repository" : account
if (!meta.supports?.includes(context))
throw {error: {message: `Not supported for: ${context}`, instance: new Error()}}
throw {error: {message: `Unsupported context ${context}`, instance: new Error()}}
}
//Special values replacer
const replacer = value => {
@@ -214,33 +214,59 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
//Extra features parser
{
meta.extras = function(input, {extras = {}}) {
//Required permissions
const required = inputs[metadata.to.yaml(input, {name})]?.extras ?? null
if (!required)
meta.extras = function(input, {extras = {}, error = true}) {
const key = metadata.to.yaml(input, {name})
try {
//Required permissions
const required = inputs[key]?.extras ?? null
if (!required)
return true
console.debug(`metrics/extras > ${name} > ${key} > require [${required}]`)
//Legacy handling
const enabled = extras?.features ?? extras?.default ?? false
if (typeof enabled === "boolean") {
console.debug(`metrics/extras > ${name} > ${key} > extras features is set to ${enabled}`)
if (!enabled)
throw new Error()
return enabled
}
if (!Array.isArray(required)) {
console.debug(`metrics/extras > ${name} > ${key} > extras is not a permission array, skipping`)
return false
}
//Legacy options handling
if (!Array.isArray(extras.features))
throw new Error(`metrics/extras > ${name} > ${key} > extras.features is not an array`)
if (extras.css) {
console.warn(`metrics/extras > ${name} > ${key} > extras.css is deprecated, use extras.features with "metrics.run.puppeteer.user.css" instead`)
extras.features.push("metrics.run.puppeteer.user.css")
}
if (extras.js) {
console.warn(`metrics/extras > ${name} > ${key} > extras.js is deprecated, use extras.features with "metrics.run.puppeteer.user.js" instead`)
extras.features.push("metrics.run.puppeteer.user.js")
}
if (extras.presets) {
console.warn(`metrics/extras > ${name} > ${key} > extras.presets is deprecated, use extras.features with "metrics.setup.community.presets" instead`)
extras.features.push("metrics.setup.community.presets")
}
//Check permissions
const missing = required.filter(permission => !extras.features.includes(permission))
if (missing.length > 0) {
console.debug(`metrics/extras > ${name} > ${key} > missing permissions [${missing}]`)
throw new Error()
}
return true
console.debug(`metrics/extras > ${name} > ${input} > require [${required}]`)
//Legacy handling
const enabled = extras?.features ?? extras?.default ?? false
if (typeof enabled === "boolean") {
console.debug(`metrics/extras > ${name} > ${input} > extras features is set to ${enabled}`)
return enabled
}
if (!Array.isArray(required)) {
console.debug(`metrics/extras > ${name} > ${input} > extras is not a permission array, skipping`)
return false
catch {
if (!error) {
console.debug(`metrics/extras > ${name} > ${key} > skipping (no error mode)`)
return false
}
throw Object.assign(new Error(`Unsupported option "${key}"`), {extras:true})
}
//Check permissions
if (!Array.isArray(extras.features))
throw new Error(`metrics/extras > ${name} > ${input} > extras.features is not an array`)
const missing = required.filter(permission => !extras.features.includes(permission))
if (missing.length > 0) {
console.debug(`metrics/extras > ${name} > ${input} > missing permissions [${missing}], skipping`)
return false
}
return true
}
}
@@ -576,7 +602,9 @@ metadata.to = {
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
},
yaml(key, {name = ""} = {}) {
const parts = [key.replaceAll(".", "_")]
const parts = []
if (key !== "enabled")
parts.unshift(key.replaceAll(".", "_"))
if (name)
parts.unshift((name === "base") ? name : `plugin_${name}`)
return parts.join("_")

View File

@@ -78,63 +78,68 @@ export default async function({log = true, sandbox = false, community = {}} = {}
logger("metrics/setup > load package.json > success")
//Load community templates
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
logger("metrics/setup > parsing community templates list")
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
}
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
//Clean remote repository
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
//Download community templates
for (const template of conf.settings.community.templates) {
try {
//Parse community template
logger(`metrics/setup > load community template ${template}`)
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
logger(`metrics/setup > run ${command}`)
//Clone remote repository
processes.execSync(command, {stdio: "ignore"})
//Extract template
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, `@${name}`), {recursive: true, force: true})
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
//JavaScript file
if (trust)
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
logger(`metrics/setup > removing @${name}/template.mjs`)
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
if (inherit) {
logger(`metrics/setup > @${name} extends from ${inherit}`)
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
}
else {
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
if ((conf.settings.extras?.features?.includes("metrics.setup.community.templates"))||(conf.settings.extras?.features === true)||(conf.settings.extras?.default)) {
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
logger("metrics/setup > parsing community templates list")
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
}
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
//Clean remote repository
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
//Download community templates
for (const template of conf.settings.community.templates) {
try {
//Parse community template
logger(`metrics/setup > load community template ${template}`)
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
logger(`metrics/setup > run ${command}`)
//Clone remote repository
processes.execSync(command, {stdio: "ignore"})
//Extract template
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, `@${name}`), {recursive: true, force: true})
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
//JavaScript file
if (trust)
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
logger(`metrics/setup > removing @${name}/template.mjs`)
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
if (inherit) {
logger(`metrics/setup > @${name} extends from ${inherit}`)
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
}
else {
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
}
}
}
}
else {
logger(`metrics/setup > @${name}/template.mjs does not exist`)
}
else {
logger(`metrics/setup > @${name}/template.mjs does not exist`)
}
//Clean remote repository
logger(`metrics/setup > clean ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
logger(`metrics/setup > loaded community template ${name}`)
}
catch (error) {
logger(`metrics/setup > failed to load community template ${template}`)
logger(error)
//Clean remote repository
logger(`metrics/setup > clean ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
logger(`metrics/setup > loaded community template ${name}`)
}
catch (error) {
logger(`metrics/setup > failed to load community template ${template}`)
logger(error)
}
}
}
else {
logger("metrics/setup > no community templates to install")
}
}
else {
logger("metrics/setup > no community templates to install")
logger("metrics/setup > community templates are disabled")
}
//Load templates
@@ -188,6 +193,18 @@ export default async function({log = true, sandbox = false, community = {}} = {}
//Load metadata
conf.metadata = await metadata({log})
//Modes
if ((!conf.settings.modes)||(!conf.settings.modes.length))
conf.settings.modes = ["embed", "insights"]
logger(`metrics/setup > setup > enabled modes ${JSON.stringify(conf.settings.modes)}`)
//Allowed outputs formats
if ((!conf.settings.outputs)||(!conf.settings.outputs.length))
conf.settings.outputs = metadata.inputs.config_output.values
else
conf.settings.outputs = conf.settings.outputs.filter(format => metadata.inputs.config_output.values.includes(format))
logger(`metrics/setup > setup > allowed outputs ${JSON.stringify(conf.settings.outputs)}`)
//Store authenticated user
if (conf.settings.token) {
try {

View File

@@ -125,6 +125,49 @@ export function formatters({timeZone} = {}) {
return license.nickname ?? license.spdxId ?? license.name
}
/**Error formatter */
format.error = function(error, {descriptions = {}, ...attributes} = {}) {
try {
//Extras features error
if (error.extras)
throw {error: {message: error.message, instance: error}}
//Already formatted error
if (error.error?.message)
throw error
//Custom description
let message = "Unexpected error"
if (descriptions.custom) {
const description = descriptions.custom(error)
if (description)
message += ` (${description})`
}
//Axios error
if (error.isAxiosError) {
//Error code
const status = error.response?.status
message = `API error: ${status}`
//Error description (optional)
if ((descriptions)&&(descriptions[status]))
message += ` (${descriptions[status]})`
else {
const description = error.response?.data?.errors?.[0]?.message ?? error.response.data?.error_description ?? error.response?.data?.message ?? null
if (description)
message += ` (${description})`
}
//Error data
console.debug(error.response.data)
error = error.response?.data ?? null
throw {error: {message, instance: error}}
}
throw {error: {message, instance: error}}
}
catch (error) {
return Object.assign(error, attributes)
}
}
return {format}
}